C++ Chapter 2: Data Types and Arithmetic Operators

C++ Chapter 2: Data Types and Arithmetic Operators

Integer Numbers 整型

变量声明、初始化和赋值

有几种整数类型,而int是最常用的一种。int类型的变量可以如下声明。其他类型类似。

int n; //声明一个int变量n

这行代码是用来声明和初始化一个变量的。

int n = 10; //声明并初始化

以下一行代码与前一行非常相似。但是操作是不同的。但它没有初始化。第一行是声明一个变量,而第二行是赋值。赋值是一种与初始化不同的操作。这两段源代码是等效的。它们都声明了一个变量,然后其值为10。但如果数据类型不是基本类型(如intfloat等),而是复合类型(例如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中有两个未初始化的变量num1num2。它们在声明后立即被打印出来。我在我的计算机上尝试了这个例子,输出如下。如果在其他的计算机上运行它,输出可能会有所不同。

num1 = 1
num2 = 74456160

在程序中使用未初始化的变量可能导致不可预测的行为。虽然在某些情况下程序可能看起来工作正常,但在其他情况下可能会意外失败。如果程序很大,看起来工作正常,但在将其迁移到不同的平台时出现问题,从大型程序中调试随机错误可能会变成一场噩梦。为避免这种情况,重要的是仔细编写每一行代码,并确保所有变量都得到了适当的初始化。

Overflow 溢出

C和C++标准没有规定int的宽度。在大多数平台上,它是32位的,而int等同于signed intint的范围是 [-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 intlong intlong long intlong long int 是在 C++11 中引入的)。short int 有16位。int 有16或32位。long int 有32或64位。long long int 有64位。

char也是一个经常使用的整数类型。有人可能会感到困惑,认为 char 只是用于字符。由于字符被编码为整数值,char 实际上是一种整数类型,有8位。char 对于英文字符来说足够宽,但对于中文、日文、韩文和一些其他字符来说可能不够。char16_tchar32_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 有符号和无符号整型

signedunsigned可以在整数类型名称前用于指示整数是有符号的还是无符号的。当整数是有符号的时,关键字signed可以省略。这意味着int表示signed int,而short表示signed short。但有一个例外。char并不总是表示signed char,在一些平台上它表示unsigned char。我强烈建议始终使用signed charunsigned 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,它的值可以是 truefalsetrue 的整数值是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,其结果是 truefalse

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 定义为 1false 定义为 0,通过 <stdbool.h> 头文件中的宏来实现,该头文件也是C99引入的。

#include <stdbool.h>

size_t Type

另一个经常使用的整数类型是 size_t。它是 sizeof 运算符的类型。它可以存储任何类型的理论上可能的对象的最大大小。计算机内存在过去的几十年中不断增加,并将在未来继续增加。我们经常需要一个整数变量来存储特定内存块的数据大小。unsigned int 是不够的,因为它的最大值是232,即4GB的内存。malloc() 函数,它可以分配 size 字节的内存,接受一个 size_t 类型的参数。如果其参数 sizeint 类型,那么它的最大内存为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_tuint8_tint16_tuint16_tint32_tuint32_tint64_tuint64_t等。还有一些有用的宏,如INT_MAXINT_MININT8_MAXUINT8_MAX等。这些整数类型可以显式声明变量的宽度。

Floating-Point Numbers 浮点类型

在介绍浮点数之前,我想先介绍一下以下的例子 float.cppf1 被赋值为 1.2,然后乘以 1000000000000000(15个零)。我们可能认为 f2 应该是 1200000000000000。但如果我们以非常高的精度打印 f1f2,我们会发现一个可怕的事实。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_EPSILONDBL_EPSILON 分别适用于 floatdouble,我们可以认为它们是相等的。

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

有两种典型的浮点类型,floatdouble。它们分别用于单精度浮点数和双精度浮点数。前者有32位,后者有64位。doublefloat 具有更广的范围和更好的精度。但是,float 的运算通常比 double 快得多。

请注意除法运算,如果除数是零,结果可能是 INFNAN。示例 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.1415926.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++中有许多数据类型,但只有四种类型用于算术运算。它们是 intlongfloatdouble。如果操作数不是这四种类型之一,它们将被隐式地转换为其中一种。这可以通过以下代码来解释。char1char2 的类型都是 unsigned char,它们的最大值是 255。如果 char1 + char2 的操作是在 unsigned char 类型中进行的,那么和 256 将会溢出,因为在 unsigned char 中没有 256。在这个操作中,char1char2 首先会被隐式转换为 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,浮点数的小数部分将被丢失。

下面的代码也可能让我们产生误导。由于175都是int,所以这个操作是一个int的除法,而不是一个float的除法。表达式17 / 5的结果是整数3,而不是浮点数3.4f。这就是为什么float_num3.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.32.3 首先会被隐式转换为 int2,然后被赋给变量 a。因此,a 的值应该是 2,而不是 2.3,因为 aint 类型。在使用 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

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦