本节摘要:整数用补码存储,让加法器一套电路通吃加减法;有符号溢出是未定义行为,编译器会据此做出反直觉优化。浮点数按 IEEE 754 分成符号、指数、尾数三个位域,换来巨大的表示范围与不可避免的精度损失。本节把两套编码拆到二进制位,并给出工程上的避坑清单。
设想只用 4 位存数。补码的规则一句话:负数等于"模减去它的绝对值"。4 位的模是 16,于是 -3 存成 16 - 3 = 13,即二进制 1101;+3 存成 0011。两者相加:
1101 (-3) + 0011 (+3) ------ 10000 丢弃最高进位后得 0000,正是 0
加 -3 与减 3 结果一致,硬件不再需要减法器。这就是补码存在的全部理由:省电路。顺带的礼物还有:0 只有一种表示(反码有 +0 和 -0 两种)。
手工求补码口诀:按位取反再加一。-5 的 8 位补码:5 是 00000101,取反 11111010,加一 11111011,等于十进制 251——所以同一字节按 signed char 读是 -5,按 unsigned char 读是 251。字节没变,滤镜变了。
#include <stdio.h> int main(void) { signed char s = -5; unsigned char u = 251; printf("%d %d\n", s, u); /* -5 251 */ printf("%d\n", (unsigned char)s); /* 251:同一字节的另一面 */ return 0; }

INT_MAX + 1 结果是什么?很多人以为是回绕成 INT_MIN。在补码硬件上确实会回绕,但 C 标准把有符号溢出定义为未定义行为——编译器可以假设它永不发生并据此优化。一个经典案例:
int check_overflow(int a, int b) { if (a + b < a) /* 想靠回绕检测溢出 */ return 1; return 0; }
开 -O2 后,编译器推理"a + b 溢出不可能发生(未定义行为),于是 a + b < a 恒假",直接返回 0——检测代码被整个删除。无符号数没有这个问题:标准明确无符号运算按模 2 的幂回绕,检测溢出用无符号运算是安全的:
unsigned add_ok(unsigned a, unsigned b, unsigned *r) { *r = a + b; return *r >= a; /* 回绕时结果必然小于 a */ }
⚠️ 常见坑:用有符号算式做溢出检测、或依赖回绕行为——在优化器眼里这俩都不成立。
IEEE 754 把浮点数存成"科学计数法的二进制版"。float 的 32 位分成 1 位符号、8 位指数、23 位尾数;指数存的是"真实指数 + 127"(移码),尾数省略了开头的 1(规格化数首位恒为 1,不必存)。这套设计换来了夸张的表示范围——float 能表达 10 的 38 次方量级——代价是精度按位分配,不是均匀刻度。
直观验证精度问题:
#include <stdio.h> int main(void) { double a = 0.1 + 0.2; printf("%.17f\n", a); /* 0.30000000000000004 */ printf("%d\n", a == 0.3); /* 0:不相等 */ float f = 16777216.0f; /* 2 的 24 次方 */ printf("%.1f\n", f + 1.0f); /* 16777216.0:加 1 丢失! */ return 0; }
第二个实验最能说明问题:float 的尾数 23 位加上隐含位共 24 位,2 的 24 次方之后,相邻两个可表示数的间隔变成 2——+1 落进缝隙,被四舍五入回原值。浮点数轴越往大越稀疏,这是位域结构的直接后果。
工程守则:
== 比较浮点,用差值小于容差判断:fabs(a-b) < 1e-9;float 与 double 运算会先统一成 double,中间精度可能与你预期不同;x != x 为真是 NaN 的判别法。int i = 3000000000 > INT_MAX ? 0 : 0; /* 占位 */ double big = 3e12; int n = (int)big; /* 超出 int 范围:未定义行为,常见输出垃圾值 */ double d = 1e300; float f2 = d; /* 超 float 范围:变成无穷大 */
浮点转整数超范围是未定义行为,不是"取低位"——不同平台、同一平台不同优化等级都可能给出不同结果。永远先判断范围再转。
下一节讲数据的进出口:printf 与 scanf 如何按格式串的字节滤镜读参数,以及缓冲区在这中间扮演的角色。
问:为什么不用原码存负数? 原码要求加法器先判断符号再决定加减,还要处理正零负零两套零;补码把这一切都省掉了——一套加法电路通吃所有符号组合,零只有一种表示。当年省下的是晶体管,今天省下的是指令延迟。
问:浮点误差能彻底避免吗? 不能,只能管理。多数十进制小数在二进制里是无限循环(如同三分之一在十进制),有限位数必然截断。对策按场景选:货币用整数按分计算;科学计算用 double 并全程容差比较;需要精确十进制的场合用定点数或专门的十进制库。
问:负零存在吗? 存在。浮点的符号位独立于数值位,零也带符号。好在负零与正零比较相等,日常逻辑不受影响,只在打印与位级比较时露出差异。