2.2 整数补码与浮点编码


2.2 整数补码与浮点编码

本节摘要:整数用补码存储,让加法器一套电路通吃加减法;有符号溢出是未定义行为,编译器会据此做出反直觉优化。浮点数按 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; }

图 1 同一字节的两副面孔与浮点位域

图 1 同一字节的两副面孔与浮点位域

有符号溢出:未定义行为不只是"回绕"

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 落进缝隙,被四舍五入回原值。浮点数轴越往大越稀疏,这是位域结构的直接后果。

工程守则:

  1. 永远不用 == 比较浮点,用差值小于容差判断:fabs(a-b) < 1e-9
  2. 钱用整数(按分存),别用浮点累计;
  3. 混合比较时注意提升:floatdouble 运算会先统一成 double,中间精度可能与你预期不同;
  4. 特殊值认识一下:无穷大(指数全 1 尾数全 0)参与比较有确定次序,NaN(尾数非 0)与任何值比较都为假——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 范围:变成无穷大 */

浮点转整数超范围是未定义行为,不是"取低位"——不同平台、同一平台不同优化等级都可能给出不同结果。永远先判断范围再转。

本节要点回顾

  • 补码让加减法共用一套电路:负数 = 模减绝对值;按位取反加一是手工求法。
  • 同一字节两副面孔:有符号无符号只是滤镜,unsigned 视角常用于观察原始位模式。
  • 有符号溢出是未定义行为:靠回绕检测溢出的代码会被优化器删除,用无符号运算才安全。
  • 浮点是位域结构:符号、指数、尾数;范围靠指数,精度靠尾数,数轴上疏密不均。
  • 浮点比较用容差,钱用整数,超范围转换先判界:三条工程铁律。

下一节讲数据的进出口:printf 与 scanf 如何按格式串的字节滤镜读参数,以及缓冲区在这中间扮演的角色。

常见疑问

问:为什么不用原码存负数? 原码要求加法器先判断符号再决定加减,还要处理正零负零两套零;补码把这一切都省掉了——一套加法电路通吃所有符号组合,零只有一种表示。当年省下的是晶体管,今天省下的是指令延迟。

问:浮点误差能彻底避免吗? 不能,只能管理。多数十进制小数在二进制里是无限循环(如同三分之一在十进制),有限位数必然截断。对策按场景选:货币用整数按分计算;科学计算用 double 并全程容差比较;需要精确十进制的场合用定点数或专门的十进制库。

问:负零存在吗? 存在。浮点的符号位独立于数值位,零也带符号。好在负零与正零比较相等,日常逻辑不受影响,只在打印与位级比较时露出差异。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U