2. C++ 中的数据
By FunnyAWM
1. 变量的数据类型与名称限定
在第一章中,我们详细解释了 C++ 中变量的创建方法,那么为什么这里还要再讲解一次变量呢?那是因为变量中有很多额外的东西,比如变量的数据类型、CV 限定符等。这里我们继续来详细讲解 C++ 中变量的数据类型。
在 C++ 中,基本的数类型可以分为以下几种:
- 整形:用于存储整数;
- 浮点型:用于存储小数;
- 字符型:用于存储各种字符;
- 布尔型:用于存储逻辑真或假;
- void 类型:可用于函数不返回值,在 C 中也用来作为动态内存分配的通用类型。
C++ 中的变量名称规则如下:
- 变量名只能使用字母或下划线作为开头;
- 变量名只能包含字母、数字与下划线;
- C++ 的变量名是大小写敏感的,所以拼写相同,但大小写顺序不同的变量会被认作不同的变量;
- 不能使用语言保留字作为变量名;
接下来我们来逐个讲解这些数据类型。
1. 整形
整形用于存储整数。这里的整形可以分为很多类型,每个类型都有不同的内存占用量和存储值范围,具体如下:
| 类型 | 占用 | 范围 |
|---|---|---|
| short | 2 字节 | -215~215-1 |
| int | 4 字节 | -231~231-1 |
| long | 4 字节(32 位)或 8 字节(64 位) | 32 位系统同上(int |
| long long | 8 字节 | -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++ 中,我们能用如下方法引入这个头文件:
#include <cstdint>这个头文件中提供了从 8 bits 到 64 bits 的完整整形类型支持,包括有符号类型和无符号类型。例如,下面声明了一个 8 bits 长的有符号整形变量:
int8_t value;这里的 8 代表位数,如果需要声明 16 bits 的变量,则数据类型需要写成 int16_t,32 bits 为 int32_t,以此类推。如果需要声明的是无符号类型,则在对应的有符号型前加一个字母 u 即可。例如,下面声明了一个 8 bits 长的无符号整形变量:
uint8_t value;整形的溢出情况
在上面,我们说明了各类型的整形能存储的最小与最大值,如果我们实际存储的值超过了上面的限制,会发生什么呢?假设我们写了下面的程序:
#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++ 中可以写作:
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(无限大
从上面的表述我们可以看出,单精度浮点数与双精度浮点数的内存占用如下:
- 单精度浮点:占用了 4 个字节;
- 双精度浮点:占用了 8 个字节。
上面的表述同时解释了浮点数为什么被称作 “浮点数
3. 字符型
顾名思义,字符型是为了存储字符来设计的。这不废话么
在 C++ 中,一般的 char 型只能存储 ASCII 字符集中所有的字符。ASCII 中共定义了 128 个字符,主要用于显示英语及其他语言。在 C++ 中,字符型存储的字符是字符对应的 ASCII 码,本质上是整数,因此 char 型也被用来存储范围不大,但需要小内存占用的整数。
到这里,我们也可以看出一个 char 型的弱点:不能存储 ASCII 字符以外的任何字符。为了解决这个问题,C++ 在后续推出了宽字符型(wchar_t
还记得我们说过 char 型本质上存储的是整数吗?与整形相同,char 也允许定义为无符号型,只需要将这样的字符定义为 unsigned char 型即可。同时,为了克服宽字符型的可移植性问题(参考前面整形的问题,因为字符型本质上是整形
4. 布尔型
布尔型又被称为 boolean 型或 bool 型,主要表示逻辑上的真与假,在 C++ 中用 bool 表示布尔型。布尔型只能存储 2 个值:0 与 1。其中 0 代表假,即条件不成立或结论不正确;1 代表真,即条件成立或结论正确。这样的值在后面要讲解的判断语句中十分重要且有用。
值得一提的是,C 语言在 C99 标准以前没有原生的 bool 类型,在 C99 中,添加了 stdbool.h,这个文件包含了对布尔值的支持。
2. C++ 中的常量
在 C++ 中,声明常量的方式是在变量名前加上 const。例如:
const int value = 114514;其中 const 描述了这个数据是不可变的,也就是常量。
常量在 C++ 程序的编写中十分有用。例如我们需要定义圆周率 π 来做圆相关的计算,只需要写:
const double PI = 3.141592654这样能够确保我们不会意外改变这个常量,由此保证了计算的正确性。
3. C++ 中的算术运算与类型转换
1. C++ 的算术运算
C++ 使用运算符来完成数值的计算。C++ 能完成的运算除了常见的加减乘除以外,还包含取模等。例如:
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,在所有算术运算符中都能够使用这种写法,只需要修改等号前的运算符即可取模操作是根据除法结果取余数。例如:
int e = 6 % 4; // 6整除以4的结果应该是1余2,这个2就是e的值sizeof 运算符可以计算这个变量占用内存的大小(单位字节
cout << sizeof(uint8_t) << endl; // 这里应该输出12. C++ 的类型转换 —— 隐式转换与强制转换
有些时候,我们需要处理不同的数据类型。例如在上面的例子中,我们需要计算 6 / 4,但 6 和 4 都是整数,而这两个数计算的结果也应该是整数,但是整数不能存储小数点后的位数,所以我们需要在变量类型之间进行转换。
隐式转换
隐式转换由编译器在编译时自动完成。由于不需要明确在程序中说明需要转换,因此这种转换方式被称为隐式转换。例如:
int value = 1;
double valueDouble =
value; // 这里编译器会自动将value转换成double后再赋值给valueDouble隐式转换的好处显而易见:隐式转换方便了我们编写程序的过程。如果每次在不同的类型间赋值都要报一次错的话,那这门语言的设计未免有点太反直觉了。然而隐式转换的坏处也非常容易看出来:在部分场合下,即便我们在直觉上希望触发隐式转换,隐式转换也不会触发。例如:
double result = 6 / 4; // 这里6÷4的结果是整数在上面的例子中,由于被除数和除数的类型都是整数,就导致编译器认为运算以后的结果也是整数。在任意语言中,小数向整数的转换会丢掉整个小数部分,因此强制转换会导致精度损失。这种情况下我们就需要通过显式转换指导编译器做出正确的行为。
显式转换
在 C++ 中,显式转换有两种实现方式:
float result1 = (float)6 / 4; // 兼容C的方式
float result2 = float(6) / 4; // C++方式其中第一种方式仍然存在的原因是 C++ 需要兼容 C 语言,在实际编写中,我们更推荐使用第二种方式进行显式转换。在 C++ 中,还有一种更安全的转换方式:
float result3 = static_cast<float>(6) / 4; // 更安全的转换方式这样的转换方式能够防止一些错误的发生,同时对我们自己定义的类型拥有更好的支持(将在后续介绍用户自定义类型