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

作者:PAK向日葵日期:2026/2/24

6609c93d70cf3bc7a121815ed800baa1cd112a5a.jpg

背景

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. 死循环
1// 灾难:如果用 unsigned 表示数组下标,执行倒序遍历
2for (unsigned int i = vec.size() - 1; i >= 0; --i) { 
3    // i  0 时,--i 变成 4294967295,依然 >= 0。
4    // 死循环 !!!
5}
6
  1. 差值计算灾难
1usigned int a = 2020;
2usigned int b = 2026;
3if (a - b < 0) {
4  std::cout << "a < b" << std::endl;
5} else {
6  std::cout << "a >= b" << std::endl;
7}
8

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

坑爹的隐式类型提升

混合运算引发的类型提升

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

这会引发类似下面的Bug:

1int a = -1;
2unsigned int b = 1;
3
4if (a < b) {
5    // 你以为会执行这里?错!
6} else {
7    // 实际会执行这里!
8    // 因为 a 被偷偷转换成了 unsigned,-1 变成了 4294967295
9    // 4294967295 < 1 显然是 false。
10}
11

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

算术运算引发的类型提升

例子:

1  uint8_t a = 254;
2mov   byte ptr [a],0FEh  
3  uint8_t b = 255;
4mov   byte ptr [b],0FFh  
5  uint8_t c = a + b;
6movzx eax,byte ptr [a]  // 隐式提升a为uint32_t
7movzx ecx,byte ptr [b]  // 隐式提升b为uint32_t
8add   eax,ecx // 计算(uint32_t)a+(uint32_t)b
9mov   byte ptr [c],al // 将eax当中的计算结果强行截断成8bit,然后写回c变量
10

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

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

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

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

举个例子:

1int x = std::numeric_limit<int>::max();
2x += 1;  // UB
3

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

一个更极端的例子:

1int f(int x) {  
2  if (x + 1 > x)  
3    return 1;  
4  else  
5    return 0;  
6}
7

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

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

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

坑爹的静默截断

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

比如:

1int64_t n = 5000000000;
2int x = n;
3int limit = 800000000;
4if (x < limit) {
5  std::cout << x << std::endl;  // 得到垃圾值705032704
6}
7

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

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

标准库的世纪失误

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

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

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

  • 逆向迭代陷阱(Underflow)
1for (size_t i = v.size() - 1; i >= 0; --i) { // 永远不会停止!  
2    //  i  0 时,--i 会变成一个巨大的正数(溢出/绕回)  
3}  
  • 隐式类型转换与比较错误
1std::vector<int> v;  
2int x = -1;  
3if (x < v.size()) {  
4    // 如果 v 为空(size  0),这个条件居然是 FALSE!  
5    // 因为 -1 被转换成了 18446744073709551615 (2^64-1)  
6}  

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。

错误示范:

1unsigned int age; //  只是想让 age >= 0
2

正确做法:

1int age;
2// 如果你想保证它不能为负数,在代码里写断言。
3assert(age >= 0);
4

豁免

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

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

例子1:

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

1// 来源:google/leveldb
2// 这里的 seed, m, r 以及 h 都在进行位操作和故意的溢出计算
3uint32_t Hash(const char* data, size_t n, uint32_t seed) {
4  // 常量 m 充当乘法因子,利用无符号乘法溢出截断的特性
5  const uint32_t m = 0xc6a4a793;
6  const uint32_t r = 24;
7  const char* limit = data + n;
8  
9  // seed  (n * m) 进行异或,n*m 极可能溢出,但 uint32_t 保证了其安全性
10  uint32_t h = seed ^ (n * m);
11
12  // 一段典型的每次处理 4 字节的哈希混合过程
13  while (data + 4 <= limit) {
14    uint32_t w = DecodeFixed32(data); // 读取 4 个原始字节
15    data += 4;
16    h += w;           // 这里的加法依赖模 2^32 运算
17    h *= m;           // 乘法依赖模 2^32 运算
18    h ^= (h >> 16);   // 位移与异或,打乱比特位
19  }
20  // ...
21  return h;
22}
23

例子2:

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

1// 来源:google/protobuf
2// 使用无符号整数来进行左移、按位或、按位与等操作
3inline uint32_t WireFormatLite::MakeTag(int field_number, WireType type) {
4  //  field_number 左移 3 位,然后与低 3 位的 type 进行按位或 (|)
5  // uint32_t 保证了移位操作绝对不会受符号位影响
6  return (static_cast<uint32_t>(field_number) << 3) | static_cast<uint32_t>(type);
7}
8
9inline WireFormatLite::WireType WireFormatLite::GetTagWireType(uint32_t tag) {
10  // 使用按位与 (&) 提取低 3 位的数据
11  return static_cast<WireType>(tag & 7);
12}
13

例子3:

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

1// 来源:google/abseil (absl)
2// 使用 uint8_t 数组来表示原始的字节流序列
3static const uint8_t kBase64DecoderRules[256] = {
4    // ... 大量解析规则状态码 ...
5};
6
7bool Base64UnescapeInternal(const char* src_param, size_t szsrc,
8                            char* dest, size_t* szdest) {
9  // 将输入的字符指针强制转换为无符号的字节流指针
10  // 因为 Base64 处理过程中,我们只关心这 8  bit 是什么,不关心它代表什么字符或正负数
11  const uint8_t* src = reinterpret_cast<const uint8_t*>(src_param);
12  const uint8_t* src_end = src + szsrc;
13  
14  while (src < src_end) {
15    // 作为数组索引时,如果是 signed char 遇到大于 127 的值会变成负数而越界崩溃
16    // uint8_t 完美避免了这个问题
17    uint8_t rule = kBase64DecoderRules[*src++];
18    // ...
19  }
20}
21

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 文档示例:

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

容器大小要谨慎

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

1//  Good
2for (auto it = v.begin(); it != v.end(); ++it);
3
4//  Good
5for (auto& x : v);
6
7//  Bad(混用signed和unsigned)
8for (int i = 0; i < v.size(); i++);
9

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

以下是一个 Good case:

1size_t size = container.size();             //  STL 兼容
2int64_t count = static_cast<int64_t>(size); // 内部转换防止 signed/unsigned 混用
3for (int64_t i = 0; i < count; ++i) { ... } // 内部循环
4

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


【C++】整数类型(Integer Types)避雷指南与正确使用姿势》 是转载文章,点击查看原文


相关推荐


百度 APP 正式接入 OpenClaw,所有人限时免费!
苍何2026/2/15

这是苍何的第 495 篇原创! 大家好,我是苍何。 最近被 OpenClaw 刷屏了吧? 3 周时间 GitHub Star 干到 19 万,比当年 DeepSeek 还猛。 我也发了好几篇文章了,然后还开源了个知识库,你别说,还挺多人用的。 基本上接入 QQ、微信、飞书、discord 等都写的比较全了。 但是说实话,OpenClaw 的部署使用过程并不算丝滑。 买服务器、配环境、装依赖,光是部署就需要折腾大半天。 好不容易跑起来了,还得通过 Telegram 来发指令。 就,怎么说呢,能用


主流模型对比-02
一诺滚雪球2026/2/6

前言 GPT-4、Claude、Llama、Qwen、DeepSeek... 面对层出不穷的大语言模型,你是否也曾感到迷茫? 选贵的 GPT-4,还是用免费的开源模型? 中文场景应该用什么模型? 本地部署和云端 API 各有什么优劣? 性价比最高的选择是什么? 选对模型,不仅能节省成本,还能获得更好的效果。今天我们来聊聊如何做出明智的选择。 1. 什么是模型选型 1.1 闭源模型 vs 开源模型 特点闭源模型开


langchain学习笔记(二):工具的调用
Shawn_Shawn2026/1/29

Tool Calling 定义简单tool 创建工具最简单的方法是使用 @tool 装饰器。 @tool(description="Returns the current time in yyyy-MM-dd HH:mm:ss format.") def get_current_time(*args, **kwargs) -> str: """ 获取当前系统时间。 格式为:yyyy-MM-dd HH:mm:ss """ now = datetime.dat


2025.12.17华为软开
ゞ 正在缓冲99%…2026/1/19

细胞增殖 import java.util.HashMap; import java.util.Map; import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); long n = scanner.nextLong();//n个观测值


hive问题
心止水j2026/1/11

一、基础概念 问题:简述 Hive 的定义及核心作用?答案:Hive 是基于 Hadoop 的数据仓库工具,支持类 SQL(HQL)查询分析;核心作用是让非开发人员通过 SQL 分析 Hadoop 上的海量数据。 问题:Hive 的元数据默认存储在哪里?生产环境中为什么要替换为 MySQL?答案:默认存 Derby;生产换 MySQL 因 Derby 仅单用户、不支持并发,MySQL 支持多用户并发、数据持久化且易维护。 问题:Hive 支持哪些执行引擎?它们的区别是什么?答案


GDAL 实现自定义数据坐标系
GIS之路2026/1/3

^ 关注我,带你一起学GIS ^ 前言 ❝ 在GIS开发中,经常需要进行数据的转换处理,特别是Shapefile数据的投影转换更是重中之重,如何高效、准确的将源数据坐标系转换到目标坐标系是我们需要研究解决的问题。 在之前的文章中讲了如何使用GDAL或者ogr2ogr工具将txt以及csv文本数据转换为Shp格式,本篇教程在之前一系列文章的基础上讲解如何使用GDAL实现自定义数据坐标系。 GDAL 简介 GDAL 下载安装 GDAL 开发起步 GDAL 实现 GIS 数据读取转换(全) 如


刷新后点赞全变 0?别急着怪 Redis,这八成是 Long 被 JavaScript 偷偷“改号”了(一次线上复盘)
WanderInk2025/12/25

做社区功能的人,多半都经历过这种抓狂时刻:你在帖子上点了个赞,按钮立刻高亮,数字也加一,用户体验看起来很丝滑;可你一刷新页面,点赞数像被人清空了一样,全部回到 0。你打开 Redis 客户端看,计数 key 明明存在,值也不是 0。于是你开始怀疑缓存一致性,怀疑是不是读了另一台 Redis,怀疑线上 jar 没更新,甚至怀疑自己是不是在梦里写代码。 我得说,这类问题最阴的地方就在于它特别像缓存问题,实际上却往往跟缓存一点关系都没有。真正的凶手是数据类型边界,准确地说,是 Java 的 long


【金猿人物展】涛思数据创始人、CEO陶建辉:实现AI时代时序数据库向“数据平台”的转型
数据猿2025/12/16

陶建辉 “【提示】2025第八届年度金猿颁奖典礼将在上海举行,此次榜单/奖项的评选依然会进行初审、公审、终审三轮严格评定,并会在国内外渠道大规模发布传播欢迎申报。 大数据产业创新服务媒体 ——聚焦数据 · 改变商业 在数字化转型与AI技术爆发的浪潮中,时序数据库作为处理海量实时数据的核心工具,已成为工业互联网、自动驾驶、能源电力等领域的刚需。 作为国内时序数据库赛道的领军企业,涛思数据从2016年入局至今,凭借精准的赛道选择、持续的技术迭代与独特的发展策略,实现了从单一产品到生


深度学习在教育数据挖掘(EDM)中的方法体系:从任务建模到算法范式的理论梳理与总结
智算菩萨2025/12/8

目录 1 引言 2 理论知识与技术基础 2.1 教育数据的形式化:事件流、序列、图与稀疏矩阵 2.2 监督学习的目标函数:从分类到排序 2.3 表示学习与自编码器:从重构到迁移 2.4 图神经网络与知识图谱:结构归纳偏置 2.5 生成模型与能量函数:RBM/DBN 的另一条线 2.6 强化学习:把推荐与学习路径当作序列决策 3 EDM 的典型任务与场景:问题定义、输入输出与评价方式 4 深度学习范式在 EDM 中的总体框架:监督、无监督与强化学习如何落到教育任务 4.1 监


从客户端自适应码率流媒体迁移到服务端自适应码率流媒体
AKAMAI2025/11/28

流媒体平台需要扩展以容纳数百万在不同设备和网络条件下同时观看的观众,这使得高效的自适应码率(ABR)流媒体技术至关重要。在这篇博文中,我们将探讨从客户端 ABR 过渡到服务端 ABR 的技术细节,重点关注实现细节和性能优化。 如您所在的企业也在考虑采购云服务或进行云迁移, 点击链接了解Akamai Linode解决方案,现在申请试用可得高达500美元专属额度 自适应码率流媒体基础 自适应码率流媒体究竟是如何工作的?让我们一步步来看。首先,视频内容需要经过准备,即将其编码为多种码率(例如 5

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客