7、整数安全:整数溢出检测、有符号与无符号转换陷阱、安全算术运算、size_t的正确使用
整数安全这个话题,说实话,是C语言里最容易踩坑、也最容易被忽视的地方。我见过太多项目,功能跑得好好的,突然某天因为一个整数溢出就崩了。你想想看,一个int变量,你以为是正数,结果加着加着变成了负数,这谁受得了?
今天我们就来聊聊整数安全。我会结合我自己的踩坑经历,把整数溢出、有符号无符号转换、安全算术运算、还有size_t的正确用法,一次性讲清楚。
7.1 整数溢出:你以为的“大数”其实是个负数
整数溢出,说白了就是变量存不下你给的值了。比如一个8位的unsigned char,最大只能存255。你给它加1,它就变成0了。这就是溢出。
为什么会这样?因为计算机里的整数是用固定位数表示的。超出范围的部分,会被截断。嗯,这里要注意,C语言标准对有符号整数溢出的行为是未定义的。也就是说,编译器想怎么处理就怎么处理,你的程序可能直接崩溃,也可能悄无声息地算出个错误结果。
我在项目中遇到过这样一个问题:一个计数器,用来记录接收到的数据包数量。用的是int类型。结果某天流量暴增,计数器溢出了,变成了负数。然后程序里有个判断:if (count > 0),直接不成立了。整个系统就卡死了。从那以后,我所有计数器都用unsigned类型,并且加上溢出检测。
7.1.1 如何检测整数溢出?
检测溢出,其实有标准套路。我建议你记住下面这几个公式:
| 运算 | 溢出条件 | 检测方法 |
|---|---|---|
| 加法 a + b | a > INT_MAX - b | if (a > INT_MAX - b) 溢出 |
| 减法 a - b | a < INT_MIN + b | if (a < INT_MIN + b) 溢出 |
| 乘法 a * b | a > INT_MAX / b | if (b != 0 && a > INT_MAX / b) 溢出 |
注意,这些检测要在运算之前做。不要先算再判断,因为一旦溢出,结果已经错了。
#include <limits.h>
#include <stdbool.h>
bool safe_add(int a, int b, int *result) {
if (a > 0 && b > INT_MAX - a) {
return false; // 正溢出
}
if (a < 0 && b < INT_MIN - a) {
return false; // 负溢出
}
*result = a + b;
return true;
}
这段代码,我个人习惯用在所有可能涉及大数运算的地方。尤其是网络协议解析、文件大小计算这些场景,一定要加。
7.2 有符号与无符号转换陷阱:一个不小心就是大坑
有符号和无符号之间的隐式转换,是C语言里最隐蔽的陷阱之一。我刚开始做嵌入式时,就因为这个吃过亏。
举个例子:
int a = -1;
unsigned int b = 10;
if (a < b) {
printf("a 小于 b\n");
} else {
printf("a 大于 b\n");
}
你猜输出是什么?是“a 大于 b”。为什么?因为C语言在做比较时,会把有符号的a隐式转换成无符号。-1转换成无符号后,变成了0xFFFFFFFF,也就是4294967295。当然大于10了。
我曾经在一个项目中,用int存储了一个传感器的差值,然后跟一个size_t类型的缓冲区长度做比较。结果差值一旦为负,就变成了一个巨大的正数,直接绕过了长度检查,导致了缓冲区溢出。嗯,那次排查花了我整整两天。
7.2.1 如何避免这个陷阱?
- 统一类型:能用unsigned就用unsigned,能用size_t就用size_t。不要混用。
- 显式转换:如果必须混用,一定要显式转换,并且确保转换后的值在目标类型的范围内。
- 编译器警告:打开编译器的-Wsign-compare选项,让编译器帮你发现这些问题。
// 不好的做法
int len = -1;
size_t buf_size = 1024;
if (len < buf_size) { ... } // 陷阱!
// 好的做法
size_t len = 0; // 用size_t
if (len < buf_size) { ... } // 安全
7.3 安全算术运算:别让数学成为你的敌人
安全算术运算,说白了就是“先检查,后运算”。这个原则适用于所有可能溢出的场景。
我建议你封装一些安全算术函数,就像下面这样:
#include <stdint.h>
#include <stdbool.h>
bool safe_mul(size_t a, size_t b, size_t *result) {
if (a == 0 || b == 0) {
*result = 0;
return true;
}
if (a > SIZE_MAX / b) {
return false; // 乘法溢出
}
*result = a * b;
return true;
}
这个函数,我在分配内存时经常用。比如你要分配一个n * sizeof(T)的数组,如果n很大,乘法就可能溢出。用这个函数就能提前发现。
<stdckdint.h>头文件,里面有ckd_add、ckd_sub、ckd_mul等安全算术宏。如果你的编译器支持,可以直接用。
7.4 size_t的正确使用:不只是“无符号整数”那么简单
size_t是C语言里用来表示大小的类型。它本质上是unsigned的,但它的位数取决于平台。在32位系统上是32位,在64位系统上是64位。
使用size_t时,有几个要点:
- 不要用int代替size_t:int可能是32位,但size_t可能是64位。用int存size_t,会截断。
- 不要对size_t做减法得到负数:size_t是无符号的,减法结果如果为负,会变成一个巨大的正数。
- 循环变量用size_t:遍历数组时,用size_t作为索引,避免类型不匹配。
// 不好的做法
int i;
for (i = 0; i < strlen(str); i++) { ... } // strlen返回size_t,i是int,比较时会有符号转换
// 好的做法
size_t i;
for (i = 0; i < strlen(str); i++) { ... } // 类型一致,安全
我记得有一次,一个同事用int来存sizeof的结果。sizeof返回的是size_t,在64位系统上是8字节。而int只有4字节。结果就是,当结构体大小超过2GB时,sizeof的值被截断了,程序直接崩溃。嗯,从那以后,我们团队就规定:所有跟大小、长度、索引相关的变量,一律用size_t。
7.5 知识体系图:整数安全的核心逻辑
下面这张图,是我自己总结的整数安全知识体系。你可以把它当作一个检查清单,写代码时对照着看。
7.6 避坑指南:我踩过的那些坑
最后,分享几个我亲身踩过的坑,希望能帮你少走弯路。
- 我曾经用
int存sizeof的结果,导致在64位系统上截断。从那以后,所有sizeof的结果我都用size_t存。 - 我曾经在循环里用
int i跟strlen()比较,结果字符串长度超过2GB时,循环直接不走了。嗯,那次排查花了我一整天。 - 我曾经在计算缓冲区大小时,忘了检查乘法溢出。结果分配了一个比预期小得多的缓冲区,导致后续写入越界。从那以后,所有乘法我都用安全函数。
整数安全,说白了就是“小心驶得万年船”。多检查一次,多封装一个函数,就能避免很多线上事故。希望今天的分享对你有帮助。