Skip to content

2. C++ 中的数据

By FunnyAWM

1. 变量的数据类型与名称限定

第一章中,我们详细解释了 C++ 中变量的创建方法,那么为什么这里还要再讲解一次变量呢?那是因为变量中有很多额外的东西,比如变量的数据类型、CV 限定符等。这里我们继续来详细讲解 C++ 中变量的数据类型。

在 C++ 中,基本的数类型可以分为以下几种:

  • 整形:用于存储整数;
  • 浮点型:用于存储小数;
  • 字符型:用于存储各种字符;
  • 布尔型:用于存储逻辑真或假;
  • void 类型:可用于函数不返回值,在 C 中也用来作为动态内存分配的通用类型。

C++ 中的变量名称规则如下:

  • 变量名只能使用字母或下划线作为开头;
  • 变量名只能包含字母、数字与下划线;
  • C++ 的变量名是大小写敏感的,所以拼写相同,但大小写顺序不同的变量会被认作不同的变量;
  • 不能使用语言保留字作为变量名;

接下来我们来逐个讲解这些数据类型。

1. 整形

整形用于存储整数。这里的整形可以分为很多类型,每个类型都有不同的内存占用量和存储值范围,具体如下:

类型占用范围
short2 字节-215~215-1
int4 字节-231~231-1
long4 字节(32 位)或 8 字节(64 位)32 位系统同上(int64 位系统同下(long long)
long long8 字节-263~263-1

上面的每个类型都可以加 unsigned(无符号)修饰,变为 unsigned short、unsigned、unsigned long 与 unsigned long long。无符号修饰的类型不能出现负数,相对的,这些类型能存储的范围不变,但起始范围从 0 开始。例如:无符号 short 的范围从 - 215~215-1 变为 0~216-1,以此类推。

从上面的说明我们可以发现,这些数据类型的范围有些会随着操作系统与 CPU 支持的位数发生变化。在一些嵌入式项目中,这样的变化对软件的可移植性是十分致命的。

在 Arduino 中,Uno R3 与 Uno R4 使用的 CPU 分别是 ATmega328P 与 RA4M1,其中 ATmega328P 是 AVR 架构的 8 位 CPU,而 RA4M1 则是 32 位的 ARM Cortex-M4 CPU。这两个开发板中的 int 范围不同 ——R3 的 int 只占 2 个字节,而 R4 的 int 占用 4 个字节。如果 R4 中的某个整形变量的值大于 215-1,那么就算 R3 使用了与 R4 相同的基础库,R4 的程序也不能直接用于 R3—— 这样的变量在 R3 中会直接导致变量溢出(将在稍后解释变量溢出的情况导致程序无法正常工作。秦始皇哪见过这样的冥场面啊

为了解决这些由平台相关的问题导致的不统一问题,C 语言引入了 inttype.h 用于统一整形位数,以在不同的平台上获得相同的整形长度。在 C++ 中,我们能用如下方法引入这个头文件:

cpp
#include <cstdint>

这个头文件中提供了从 8 bits 到 64 bits 的完整整形类型支持,包括有符号类型和无符号类型。例如,下面声明了一个 8 bits 长的有符号整形变量:

cpp
int8_t value;

这里的 8 代表位数,如果需要声明 16 bits 的变量,则数据类型需要写成 int16_t,32 bits 为 int32_t,以此类推。如果需要声明的是无符号类型,则在对应的有符号型前加一个字母 u 即可。例如,下面声明了一个 8 bits 长的无符号整形变量:

cpp
uint8_t value;

整形的溢出情况

在上面,我们说明了各类型的整形能存储的最小与最大值,如果我们实际存储的值超过了上面的限制,会发生什么呢?假设我们写了下面的程序:

cpp
#include <cstdint>
#include <iostream>

using std::cout;

int main() {
    uint16_t money = 0;
    cout << "我现在有 " << money << " 块钱\n";
    cout << "现在我又花掉了一块钱\n";
    money -= 1;
    cout << "我现在有 " << money << " 块钱,真好!\n";
    return 0;
}

程序的执行结果如下:

我现在有 0 块钱
现在我又花掉了一块钱
我现在有 65535 块钱,真好!

为什么会这样?这就是整形溢出的结果。在整形中,如果存储的值超出了变量的存储范围,执行时,超出的范围将重新从最小值开始算起。这与计算机如何计算与存储整数有关。在计算机中,任何数据都以二进制形式存储,在上面的例子中,uint16_t 存储为一个拥有 16 个二进制位的整数。其二进制值与十进制值换算相同。计算机组成原理告诉我们,计算机在进行任何运算时,本质上都在进行二进制运算。我们可以假设这里进行的也是加法运算(减 1 可以看做加上 - 1那么计算方式就应该如下:

    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+   1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 (计算机中全1代表有符号的-1)
-----------------------------------
    1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

因此我们得到了 16 位 1。用计算器计算 216-1,结果正好是 65535,与前面的 “错误” 结果完全一致!

由此我们可以得出,整形的溢出现象,正是整形进行高位截断后的结果。

高位截断:指整形无法容纳比自身最大(或最小)值更大(或更小)的值,因此裁切掉二进制超出部分的现象。这是不是也能叫做和溢位

2. 浮点

浮点数表示能够带有小数的类型,这些类型能够存储比整形更大与更小的值,对应的,这样做的代价就是内存占用会比整形高一些。

浮点数有两种表示方式:常规法与 e 方法。常规方法能够让我们以小数的形式存储浮点数,例如 114.514、3.14159 等;e 方法与科学计数法的表示方法相同。在初中数学中,我们已经了解了科学计数法非常适合用于记录非常大与非常小的数。例如,电子的质量是 9.11 * 10-31kg,在 C++ 中可以写作:

cpp
double weightOfElectron = 9.11e-31

为什么浮点数要用这种方式来书写与存储呢?这与计算机如何存储浮点数有关。计算机需要高效表示和处理实数,但二进制系统无法直接表示小数,因此需要特殊的存储方式。

与科学计数法类似,计算机也使用相同的思想存储浮点数。区别在于,由于计算机内部以二进制存储数据,所以科学计数法中的 x * 10y要更换为 x * 2y。计算机组成原理的知识告诉我们,计算机存储浮点数遵循的是 IEEE 754 标准,以 32 bits 浮点数为例,在计算机的内存中,是这样存储的:

  • 符号位(S占第一位,0 代表存储的是正数,1 代表负数;
  • 指数位(E中间 8 位,存储指数,也就是 x * 2y中 y 的部分。指数位根据 IEEE 754 标准需要加上一个偏移量用于指定正负,偏移量的计算公式为 2d-1-1,其中 d 为指数位的位数;
  • 尾数位(M最后 23 位,存储小数点后的有效数字,也就是 x * 2y中 x 的部分。由于浮点数的尾数第一位永远为 1(第一位存储 0 没有意义,计算机会自动将数字的小数点向前移动所以这个 1 一般忽略不写,只记录小数部分。

在 64 bits 浮点数中,仅有 E 增加至 11 位、M 增加至 52 位,用来表示更大的数值,其余没有变化。在 Java 等语言中,浮点数同时还支持了 Inf(无限大NaN(非数字)的表示方式。

从上面的表述我们可以看出,单精度浮点数与双精度浮点数的内存占用如下:

  • 单精度浮点:占用了 4 个字节;
  • 双精度浮点:占用了 8 个字节。

上面的表述同时解释了浮点数为什么被称作 “浮点数在这样的数字中,小数点能够根据指数位的不同而变动,也就是说小数点是浮动的。

3. 字符型

顾名思义,字符型是为了存储字符来设计的。这不废话么

在 C++ 中,一般的 char 型只能存储 ASCII 字符集中所有的字符。ASCII 中共定义了 128 个字符,主要用于显示英语及其他语言。在 C++ 中,字符型存储的字符是字符对应的 ASCII 码,本质上是整数,因此 char 型也被用来存储范围不大,但需要小内存占用的整数。

到这里,我们也可以看出一个 char 型的弱点:不能存储 ASCII 字符以外的任何字符。为了解决这个问题,C++ 在后续推出了宽字符型(wchar_t它提供了更长的长度,非常适合存储中文、日文、emoji 等使用 Unicode 表示的字符。

还记得我们说过 char 型本质上存储的是整数吗?与整形相同,char 也允许定义为无符号型,只需要将这样的字符定义为 unsigned char 型即可。同时,为了克服宽字符型的可移植性问题(参考前面整形的问题,因为字符型本质上是整形C++ 同样引入了可以指定位数的字符型,即 char16_t 与 char32_t。

4. 布尔型

布尔型又被称为 boolean 型或 bool 型,主要表示逻辑上的真与假,在 C++ 中用 bool 表示布尔型。布尔型只能存储 2 个值:0 与 1。其中 0 代表假,即条件不成立或结论不正确;1 代表真,即条件成立或结论正确。这样的值在后面要讲解的判断语句中十分重要且有用。

值得一提的是,C 语言在 C99 标准以前没有原生的 bool 类型,在 C99 中,添加了 stdbool.h,这个文件包含了对布尔值的支持。

2. C++ 中的常量

在 C++ 中,声明常量的方式是在变量名前加上 const。例如:

cpp
const int value = 114514;

其中 const 描述了这个数据是不可变的,也就是常量。

常量在 C++ 程序的编写中十分有用。例如我们需要定义圆周率 π 来做圆相关的计算,只需要写:

cpp
const double PI = 3.141592654

这样能够确保我们不会意外改变这个常量,由此保证了计算的正确性。

3. C++ 中的算术运算与类型转换

1. C++ 的算术运算

C++ 使用运算符来完成数值的计算。C++ 能完成的运算除了常见的加减乘除以外,还包含取模等。例如:

cpp
int a = 1 + 2;
int b = 2 - 1;
int c = 2 * 3;     // 2x3
float d = 6.0 / 4; // 6÷4
int value = 0;
value +=
    a; // 等价于value = value +
       // a,在所有算术运算符中都能够使用这种写法,只需要修改等号前的运算符即可

取模操作是根据除法结果取余数。例如:

cpp
int e = 6 % 4; // 6整除以4的结果应该是1余2,这个2就是e的值

sizeof 运算符可以计算这个变量占用内存的大小(单位字节例如:

cpp
cout << sizeof(uint8_t) << endl; // 这里应该输出1

2. C++ 的类型转换 —— 隐式转换与强制转换

有些时候,我们需要处理不同的数据类型。例如在上面的例子中,我们需要计算 6 / 4,但 6 和 4 都是整数,而这两个数计算的结果也应该是整数,但是整数不能存储小数点后的位数,所以我们需要在变量类型之间进行转换。

隐式转换

隐式转换由编译器在编译时自动完成。由于不需要明确在程序中说明需要转换,因此这种转换方式被称为隐式转换。例如:

cpp
int value = 1;
double valueDouble =
    value; // 这里编译器会自动将value转换成double后再赋值给valueDouble

隐式转换的好处显而易见:隐式转换方便了我们编写程序的过程。如果每次在不同的类型间赋值都要报一次错的话,那这门语言的设计未免有点太反直觉了。然而隐式转换的坏处也非常容易看出来:在部分场合下,即便我们在直觉上希望触发隐式转换,隐式转换也不会触发。例如:

cpp
double result = 6 / 4; // 这里6÷4的结果是整数

在上面的例子中,由于被除数和除数的类型都是整数,就导致编译器认为运算以后的结果也是整数。在任意语言中,小数向整数的转换会丢掉整个小数部分,因此强制转换会导致精度损失。这种情况下我们就需要通过显式转换指导编译器做出正确的行为。

显式转换

在 C++ 中,显式转换有两种实现方式:

cpp
float result1 = (float)6 / 4; // 兼容C的方式
float result2 = float(6) / 4; // C++方式

其中第一种方式仍然存在的原因是 C++ 需要兼容 C 语言,在实际编写中,我们更推荐使用第二种方式进行显式转换。在 C++ 中,还有一种更安全的转换方式:

cpp
float result3 = static_cast<float>(6) / 4; // 更安全的转换方式

这样的转换方式能够防止一些错误的发生,同时对我们自己定义的类型拥有更好的支持(将在后续介绍用户自定义类型