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很大,乘法就可能溢出。用这个函数就能提前发现。

提示:C11标准提供了<stdckdint.h>头文件,里面有ckd_addckd_subckd_mul等安全算术宏。如果你的编译器支持,可以直接用。

7.4 size_t的正确使用:不只是“无符号整数”那么简单

size_t是C语言里用来表示大小的类型。它本质上是unsigned的,但它的位数取决于平台。在32位系统上是32位,在64位系统上是64位。

使用size_t时,有几个要点:

  1. 不要用int代替size_t:int可能是32位,但size_t可能是64位。用int存size_t,会截断。
  2. 不要对size_t做减法得到负数:size_t是无符号的,减法结果如果为负,会变成一个巨大的正数。
  3. 循环变量用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 知识体系图:整数安全的核心逻辑

下面这张图,是我自己总结的整数安全知识体系。你可以把它当作一个检查清单,写代码时对照着看。

整数安全 整数溢出检测 先检查,后运算 使用 INT_MAX/INT_MIN 封装安全函数 有符号与无符号转换陷阱 隐式转换导致逻辑错误 统一类型,避免混用 开启 -Wsign-compare 安全算术运算 加法/减法/乘法检测 使用 ckd_add 等宏 内存分配前检查 size_t 的正确使用 不要用 int 代替 size_t 避免减法得到负数 循环变量用 size_t 核心原则:先检查,后运算;统一类型,避免隐式转换

7.6 避坑指南:我踩过的那些坑

最后,分享几个我亲身踩过的坑,希望能帮你少走弯路。

  • 我曾经intsizeof的结果,导致在64位系统上截断。从那以后,所有sizeof的结果我都用size_t存。
  • 我曾经在循环里用int istrlen()比较,结果字符串长度超过2GB时,循环直接不走了。嗯,那次排查花了我一整天。
  • 我曾经在计算缓冲区大小时,忘了检查乘法溢出。结果分配了一个比预期小得多的缓冲区,导致后续写入越界。从那以后,所有乘法我都用安全函数。

整数安全,说白了就是“小心驶得万年船”。多检查一次,多封装一个函数,就能避免很多线上事故。希望今天的分享对你有帮助。

公众号:蓝海资料掘金营,微信 deep3321