724 字
4 分钟
[编程] C 语言陷阱:无符号数减法

在 C 语言编程中,类型选择不当往往会引发难以察觉的严重 Bug。本文将通过一个简单的数组求和函数,解析无符号数(unsigned)与隐式类型转换结合时产生的“访问违例”异常。

1. 现象描述#

观察以下数组求和函数:

int sum(int a[], unsigned len) {
int i, sum = 0;
for (i = 0; i <= len - 1; i++)
sum += a[i];
return sum;
}
异常表现

当参数 len = 0 时,执行该函数会发生 Access Violation (访问违例)。 然而,如果将 len 的类型说明为 int 型,函数却能正确执行并返回 0。


2. 深度原因分析#

问题的根源在于 无符号数的减法溢出(回绕)C 语言的隐式类型转换规则

2.1 无符号数的“回绕” (Wrap-around)#

在计算机中,无符号数没有负数概念。当 len = 0 且类型为 unsigned 时,表达式 len - 1 的计算过程如下:

  • 二进制表示:000...000 - 000...001
  • 结果(补码形式):111...111
  • 对于 unsigned 类型,这个全 1 的二进制串被解释为该类型的最大正整数(例如在 32 位系统下是 4,294,967,295)。

2.2 隐式类型转换 (Usual Arithmetic Conversions)#

循环条件为 i <= len - 1

  • i 的类型是 int(有符号)。
  • len - 1 的类型是 unsigned int(无符号)。
C 语言转换规则

当一个有符号整数(int)与一个无符号整数(unsigned int)进行算术运算或比较时,有符号整数会被隐式转换为无符号整数

2.3 逻辑执行过程#

  1. i 初始值为 0。
  2. 比较 i <= len - 1
  3. i 被转换为 unsigned(0)len - 1unsigned(4294967295)
  4. 比较结果:0 <= 4294967295 永远成立。
  5. 循环持续执行,访问 a[0], a[1], a[2]... 最终因访问了操作系统保护的非法内存地址而触发访问违例

3. 为什么 int 类型能正确执行?#

lenint 类型且等于 0 时:

  1. len - 1 得到 -1(有符号运算)。
  2. 比较条件为 i <= -1
  3. 初始值 0 <= -1 为假(False)。
  4. 循环体一次也不执行,直接返回 sum = 0,逻辑正确。

4. 最佳实践与防坑指南#

这种错误在底层开发中极具隐蔽性。为了编写更健壮的代码,建议:

修正建议

避免在循环条件中使用减法表达式,改用更直接的边界判断:

// 无论 len 是有符号还是无符号,这种写法都是 100% 安全的
for (i = 0; i < len; i++) {
sum += a[i];
}

len = 0 时,0 < 0 为假,循环会安全跳过。

总结#

这个案例警示我们:

  • 谨慎使用 unsigned 处理可能为 0 的计数逻辑。
  • 永远警惕有符号数与无符号数混合比较产生的隐式转换。
  • 优先选择 i < len 而非 i <= len - 1
[编程] C 语言陷阱:无符号数减法
https://www.eustia-astraea.top/posts/programming/c-unsigned-trap/
作者
mcsl
发布于
2024-01-20
许可协议
CC BY-NC-SA 4.0