BLOG

Day 14:整数溢出(Integer Overflow/Underflow)

前面我们讲解了未初始化变量的危害与防范。今天聚焦C语言中又一极易被忽视且后果严重的问题——整数溢出与下溢。

主题原理与细节讲解

整数溢出(Overflow):指整数类型变量的值超出其可表示的最大值时,结果会“回绕”到最小值(或反之)。

整数下溢(Underflow):即值低于类型可表示的最小值时的“回绕”现象。

C语言中的整数溢出细节

无符号整数(unsigned):溢出是明确定义的,结果会对类型最大值+1取模(模运算),行为可预测。有符号整数(signed):溢出是未定义行为(undefined behavior),标准未规定结果,编译器可自由处理(可能回绕、截断、崩溃,甚至优化掉代码)。

典型陷阱/缺陷说明及成因

大数相加/相乘导致溢出

int a = 2_000_000_000;

int b = 1_000_000_000;

int c = a + b; // 溢出

数组长度计算溢出,导致缓冲区过小(安全漏洞根源)

int len = ...;

char *buf = malloc(len * sizeof(struct item)); // len太大可能溢出

无符号减法导致“下溢”

unsigned int x = 0;

unsigned int y = x - 1; // y变为UINT_MAX

循环计数器溢出,死循环或逻辑错误

for (unsigned char i = 250; i < 255; ++i) { ... } // 永远不满足i < 255

有符号数溢出导致未定义行为,被编译器优化掉

int x = INT_MAX;

x += 1; // 未定义行为

成因剖析: C语言的整数类型有固定的位宽,超出范围后不会报错。对于unsigned,行为被设计为环绕(模运算);signed则因考虑不同平台实现方式,未定义溢出行为。开发者对这些细节认知不足,极易埋下隐患。

规避方法与最佳设计实践

在运算前检查边界,如加法、乘法是否会溢出。**使用更大类型或专门的库(如stdint.h/inttypes.h、GMP等)**处理可能超范围的整数。对于动态分配内存前的乘法,先判断是否会溢出,例如:if (len > SIZE_MAX / sizeof(struct item)) { /* 错误处理 */ }

避免有符号整数溢出,尽量使用无符号类型进行边界相关运算。利用编译器开关或工具检测溢出(如GCC/Clang的-fsanitize=undefined,MSVC的/GS等)。不要假设溢出后一定回绕为0或负值,尤其是signed类型。

典型错误与优化对比

错误代码:

#include

void sum() {

int a = 2000000000, b = 2000000000;

int c = a + b; // 溢出

printf("%d\n", c);

}

优化后代码:

#include

#include

void sum() {

int a = 2000000000, b = 2000000000;

if (a > INT_MAX - b) {

printf("Overflow detected\n");

return;

}

int c = a + b;

printf("%d\n", c);

}

机制差异分析: 第一段代码直接溢出,输出为负数或不可预测结果。第二段提前判断,避免未定义行为并输出警告。

底层原理补充

有符号溢出为何未定义?

C标准为兼容多种底层实现(如补码、原码等),未规定signed溢出结果。现代机器普遍采用补码,溢出表现为回绕,但编译器可能优化掉带溢出的分支,导致难以调试和复现的问题。

无符号溢出为何定义为模运算?

因其二进制实现天然符合模2ⁿ的运算规律,结果可预测,适合底层操作。

SVG图解(溢出回绕示意)

0

UINT_MAX

+1

无符号溢出回绕到0

总结与实践建议

整数溢出是C语言中高危又极难察觉的陷阱。unsigned溢出可预测但仍可能导致安全与逻辑漏洞;signed溢出则属于未定义行为,后果完全难以预料。

实际建议:

永远不要假设整数运算“足够大”关键路径前加溢出判断充分利用现代工具和编译器警告不依赖溢出特性做“技巧性”代码内存分配等涉及长度、索引的地方务必检测溢出

写C代码时,任何一次整数运算都值得你多考虑“溢出/下溢”可能性。安全、健壮的程序从每一处细节防护做起。

公众号 | FunIO 微信搜一搜 “funio”,发现更多精彩内容。 个人博客 | blog.boringhex.top