C++ Chapter 2: Data Types and Arithmetic Operators
Integer Numbers 整型
变量声明、初始化和赋值
有几种整数类型,而int
是最常用的一种。int
类型的变量可以如下声明。其他类型类似。
int n; //声明一个int变量n
这行代码是用来声明和初始化一个变量的。
int n = 10; //声明并初始化
以下一行代码与前一行非常相似。但是操作是不同的。但它没有初始化。第一行是声明一个变量,而第二行是赋值。赋值是一种与初始化不同的操作。这两段源代码是等效的。它们都声明了一个变量,然后其值为10
。但如果数据类型不是基本类型(如int
、float
等),而是复合类型(例如class
类型),初始化和赋值这两个操作可能具有不同的行为。原因在于初始化函数中的操作可能与赋值函数中的操作不同。可以在运算符重载部分找到相关信息。
int n; //declare a variable, its value may be a random one
n = 10; //assign (not initialize) a value to the variable
变量还可以以以下两种方式初始化。第二种是根据C++11标准引入的。它只能在C++11标准或更新的标准下编译。
int n (10); //initialize
int n {10}; //initialize, C++11 standard
未初始化变量
必须强调这里的未初始化的危险性。如果一个变量没有被初始化,C/C++编译器将不会报告错误,甚至不会警告。Java编译器会对未初始化的变量报告错误。与Java编译器相比,C/C++编译器不会那么严格。在C/C++标准中,未初始化变量的值也没有明确定义。可能会从未初始化的变量中得到一个随机值。
//init.cpp
#include <iostream>
using namespace std;
int main()
{
int num1; //bad: uninitialized variable
int num2; //bad: uninitialized variable
cout << "num1 = " << num1 << endl;
cout << "num2 = " << num2 << endl;
}
例子init.cpp
中有两个未初始化的变量num1
和num2
。它们在声明后立即被打印出来。我在我的计算机上尝试了这个例子,输出如下。如果在其他的计算机上运行它,输出可能会有所不同。
num1 = 1
num2 = 74456160
在程序中使用未初始化的变量可能导致不可预测的行为。虽然在某些情况下程序可能看起来工作正常,但在其他情况下可能会意外失败。如果程序很大,看起来工作正常,但在将其迁移到不同的平台时出现问题,从大型程序中调试随机错误可能会变成一场噩梦。为避免这种情况,重要的是仔细编写每一行代码,并确保所有变量都得到了适当的初始化。
Overflow 溢出
C和C++标准没有规定int
的宽度。在大多数平台上,它是32位的,而int
等同于signed int
。int
的范围是 [-231, 231-1]. 最大值 231 (2,147,483,648) 并不是一个难以达到的大数字。
在例子overflow.cpp
中,两个整数相乘,它们的值都是56789
。乘积c
应该是3,224,990,521
,但程序将打印出c = -1069976775
。这是一个负数,显然是错误的。
//overflow.cpp
#include <iostream>
using namespace std;
int main()
{
int a = 56789;
int b = 56789;
int c = a * b;
cout << "c = " << c << endl;
return 0;
}
编译命令和输出结果如下:
$ g++ overflow.cpp
$ ./a.out
c = -1069976775
数字 56789
在十六进制格式中表示为 0xDDD5
,是一个长度为16位的数字。它们的乘积是一个32位的数字 0xC0397339
。它的第三十二位是1。结果 0xC0397339
被复制到一个 int
变量中,其最高位1被视为有符号整数的符号位。这就是为什么输出是一个负数的原因。如果我们对变量 c
使用 unsigned int
,那么结果 0xC0397339
将被视为无符号32位整数。这将是我们期望的结果。
int a = 56789;
int b = 56789;
unsigned int c = a * b; // the value of C will be a positive number 0xC0397339 (3224990521)
期望一个 int
变量能够容纳任意整数是不现实的。在选择变量的数据类型之前,必须仔细考虑其数据范围。
Different Integer Types 不同的整型
在C和C++标准中,int
的位宽并没有固定。标准只要求 int
至少应有16位。在大多数现代平台上,它有32位。除了 int
,还有 short int
、long int
和 long long int
(long long int
是在 C++11 中引入的)。short int
有16位。int
有16或32位。long int
有32或64位。long long int
有64位。
char
也是一个经常使用的整数类型。有人可能会感到困惑,认为 char
只是用于字符。由于字符被编码为整数值,char
实际上是一种整数类型,有8位。char
对于英文字符来说足够宽,但对于中文、日文、韩文和一些其他字符来说可能不够。char16_t
和 char32_t
在 C++11 中引入,分别用于表示16位和32位的字符范围。
以下三行源代码是等效的。
char c = 'C'; // its ASCII code is 80
char c = 80; // in decimal
char c = 0x50; // in hexadecimal
16位和32位的字符类型可以按照以下方式声明和初始化。
char16_t c1 = u'于'; //C++11
char32_t c2 = U'于'; //C++11
The data widths of different integers are listed in the following table. For more details please visit https://en.cppreference.com/w/cpp/language/types
Integer type | Width in bits |
---|---|
char |
8 |
short (short int ) |
16 |
int |
16 or 32 |
long (long int ) |
32 or 64 |
long long (long long int ) |
64 |
Signed and Unsigned Integers 有符号和无符号整型
signed
或unsigned
可以在整数类型名称前用于指示整数是有符号的还是无符号的。当整数是有符号的时,关键字signed
可以省略。这意味着int
表示signed int
,而short
表示signed short
。但有一个例外。char
并不总是表示signed char
,在一些平台上它表示unsigned char
。我强烈建议始终使用signed char
或unsigned char
,而不使用char
。
如果整数是有符号的,最高位(对于int
来说是第32位)将是其符号位。如果符号位是1,则它是一个负数;如果是0,则是一个正数。有符号整数和无符号整数在下图中显示。
![Illustration for 32-bit integers. The highest bit is the sign bit or signed int
.](images/integer-sign-bit.pn
Boolean Type 布尔类型
在C++中(而不是在C中),布尔类型是 bool
,它的值可以是 true
或 false
。true
的整数值是1,而 false
的值是0。bool
的宽度是1字节,而不是1位。这意味着 bool
变量在数据上占用1字节,但只使用最低的1位。
由于在C/C++中 bool
实际上是一种整数,因此 bool
可以转换为其他类型的整数。
bool b = true;
int i = b; // the value of i is 1.
整数也可以转换为 bool
,非零值(甚至是浮点数)将被转换为 true
,如下所示的源代码。但不建议这样做,因为这可能会产生误导。
bool b = -256; // unrecommended conversion. the value of b is true
bool b = (bool)0.4; // unrecommended conversion. the value of b is true
以下等效的源代码更容易理解。表达式 (-256 != 0)
用于判断 -256
是否等于 0
,其结果是 true
或 false
。
bool b = (-256 != 0);
在C标准中并没有布尔类型。一些旧程序可能使用typedef
来创建自定义的布尔类型。
typedef char bool;
#define true 1
#define false 0
如果你使用纯C语言并想使用 bool
,你可以包含C99引入的一个头文件。这比使用 typedef
定义更好。在C99标准之前,C语言没有 bool
。C99引入了 _Bool
作为布尔类型。此外,bool
被定义为 _Bool
的别名,true
定义为 1
,false
定义为 0
,通过 <stdbool.h>
头文件中的宏来实现,该头文件也是C99引入的。
#include <stdbool.h>
size_t
Type
另一个经常使用的整数类型是 size_t
。它是 sizeof
运算符的类型。它可以存储任何类型的理论上可能的对象的最大大小。计算机内存在过去的几十年中不断增加,并将在未来继续增加。我们经常需要一个整数变量来存储特定内存块的数据大小。unsigned int
是不够的,因为它的最大值是232,即4GB的内存。malloc()
函数,它可以分配 size
字节的内存,接受一个 size_t
类型的参数。如果其参数 size
是 int
类型,那么它的最大内存为2GB,对于 unsigned int
为4GB。
void* malloc( size_t size );
size_t
的位宽取决于平台。对于大多数现代平台来说,它是64位的。由于它可以存储任何类型的理论上可能的对象的最大大小,您可以安全地将其用于内存大小。
sizeof
Operator
sizeof
可以返回类型或对象/变量的字节大小。例子 size.cpp
演示了如何使用 sizeof
来获取不同数据类型和变量的宽度。它不仅可以用于基本类型,还可以接受复合类型或任何变量作为输入。
//size.cpp
#include <iostream>
using namespace std;
int main()
{
int i = 0;
short s = 0;
cout << "sizeof(int)=" << sizeof(int) << endl;
cout << "sizeof(i)=" << sizeof(i) << endl;
cout << "sizeof(short)=" << sizeof(s) << endl;
cout << "sizeof(long)=" << sizeof(long) << endl;
cout << "sizeof(size_t)=" << sizeof(size_t) << endl;
return 0;
}
The output of the example on my computer is
sizeof(int)=4
sizeof(i)=4
sizeof(short)=2
sizeof(long)=8
sizeof(size_t)=8
确实,尽管 sizeof
的语法看起来像一个函数,但它实际上是一个运算符,而不是一个函数。函数不能将数据类型作为参数。sizeof(int)
是将类型 int
作为其输入。为了得到表达式(或变量)的宽度,也可以使用 sizeof expression
。但是 sizeof type
是无效的。以下代码展示了在不使用括号的情况下如何使用它。
//size2.cpp
#include <iostream>
using namespace std;
int main()
{
int i = 0;
short s = 0;
cout << "sizeof int =" << sizeof int << endl; // error
cout << "sizeof i =" << sizeof i << endl; // okay
cout << "sizeof short =" << sizeof s << endl; // okay
cout << "sizeof long =" << sizeof long << endl; // error
cout << "sizeof size_t =" << sizeof size_t << endl; // error
return 0;
}
Fixed Width Integer Types
同一整数类型的不同位宽可能导致程序在不同平台上难以移植。自从C++11以来,在<cstdint>
中引入了一些固定宽度的整数类型,它们包括int8_t
、uint8_t
、int16_t
、uint16_t
、int32_t
、uint32_t
、int64_t
、uint64_t
等。还有一些有用的宏,如INT_MAX
、INT_MIN
、INT8_MAX
、UINT8_MAX
等。这些整数类型可以显式声明变量的宽度。
Floating-Point Numbers 浮点类型
在介绍浮点数之前,我想先介绍一下以下的例子 float.cpp
。f1
被赋值为 1.2
,然后乘以 1000000000000000
(15个零)。我们可能认为 f2
应该是 1200000000000000
。但如果我们以非常高的精度打印 f1
和 f2
,我们会发现一个可怕的事实。f2
并不是我们期望的结果,甚至 f1
也不完全等于 1.2
。
//float.cpp
#include <iomanip>
using namespace std;
int main()
{
float f1 = 1.2f;
float f2 = f1 * 1000000000000000;
cout << std::fixed << std::setprecision(15) << f1 << endl;
cout << std::fixed << std::setprecision(1) << f2 << endl;
return 0;
}
The output is:
1.200000047683716
1200000038076416.0
我们可能认为计算机总是准确的。但事实并非如此。浮点运算总是会引入一些微小的误差。这些误差无法消除。我们能做的是管理它们,以免造成问题。
为什么浮点数据不能准确呢?我们可以深入了解浮点格式。下图显示了一个32位浮点数的示例[^float_format]。其中包含1个符号位、8个指数位和23个小数位。
一个32位浮点数的值是 $(-1)^{b_{31}} \times 2^{(b_{30}b_{29} \dots b_{23})2 - 127} \times (1.b{22}b_{21} \dots b_0)_2$
。它的最小正规数是$\pm 1.175,494,3\times 10^{-38}$,最大正规数是$\pm 3.402,823,4\times 10^{38}$。由于32位浮点数具有比32位整数更大的数据范围,其精度受到限制,有时甚至比32位整数更差。在0和1.0之间有无穷多个数字。我们不能用有限长度的二进制向量表示无穷多的数字。只有有限数量的数字在0和1.0之间可以由浮点数表示。其余的数字在浮点数的空间中消失。根据浮点数的方程,任何二进制代码的组合都无法准确表示 1.2
,只能得到一个近似值 1.200000047683716
。
由于浮点数存在精度误差,使用 ==
来比较两个浮点数是一个糟糕的选择。如果两个数字之间的差异小于一个非常小的数,例如 FLT_EPSILON
或 DBL_EPSILON
分别适用于 float
和 double
,我们可以认为它们是相等的。
if (f1 == f2) //bad
{
// ...
}
if (fabs(f1 - f2) < FLT_EPSILON) // good
{
// ...
}
以下示例 precision.cpp
展示了如果将一个大数与一个小数相加,结果将与较大的那个相同。这是由于精度问题引起的。
//precision.cpp
#include <iostream>
using namespace std;
int main()
{
float f1 = 23400000000;
float f2 = f1 + 10;
cout.setf(ios_base::fixed, ios_base::floatfield); // fixed-point
cout << "f1 = " << f1 << endl;
cout << "f2 = " << f2 << endl;
cout << "f1 - f2 = " << f1 - f2 << endl;
cout << "(f1 - f2 == 0) = " << (f1 - f2 == 0) << endl;
return 0;
}
The output:
f1 = 23399999488.000000
f2 = 23399999488.000000
f1 - f2 = 0.000000
(f1 - f2 == 0) = 1
有两种典型的浮点类型,float
和 double
。它们分别用于单精度浮点数和双精度浮点数。前者有32位,后者有64位。double
比 float
具有更广的范围和更好的精度。但是,float
的运算通常比 double
快得多。
请注意除法运算,如果除数是零,结果可能是 INF
或 NAN
。示例 nan.cpp
演示了如何生成无效的浮点数。
//nan.cpp
#include <iostream>
using namespace std;
int main()
{
float f1 = 2.0f / 0.0f;
float f2 = 0.0f / 0.0f;
cout << f1 << endl;
cout << f2 << endl;
return 0;
}
Output:
inf
nan
Constant Numbers and Constant Variables
常数可以采用十进制、八进制或十六进制表示。整数常数,如 95
,将被解释为 int
类型。95u
将是一个 unsigned int
,而 95ul
将是一个 unsigned long
。浮点数可以是 3.141592
或 6.02e13
,但这两个数字都是 double
类型。对于 float
类型的数字,需要添加后缀 f
,如 6.02e13f
。以下是一些更多的示例:
95 // decimal
0137// octal
0x5F // hexadecimal
95 // int
95u // unsigned int
95l // long
95ul // unsigned long
95lu // unsigned long
3.14159 // 3.14159
6.02e13 // 6.02 x 10^13
1.6e-9 // 1.6 x 10^-9
3.0 // 3.0
6.02e13f // float
6.02e13 // double
6.02e13L // long double
如果一个变量或对象被 const 修饰,它就无法被修改。在定义时必须对其进行初始化。
const float pi = 3.1415926f;
pi += 1; //error!
Arithmetic Operators 算术运算符
算术运算符列在下表中。
Operator name | Syntax |
---|---|
unary plus | +a |
unary minus | -a |
addition | a + b |
subtraction | a - b |
multiplication | a * b |
division | a / b |
modulo | a % b |
bitwise NOT | ~a |
bitwise AND | a & b |
bitwise OR | a \| b |
bitwise XOR | a ^ b |
bitwise left shift | a << b |
bitwise right shift | a >> b |
运算符及其操作数可以连接在一起形成表达式。有些是简单的,比如 a + b
,而有些可能很长,包含多个运算符,比如 a + b * c / (e + f)
。表达式可以赋值给一个变量。
int result = a + b * c / (e + f);
Data Type Conversions 数据类型转换
在C和C++中有许多数据类型,但只有四种类型用于算术运算。它们是 int
、long
、float
和 double
。如果操作数不是这四种类型之一,它们将被隐式地转换为其中一种。这可以通过以下代码来解释。char1
和 char2
的类型都是 unsigned char
,它们的最大值是 255
。如果 char1 + char2
的操作是在 unsigned char
类型中进行的,那么和 256
将会溢出,因为在 unsigned char
中没有 256
。在这个操作中,char1
和 char2
首先会被隐式转换为 int
,然后两个 int
值相加。r.
unsigned char char1 = 255;
unsigned char char2 = 1;
int num = char1 + char2; // why num = 256?
以下的源代码与之前的代码等效。
unsigned char char1 = 255;
unsigned char char2 = 1;
int num = (int)char1 + (int)char2; //convert to explicitly
在这个例子中,float
数字 1.2f
首先会被转换为 double
,然后两个数字相加。它们的和是 4.6
(而不是 4.6f
)。最后一步是将一个 double
数字 4.6
赋给一个整数变量 num
。编译器会发出警告消息,指示赋值可能会丢失数据。
int num = 1.2f + 3.4; // -> 1.2 + 3.4 -> 4.6 -> 4
等效的代码如下,而且编译器不会发出警告消息,因为您明确将结果转换为 int
。编译器会认为您清楚自己在做什么。在大多数情况下,建议使用显式类型转换。
int num = (int)((double)1.2f + 3.4);
程序员在进行数据类型转换时应该非常小心,因为这可能导致数据丢失。一个典型的例子是(int)3.6
将得到整数3
,浮点数的小数部分将被丢失。
下面的代码也可能让我们产生误导。由于17
和5
都是int
,所以这个操作是一个int
的除法,而不是一个float
的除法。表达式17 / 5
的结果是整数3
,而不是浮点数3.4f
。这就是为什么float_num
是3.0f
而不是3.4f
的原因。
float float_num = 17 / 5; // f = 3.0f, not 3.4f.
当我们按照从 char
-> short
-> int
-> long
-> float
-> double
-> long double
的方向进行数字转换时,通常情况下是不会有数据损失的。但如果是在相反的方向,从 long double
转换到 char
,这会导致数据损失,编译器大多数时候会发出警告。但这并不总是正确的。在将一些大整数数字从 int
转换为 float
时,可能会丢失精度,如下面的代码所示。
int num_int1 = 100000004;
float num_float = num_int1;
int num_int2 = num_float; // num_int2 = 100000000
auto
Type
占位类型说明符 auto
是在 C++11 中引入的。带有 auto
的变量的实际类型是由其初始化器推导出来的。我们可以按照以下方式声明和初始化一些变量。
auto a = 2; // type of a is int
auto bc = 2.3; // type of b is double
auto c; //valid in C, but not in C++
auto d = a * 1.2; // type of d is double
一旦推导出 auto
变量的类型,它的类型将被固定,不会再次改变。在下面的源代码中,a
被初始化为 int
类型,然后被赋值为 double
类型的 2.3
。2.3
首先会被隐式转换为 int
值 2
,然后被赋给变量 a
。因此,a
的值应该是 2
,而不是 2.3
,因为 a
是 int
类型。在使用 auto
时,请注意真实的数据类型。
auto a = 2; // type of a is int
a = 2.3; // Will a be converted to a double type variable? NO!
Assignment Operators
除了=
之外,还有一些复合赋值运算符,如下表所示。当我们改变运算符的左值时,它们非常方便。
Assignment expression | Equivalent expression |
---|---|
a = b |
|
a += b |
a = a + b |
a -= b |
a = a - b |
a *= b |
a = a * b |
a /= b |
a = a / b |
a %= b |
a = a % b |
a &= b |
a = a & b |
a \|= b |
a = a \| b |
a ^= b |
a = a ^ b |
a <<= b |
a = a << b |
a >>= b |
a = a >> b |
Special Notes
在C或C++编程中,开发者被期望了解不同数据类型的所有细节。与Python不同,变量中的值在int32
的边界之外甚至可以增加,真实的存储将自动适应。在Java中,会提供更多的警告和错误以防止溢出或精度问题。但是在C或C++中,警告要少得多。你必须非常小心处理不同的数据类型,因为你的程序可能没有编译错误,但结果是错误的。
此外,我们还需要改变计算机是准确的这种观念。如果使用浮点数进行计算,就一定会存在一些微小的误差。我们应该意识到微小的误差总是存在的。我们能做的就是控制这些错误,因为它们很难消除。
再强调一下,数字可能超出范围,计算可能存在错误,结果可能是整数除法,而不是浮点数除法等。在编写一行源代码时,请尽量探索所有可能性以及指令在其中的工作方式。更多思考和深刻理解,会减少错误。
Reference:
[1] 《C/C++从基础语法到优化策略》https://www.bilibili.com/video/BV1Vf4y1P7pq/?spm_id_from=333.999.0.0&vd_source=eb2aff91d0c138676172d1f9746b9f1e
[2] https://github.com/ShiqiYu/CPP