这一章的标题是「变量 和 基本类型」。内容很多,本篇只记录 section 2.1 的 Primitive Built-in Types (内置类型)相关笔记:内置整数类型长度和转换。
类型(types)很重要,能决定两件事:
- data 的含义
- 操作(函数)的含义
例如
i = i + j
C++ 之所以提供 class 机制,允许用户自定义类,就是希望用户能通过自定义类型, 来提供和内置类型(built-in types)一样的使用体验; 而更进一步,为什么要提供这样的使用体验,或者说,让这样的使用体验成为可能,就是为了提供更好的抽象。既然如此,务必先弄清楚 built-in types 支持哪些操作。
类型长度
对于内置的 Arithmetic Types(数值类型),包括整数、浮点数两类;编译器只能规定它们的最小字节数:
危险类型: long 类型
典型例子是 long
, 在 Linux-x64 下的长度是8字节, 和在 Windows-x64 的长度是4字节。这个特点,引发了很多跨平台编译的代码,运行结果不同的bug。
我们应该吸取教训;当看到基础库头文件中定义:
xxx_def.h:
typedef long MLong;
typedef int MInt32;
这两种定义都是危险的,只不过从实际经验来看, long 类型的坑比较明显。 而维护者声称所有SDK都应该用这个 xxx_def.h 的内容,方便在切换平台时,重新修改上述 MXxx 类型的定义。 这明显不安全,是应该规避的。正确做法是使用 #include <stdint.h>
里的 int8_t
, uint8_t
等固定长度类型。
危险类型: char 类型
char 类型可以是 signed, 也可以是 unsigned 类型,不同编译器结果不一样。
在C++11之前,unsigned char ([0, 255]) -> char -> unsigned char 的转换结果, C++ 标准并不保证和原始数据一样; GCC/Clang 编译器下结果和原始值一样,那仅仅是 implementation defined。
从 C++11 开始, 这个转换是得到了确保得到原始值。
https://en.cppreference.com/w/cpp/language/types
这样的语言级保证,确保了诸如 std::ifstream, std::ofstream 在读写二进制数据时, 使用到的 reinterpret_cast<char*> 是安全的:
#include <string>
#include <fstream>
#include <iostream>
int main()
{
int data = 42;
const std::string filename = "data.bin";
std::ofstream fout(filename, std::ios::binary);
fout.write(reinterpret_cast<char*>(&data), sizeof(data));
fout.close();
int data2;
std::ifstream fin(filename, std::ios::binary);
fin.read(reinterpret_cast<char*>(&data2), sizeof(data2));
fin.close();
std::cout << data2 << std::endl;
return 0;
}
变量赋值时超过范围?
1)当我们赋值超过范围的值到 unsigend 类型变量上时, 结果是取模:
- 被模的数量,是类型能表达的数量
- 例如
unsigned char a = -1; // 255: (-1 + 256) % 256 = 255
2)当赋值到一个signed 类型时,如果被赋予的值是超出范围的, 那么结果是 未定义的
例如
3)当计算表达式中同时出现了 signed 和 unsigned 类型, 则 signed 类型被转为 unsigned 类型。相当于说临时生成了一个 unsigned 类型的匿名变量,并且对这个变量的赋值,如果出现了值超出范围,则遵循前面提到的,取模运算, 原因是这个匿名变量是 unsigned 类型
尽管我们没有刻意的把一个负数赋值给一个unsigned 类型,但是做大小比较的表达式中同时出现了 size_t 和 int 类型,而 size_t 可以认为是 unsigned int 类型。 此时会转换 int 类型到 unsigned 类型,具体的转换,就像是我们把int类型赋值给到unsigned类型时,如果超出 unsigned 的表示范围, 则执行取模运算:
例如:
unsigned u = 10;
int i = -42;
std;:cout << u + i << std::endl; // 输出-32吗?不是!输出 -32+std::numeric_limits<unsigned>::max(), 也就是 -32 + 2^{32} = 4294967264
实例分析
这个例子来自实际工程代码, 在设备部署时表现为: “当车道线数量为0, 程序出现死循环”,其实并不是真的死循环, 但 for 循环执行了 2 64 − 1 2^{64}-1 264−1 次 (18446744073709551615).
#include <stddef.h>
#include <iostream>
// 模拟 CNN 网络给出的算法结果
int get_num_lanes() {
return 0;
}
int main()
{
int num_lane = get_num_lanes();
// 执行后处理, 本意是车道线数量为0时,不会执行循环, 实际则执行了循环
for (size_t i = 0; i < num_lane - 1; i++)
{
std::cout << i << std::endl;
}
return 0;
}
- 问题出在
i < num_lane - 1
这一表达式。 并且注意结合 i 和 num_lane 的类型做分析 - i 是 size_t 类型, 是 unsigned 而不是 signed,在64位上时 unsigned int64_t 这么大
- num_lane 是 int 类型, num_lane - 1 仍然是 int 类型
- 看起来是
0 < -1
的比较, 其实是0ULL < -1
的比较
- 因为表达式中出现了 unsigned 和 signed 类型, 按照前面提到的规则3, 会转换到 unsigned 类型做比较:
0 < (std::numeric_limits<size_t>::max() - 1)
, 也就是0
和 2 64 − 1 2^{64}-1 264−1 做比较
也就是说,当 int num_lane=0
, size_t i=0
时, 表达式 i < num_lane - 1
被这样依次展开:
(size_t)0 < (int)-1
(size_t)0 < (std::numeric_limits<size_t>::max() - 1)
0 < 18446744073709551615
也就是说,含有bug的代码是在做:
for (size_t i = 0; i < 18446744073709551615; i++)
{
std::cout << i << std::endl;
}
难怪表现为 “死循环” !
实例分析2
https://github.com/vallentin/LoadBMP/blob/master/loadbmp.h#L209-L240
unsigned y;
unsigned h = get_h();
for (y = (h - 1); y != -1; y--)
...
由于 y 是 unsigned 类型, 如果 for 循环的条件写成 y >= 0
则会导致死循环, y != -1
则是正确的写法。 原因是, y!=-1
是一个 arithmetic 表达式, 由于 y 是 unsigned 类型, -1 是 int 类型, -1 会被转换到 unsigned 类型, 而 -1 本身不在 unsigned 类型表示范围之内, 因此会执行取模运算,从而把 -1 转换到 std::numeric_limits<unsigned>::max()-1
, 也就是
2
32
−
1
2^{32}-1
232−1. 因此相当于如下的 for 循环:
unsigned y;
unsigned h = get_h();
for (y = (h - 1); y != 2147483647; y--)
...
而其中涉及的第二个 arithmetic 表达式,是 y--
, 也就是 y=y-1
的意思, 一旦y等于0,则 y=y-1
并不会得到 “-1”, 而是得到 (-1 + mod) % mod 也就是 (-1 +
2
32
2^{32}
232)%
2
32
2^{32}
232, 得到
2
32
−
1
2^{32}-1
232−1.
4)不要混合使用signed和unsigned类型
原因是: 在 arithmetic 表达式中,同时出现 signed 和 unsigned 类型时, signed 类型会自动转为 unsigned 类型, 这个转换如前面所述,如果值不在 unsigned 类型范围则会执行取模运算。
举例:
#include <iostream>
int main()
{
int a = -1;
unsigned b = 1;
std::cout << a * b << std::endl; // 得到-1吗?不!得到 4294967295
return 0;
}