【C++】整数类型(Integer Types)避雷指南与正确使用姿势

背景

C++继承自C语言。作为一门以零开销抽象为主要特征的底层语言,不同于Python或JavaScript等高抽象层次的语言,C++拥有一套较为完整、但又包含有一定历史包袱的内建整数类型。

在实际开发中,如果对C++内建整数类型的机制不熟悉,或者不遵循一定的使用规范,则非常容易引入难以排查和调试的Bug。因此学习了解C++中内建整数类型的特性,以及一套行之有效的使用规范,是非常有必要的。

内建整数类型的坑 or 历史包袱

C++ 标准没有规定具体位数

虽然在实际实践中,我们知道在x64平台,对绝大多数编译器来说:

  • short => 16 bit
  • int => 32 bit
  • long => 32 bit(Windows)或64 bit(Linux)
  • long long => 64 bit

但坑爹的地方在于,C++ 标准没有规定 int、long 等类型的具体位数🤣。

C++ 标准只规定了最小宽度(比如int的最小宽度是16 bit)和相对大小(比如sizeof(short) <= sizeof(int) <= sizeof(long))。

参见en.cppreference.com/w/cpp/langu...

这意味着如果我们要追求代码的严谨性和在未来的可移植性,就不能在使用时假定这些内建类型的具体位数。

坑爹的 unsigned 类型

人类直觉认为"大小、长度、年龄等不可能为负数",所以很自然地在这种场景下倾向于用 unsigned。但 C++ 规定,无符号整数的溢出是合法的模运算(Modulo Arithmetic)

这意味着,无符号数永远不可能为负,当它为 0 时再减 1,不会变成 -1,而是会回绕成该类型的最大值(如 32 位下变成 2³² - 1,即 4294967295)。

一旦掉进这个坑里,会导致以下几种致命 Bug:

  1. 死循环
C++ 复制代码
// 灾难:如果用 unsigned 表示数组下标,执行倒序遍历
for (unsigned int i = vec.size() - 1; i >= 0; --i) { 
    // i 为 0 时,--i 变成 4294967295,依然 >= 0。
    // 死循环 !!!
}
  1. 差值计算灾难
C++ 复制代码
usigned int a = 2020;
usigned int b = 2026;
if (a - b < 0) {
  std::cout << "a < b" << std::endl;
} else {
  std::cout << "a >= b" << std::endl;
}

结果会输出a >= b,因为a-b的结果是一个极大的正数,导致逻辑判断完全相反。

坑爹的隐式类型提升

混合运算引发的类型提升

C++ 为了让不同类型的数字能在一起做数学运算,制定了一套极其复杂的整型提升规则(Integer Promotion Rules) 。最反直觉的一条是:当有符号数和无符号数混合运算时,有符号数会被隐式强制转换为无符号数

这会引发类似下面的Bug:

C++ 复制代码
int a = -1;
unsigned int b = 1;

if (a < b) {
    // 你以为会执行这里?错!
} else {
    // 实际会执行这里!
    // 因为 a 被偷偷转换成了 unsigned,-1 变成了 4294967295
    // 4294967295 < 1 显然是 false。
}

这种错误如果出现在缓冲区检查、长度验证、边界判断等敏感地带,就非常容易被攻击者设计绕过检查,从而引发更严重的安全问题。

算术运算引发的类型提升

例子:

C++ 复制代码
  uint8_t a = 254;
mov   byte ptr [a],0FEh  
  uint8_t b = 255;
mov   byte ptr [b],0FFh  
  uint8_t c = a + b;
movzx eax,byte ptr [a]  // 隐式提升a为uint32_t
movzx ecx,byte ptr [b]  // 隐式提升b为uint32_t
add   eax,ecx // 计算(uint32_t)a+(uint32_t)b
mov   byte ptr [c],al // 将eax当中的计算结果强行截断成8bit,然后写回c变量

分析汇编代码,可知计算a+b时,ab中的值会被隐式提升成32bit。

尽管如此,但写回计算结果时仍然会发生截断。c中的计算结果仍然是错误的。

有符号溢出 = 未定义行为(UB)

不同于刚才聊的无符号类型溢出会引发"回绕"现象,在C++中,有符号整型的溢出被视作一种UB行为🤣。

举个例子:

c 复制代码
int x = std::numeric_limit<int>::max();
x += 1;  // UB

理论上编译器可能会直接决定将这行UB代码优化掉,或者引发其他异常现象。

一个更极端的例子:

C++ 复制代码
int f(int x) {  
  if (x + 1 > x)  
    return 1;  
  else  
    return 0;  
}

在高优化编译模式(比如release)下,编译器可能 会认为:既然 signed 溢出是 UB,那么我直接忽略处理 UB 的情况,即假设 x+1 一定不溢出。因此我直接将f优化成永远return 1😂。

那么此时你调用f(std::numeric_limits<int>::max())就会得到错误的结果。

在实践中,如果编译器优化掉的恰好是重要的安全检查,那么就可能引发更严重的安全漏洞。

坑爹的静默截断

大类型 → 小类型 => 静默截断

比如:

C++ 复制代码
int64_t n = 5000000000;
int x = n;
int limit = 800000000;
if (x < limit) {
  std::cout << x << std::endl;  // 得到垃圾值705032704
}

在你的编译器没有经过特殊设置的情况下,以上代码会通过编译。并且尽管n远大于limit,if块中的代码仍然被执行了。

在实践中,如果x被用于表示文件大小、网络长度或用户输入长度,那么攻击者可以通过构造一个超大数字n并依靠静默截断来绕过检查if (x < limit)

标准库的世纪失误

早期 C++ 标准委员会为了让容器(如 std::vector)能容纳尽可能多的元素,利用了无符号数比有符号数正向范围大一倍的特点,将容器的 size() 返回值和 operator[] 的参数硬性规定为 size_t(一个无符号类型)。

坑爹的地方就在这儿,由于size_t是一个无符号类型,因此你一旦调用STL库容器的size(),就必须警惕掉进前述的任何与无符号整型有关的坑。

为了让你加深印象,这里再强调一遍。

  • 逆向迭代陷阱(Underflow)

    C++ 复制代码
    for (size_t i = v.size() - 1; i >= 0; --i) { // 永远不会停止!
        // 当 i 为 0 时,--i 会变成一个巨大的正数(溢出/绕回)
    }
  • 隐式类型转换与比较错误

    C++ 复制代码
    std::vector<int> v;
    int x = -1;
    if (x < v.size()) { 
        // 如果 v 为空(size 为 0),这个条件居然是 FALSE!
        // 因为 -1 被转换成了 18446744073709551615 (2^64-1)
    }

C++ 之父 Bjarne Stroustrup 和多位委员会成员后来公开承认:这是一个巨大的错误(A Historical Mistake)。但为了 ABI 兼容,永远无法修改了。

Google C++ Style Guide 规范是怎么说的?

为了避雷前述的C++内建整型类型的各种坑或历史包袱,Google 制定了一系列可实操的工程规范

下面我对这部分规范进行了梳理和拓展。平时开发中遵循这些规范,就能避免掉一大部分的坑~

推荐使用<stdint.h><cstdint>的固定宽度类型

既然shortlong longunsigned long long等类型的位宽是不确定的,那干脆我们就不要去用了。

取而代之,我们使用固定宽度类型,比如int16_tuint32_tint64_t

注意,这些类型直接使用即可,不必加std::前缀!

int类型的正确使用姿势

  • 在 C++ 内置整数类型中,唯一推荐经常使用的是 int。比如在数据范围适用的前提下,在以下场景:

    • 循环计数器
    • 一般的小整数
    • 下标
  • 如果一个值可能 ≥ 2³¹(约 21 亿),就应使用 64 位类型(int64_t)。

    • 特别的,如果某个值/变量本身不大,但在中间计算过程中可能溢出,也应当使用int64_t
  • 如果程序明确需要特定大小的整数类型,应使用int16_tint32_tint64_t精确宽度类型

    • 比如,TCP协议规范中端口字段明确为32bit,那么你就应该明确地用int32_t而不是int

强烈抵制无符号(unsigned)类型

原则

既然混用signed和unsigned容易翻车(比如刚才提到的隐式类型提升),Google 的做法非常简单粗暴------绝大多数业务代码里直接禁用 unsigned,全部用有符号整型。这直接消灭了混用的可能性。

特别强调,不要为了"保证非负"而用 unsigned。

错误示范:

C++ 复制代码
unsigned int age; // ❌ 只是想让 age >= 0

正确做法:

C++ 复制代码
int age;
// 如果你想保证它不能为负数,在代码里写断言。
assert(age >= 0);

豁免

只有当你明确在以下场景时,才能使用无符号类型:

  1. 需要进行位操作(如移位、按位逻辑操作)
    • 对于有符号整数(如 int),进行右移操作(>>)时,到底是"逻辑右移(补0)"还是"算术右移(补符号位)"在 C++20 以前不确定的(通常是算术右移,会补符号位)
    • 这会带来跨平台的不确定性。
    • 而无符号类型进行位运算(&, |, ^, <<, >>)有着绝对一致的跨平台表现。
  2. 表示位掩码(bitmask)或位域(bitfields)
  3. 需要利用无符号类型的溢出回绕特性(比如密码学或哈希算法)
  4. 用于表示单个二进制字节的值
    • 当我们在进行网络编程、文件 IO、序列化时,处理的基础单位是"字节"。一个字节就是 8 个比特,它没有正负之分。
    • 如果你用 char(有符号),当读取到大于 127 的字节时,它会被解释为负数,这在作为数组索引或进行宽类型转换时会引发严重的 Bug。

例子1:

LevelDB 使用了自定义的 MurmurHash 变种。你看这里清一色使用的是 uint32_t

C++ 复制代码
// 来源:google/leveldb
// 这里的 seed, m, r 以及 h 都在进行位操作和故意的溢出计算
uint32_t Hash(const char* data, size_t n, uint32_t seed) {
  // 常量 m 充当乘法因子,利用无符号乘法溢出截断的特性
  const uint32_t m = 0xc6a4a793;
  const uint32_t r = 24;
  const char* limit = data + n;
  
  // seed 和 (n * m) 进行异或,n*m 极可能溢出,但 uint32_t 保证了其安全性
  uint32_t h = seed ^ (n * m);

  // 一段典型的每次处理 4 字节的哈希混合过程
  while (data + 4 <= limit) {
    uint32_t w = DecodeFixed32(data); // 读取 4 个原始字节
    data += 4;
    h += w;           // 这里的加法依赖模 2^32 运算
    h *= m;           // 乘法依赖模 2^32 运算
    h ^= (h >> 16);   // 位移与异或,打乱比特位
  }
  // ...
  return h;
}

例子2:

在 Protobuf 的底层序列化格式中,一个字段的标签(Tag)由"字段编号(Field Number)"和"数据类型(Wire Type)"压缩进同一个整数中。

C++ 复制代码
// 来源:google/protobuf
// 使用无符号整数来进行左移、按位或、按位与等操作
inline uint32_t WireFormatLite::MakeTag(int field_number, WireType type) {
  // 将 field_number 左移 3 位,然后与低 3 位的 type 进行按位或 (|)
  // uint32_t 保证了移位操作绝对不会受符号位影响
  return (static_cast<uint32_t>(field_number) << 3) | static_cast<uint32_t>(type);
}

inline WireFormatLite::WireType WireFormatLite::GetTagWireType(uint32_t tag) {
  // 使用按位与 (&) 提取低 3 位的数据
  return static_cast<WireType>(tag & 7);
}

例子3:

Base64 编码解码时,针对的是原始字节流。

C++ 复制代码
// 来源:google/abseil (absl)
// 使用 uint8_t 数组来表示原始的字节流序列
static const uint8_t kBase64DecoderRules[256] = {
    // ... 大量解析规则状态码 ...
};

bool Base64UnescapeInternal(const char* src_param, size_t szsrc,
                            char* dest, size_t* szdest) {
  // 将输入的字符指针强制转换为无符号的字节流指针
  // 因为 Base64 处理过程中,我们只关心这 8 个 bit 是什么,不关心它代表什么字符或正负数
  const uint8_t* src = reinterpret_cast<const uint8_t*>(src_param);
  const uint8_t* src_end = src + szsrc;
  
  while (src < src_end) {
    // 作为数组索引时,如果是 signed char 遇到大于 127 的值会变成负数而越界崩溃
    // uint8_t 完美避免了这个问题
    uint8_t rule = kBase64DecoderRules[*src++];
    // ...
  }
}

size_t正确使用姿势

虽然前文中我们细数了size_t作为无符号整型的一系列罪状,但由于其较为明确的语义(比如用于表示内存数据块的字节数、偏移量),Google规范中也没有一棍子打死它,而是允许在适当的情况下使用。

原文:When appropriate, you are welcome to use standard type aliases like size_t and ptrdiff_t.

举例:TensorFlow 代码中 size_t 与 int64_t 并存

来源于 TensorFlow 官方 C++ API 文档示例:

C++ 复制代码
size_t TotalBytes() const   // returns memory usage
int64_t dim_size(int d) const   // returns shape dimension
  • TotalBytes() 用的是 size_t,很自然用于表示内存 尺寸/字节数(不可能为负)。
  • dim_size() 返回 int64_t,用于表示 tensor 的 逻辑维度大小/形状 ,因为:
    • TensorFlow 的维度整数可能参与算术计算
    • 需要 signed 类型有助于防止 signed/unsigned 隐式转换问题

容器大小要谨慎

针对表示STL容器大小的size_t存在的缺陷,Google建议:尽量使用迭代器(iterators)和容器(containers),而不是指针(pointers)和大小(sizes)

C++ 复制代码
// ✅ Good
for (auto it = v.begin(); it != v.end(); ++it);

// ✅ Good
for (auto& x : v);

// ❌ Bad(混用signed和unsigned)
for (int i = 0; i < v.size(); i++);

另外,需要尽量避免无意义的 unsigned 扩散到业务代码。

以下是一个 Good case:

C++ 复制代码
size_t size = container.size();             // 与 STL 兼容
int64_t count = static_cast<int64_t>(size); // 内部转换防止 signed/unsigned 混用
for (int64_t i = 0; i < count; ++i) { ... } // 内部循环

这种方式既兼容了容器接口的 size_t,又避免了 signed/unsigned 混用引发的 bug。

相关推荐
ShineWinsu6 小时前
对于C++:继承的解析—上
开发语言·数据结构·c++·算法·面试·笔试·继承
码农阿豪7 小时前
Nacos 日志与 Raft 数据清理指南:如何安全释放磁盘空间
java·安全·nacos
国科安芯7 小时前
芯片抗单粒子性能研究及其在商业卫星测传一体机中的应用
嵌入式硬件·安全·fpga开发·性能优化·硬件架构
黑果魏叔7 小时前
手滑点错更新也不怕!超详细 Mac 系统更新屏蔽指南(附安全恢复方案)
安全·macos
绿蕉7 小时前
飞机与高铁,谁更安全?——基于中国出行死亡数据的深度对比分析
安全·飞机·高铁
左手厨刀右手茼蒿7 小时前
Flutter for OpenHarmony: Flutter 三方库 hashlib 为鸿蒙应用提供军用级加密哈希算法支持(安全数据完整性卫士)
安全·flutter·华为·c#·哈希算法·linq·harmonyos
星河耀银海7 小时前
人工智能大模型的安全与隐私保护:技术防御与合规实践
人工智能·安全·ai·隐私
王码码20357 小时前
Flutter for OpenHarmony: Flutter 三方库 cryptography 在鸿蒙上实现金融级现代加解密(高性能安全库)
android·安全·flutter·华为·金融·harmonyos
消失的旧时光-19437 小时前
C++ 多线程与并发系统取向(二)—— 资源保护:std::mutex 与 RAII(类比 Java synchronized)
java·开发语言·c++·并发