补码原码反码转换
计算机里存负数用的是补码。同一个位模式按补码、反码、原码读会得到不同的十进制数,混用是查位运算 bug 的常见起点。
结果
11111111
负数补码的求法是「原码取反再加一」,反码则是「原码取反不加一」,两者相差 1。
补码为什么可以直接相加
把补码看成「最高位是负权重、其余位是正权重」的组合,就能直接按位权相加得到原数, 不需要区分正负。这也是补码能做无差别加法的根本原因。
| 位 | 权重 | 该位的值 | 贡献 |
|---|---|---|---|
| 第 7 位 | -128 | 1 | -128 |
| 第 6 位 | +64 | 1 | +64 |
| 第 5 位 | +32 | 1 | +32 |
| 第 4 位 | +16 | 1 | +16 |
| 第 3 位 | +8 | 1 | +8 |
| 第 2 位 | +4 | 1 | +4 |
| 第 1 位 | +2 | 1 | +2 |
| 第 0 位 | +1 | 1 | +1 |
把有贡献的项加起来:-128 +64 +32 +16 +8 +4 +2 +1 = -1, 和十进制输入一致。
反向:从二进制串读回十进制
三种编码的取值范围
| 位宽 | 补码 | 反码 | 原码 |
|---|---|---|---|
| 4 位 | -8 到 7 | -7 到 7 | -7 到 7 |
| 8 位 | -128 到 127 | -127 到 127 | -127 到 127 |
| 16 位 | -32768 到 32767 | -32767 到 32767 | -32767 到 32767 |
| 32 位 | -2147483648 到 2147483647 | -2147483647 到 2147483647 | -2147483647 到 2147483647 |
只有补码的负数范围比正数多一个。反码和原码都要为正负零各留一种表示, 所以少一个可用的负数,范围是对称的 -127 到 127。
补码与浮点的常见坑
补码让减法变成加法。补码最大的好处是不需要单独的减法电路:a - b 直接算成 a + (-b的补码),硬件上加法器就够了。而且零只有一种表示,不会出现正零和负零比较不相等的情况。反码和原码都有正负零,还要单独处理进位回卷,所以现代 CPU 一律用补码。
最小负数没有对应的正数。8 位补码范围是 -128 到 127,不是对称的 -127 到 127。原因是要给零留一种表示,于是负半边多出一个。这导致 -128 取相反数还是 -128(溢出回卷),取绝对值也是负数。写代码时要小心 abs(INT_MIN) 这类陷阱。
0.1 在二进制里存不精确。十进制小数转二进制要不断乘二取整,0.1 会得到无限循环的小数,单精度只能存最近似的值(约 0.100000001490116)。所以 0.1 + 0.2 不等于 0.3,做金额计算要用整数分或专门的十进制类型,不能直接用浮点。
浮点比较不能直接用等号。因为存在舍入误差,应该比较两个数之差的绝对值是否小于容差。另一个坑是大数吃小数:单精度只有约 7 位有效数字,1e8 加上 1 还是 1e8,做累加时如果量级相差太大会把小数项整个丢掉。
非规格化数换来了渐进下溢。当指数域全为零时,尾数不再有隐含的 1,数值可以一直小到 2⁻¹⁴⁹(单精度)。这让「非常小的数相减是否为零」这件事有了答案,而不是突然掉到零。代价是处理非规格化数在部分硬件上会明显变慢。