C 有符号与无符号
更新时间:2026-08-26。本文是
languages/c/主题入门层第 6 篇。上一篇讲了类型占几字节,这一篇讲"符号"——同一个二进制串,当作有符号和无符号解释,值完全不同。C 里-1和4294967295在内存里可能是同一个字节序列,这就是混乱的根源。
本文要回答的问题
- 有符号溢出和无符号溢出,结果一样吗?
-1 < 1U为什么是假的?for (size_t i = n; i >= 0; i--)为什么是死循环?
一、二进制怎么表示负数
先建立最基础的直觉。一个 int(4 字节)有 32 位,最高位是符号位:
int 1 = 00000000 00000000 00000000 00000001
int -1 = 11111111 11111111 11111111 11111111 (补码表示)负数用补码表示:-1 就是"全 1"。于是 -1 和 unsigned int 的 4294967295(2^32-1)在内存里是同一个字节序列——同一串 0 和 1,当作 int 读是 -1,当作 unsigned 读是 4294967295。
| 二进制(32 位全 1) | 当作 int | 当作 unsigned int |
|---|---|---|
1111...1111 | -1 | 4294967295 |
1000...0000 | -2147483648 | 2147483648 |
二、两种溢出的本质区别
2.1 无符号溢出:回绕(定义好的)
unsigned int u = 0;
u = u - 1; // 回绕到 4294967295无符号运算超出范围是模运算(wrap around),C 标准明确规定这是定义好的行为。0 - 1 就是 2^32-1,不会报错,也不是 UB。
2.2 有符号溢出:未定义行为(UB)
int i = 2147483647; // INT_MAX
i = i + 1; // 未定义行为!有符号溢出是 UB(未定义行为)。编译器在 -O2 下会假设"有符号数永远不会溢出"来做优化,结果可能完全出乎意料——不是"变成负数"这么简单,可能是死循环、结果消失、分支错误。专家的解读见未定义行为与优化,入门只需记住:别让有符号数溢出,溢出是 bug,不是"碰巧得到某个值"。
三、有符号与无符号混用:最大的坑
int a = -1;
unsigned int b = 1;
if (a < b) {
printf("a < b\n");
} else {
printf("a > b\n"); // 实际打印这个!
}原因:a(有符号)和 b(无符号)比较时,编译器把 a 隐式转换成无符号——-1 变成 4294967295,于是 a > b。
这条规则叫隐式算术转换:有符号和无符号混用时,通常"小范围"向"大范围"转,两者同样大时无符号胜出。结果是 -1 这个值在比较里消失了。
预防三招:
- 比较时显式转换:
if ((unsigned)a < b)或if (a < (int)b),写清楚意图; - 尽量避免混用,特别是循环下标、数组长度这类天然无符号的场景;
- 编译时开
-Wall -Wextra,编译器会警告comparison between signed and unsigned。
四、size_t 与死循环:经典中的经典
size_t 是无符号的,这就引出最著名的 C 陷阱:
for (size_t i = 10; i >= 0; i--) { // 死循环!
printf("%zu\n", i);
}i >= 0 对无符号数永远成立(任何无符号数都大于等于 0)。i 减到 0 后再 i--,回绕成 18446744073709551615(64 位 size_t 的最大值),继续循环……直到内存耗尽或你 Ctrl+C。
正确的倒着遍历写法:
for (size_t i = 10; i-- > 0; ) { // 用后置递减:i 先取原值判断,再减
printf("%zu\n", i); // 打印 9 8 7 ... 0
}或者干脆用有符号下标:
for (int i = 10; i >= 0; i--) { // 有符号,没问题
printf("%d\n", i);
}五、何时该用无符号
既然坑这么多,无符号是不是就该弃用?不是。它有两个明确用途:
| 用途 | 原因 | 示例 |
|---|---|---|
| 表示"大小 / 长度" | 不可能是负的,范围翻倍 | size_t、strlen 返回值 |
| 位运算 / 位掩码 | 右移时无符号是逻辑移位(补 0),有符号是算术移位(补符号位) | 掩码、颜色值 |
第 2 条值得展开:int x = -8; x >> 1; 结果是 -4(算术移位,补符号位),而 unsigned y = 0x80000000; y >> 1; 结果是 0x40000000(逻辑移位,补 0)。做位运算用无符号,行为可预测。详见位运算符入门。
六、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 有符号溢出 | -O2 下行为诡异 | 别让 int 溢出,用 long long 或检查范围 |
-1 < 1U 为假 | 比较结果反直觉 | 显式转换,避免混用 |
size_t 倒着循环 | 死循环 | 用 i-- > 0 或改有符号 |
%d 打印 unsigned | 大数打错 | 用 %u |
strlen 结果减某数 | 负数变巨大无符号数 | 先转 int 再运算 |
七、与本站主线衔接
一句话总结
无符号溢出是回绕(定义好的)、有符号溢出是 UB(编译器会拿"不会溢出"做优化);有符号无符号混用时无符号"赢",-1 会变成 4294967295;无符号适合大小和位运算,循环下标最好用有符号——这四句记住了,能躲开 C 里最经典的一类 bug。