19.3 补充:原码、反码、补码与移码
在 19.2 节我们学习了计算机如何用二进制表示正整数。但这引出了一个巨大的问题:计算机底层只有 0 和 1,它连负号(-)都不认识,那么它到底是如何表示负数的呢?
为了解决如何在有限的硬件中同时搞定“正负”和“加减法”的问题,计算机科学家们先后发明了原码、反码和补码,后来在浮点数领域又引入了移码。今天,几乎所有现代计算机的整数底层都是用补码来存储的。
1. 铺垫:机器数与真值
Section titled “1. 铺垫:机器数与真值”- 真值:我们在现实中真正想表示的数字,拥有正负号。例如:
+3,-5。 - 机器数:这个数字在计算机内存中实际的二进制长相。由于计算机不认识正负号,科学家规定:把内存最高位(最左边那一位)作为符号位,
0代表正数,1代表负数。
为了方便演示,下面我们假设所有的数字都用 8位(1个字节) 的内存来存储。
2. 原码 (Sign-Magnitude):最符合人类直觉
Section titled “2. 原码 (Sign-Magnitude):最符合人类直觉”原码的定义:最高位是符号位(0正1负),剩下 7 位直接用它的二进制绝对值表示。
示例:
+3的原码:[0] 0000011-3的原码:[1] 0000011
优点:简单直白,人类一眼就能看懂。 致命缺点(为什么被淘汰?):
- 会有两个零:
[0] 0000000叫做+0,[1] 0000000叫做-0。在数学上 0 就是 0,分正负 0 纯属浪费硬件,还导致比较大小很麻烦。 - 没法直接做加减法:如果让计算机直接把
+3和-3的原码按位相加,会得到[1] 0000110(即-6),而不是预期的 0。如果要通过原码做减法,CPU 里面还得专门设计一套极其复杂的减法器电路。
3. 反码 (Ones’ Complement):走向加减法统一的过渡
Section titled “3. 反码 (Ones’ Complement):走向加减法统一的过渡”为了让计算机只需要加法器就能做减法(把 A - B 变成 A + (-B)),人们发明了反码。
反码的规则:
- 正数的反码:与原码完全一样,不改变。
- 负数的反码:符号位保持为
1,剩下的所有数据位全盘按位取反(0变1,1变0)。
示例:
+3的原码:00000011\rightarrow 反码:00000011-3的原码:10000011\rightarrow 反码:11111100(看,后面 7 位原本是 0000011,全反过来了)
尝试做加法 (+3 加上 -3):
用它们的反码相加:00000011 + 11111100 = 11111111。
结果 11111111 是个反码(最高位是1说明是负数)。我们把它转换回原码看看是多少(符号位不变,剩下取反):变为 10000000,也就是 -0!
反码的缺点:
虽然反码解决了部分加法问题,但它依然没有解决“世界上有两个零(+0 和 -0)”的奇葩问题,在跨越 0 的算术进位上依然有瑕疵。
4. 补码 (Two’s Complement):现代计算机的唯一真神
Section titled “4. 补码 (Two’s Complement):现代计算机的唯一真神”为了彻底干掉 -0 并且让一套加法器电路完美搞定所有正负数的加减法,补码诞生了。如今所有的 C++ int 型底层都是补码。
补码的规则:
- 正数的补码:与原码、反码完全一样。(正数永远是本体)
- 负数的补码:在它反码的基础上,再加 1。
口诀:负数的补码 = 符号位不变,其余位取反,最后整体 + 1。
示例:推导 -3 的补码:
- 写出原码:
10000011 - 变反码:
11111100 - 最末位 +1 变补码:
11111101
补码的两大神级优势:
优势一:天下大一统的加法:
让 +3 和 -3 用补码相加:
00000011 (这就是+3)
11111101(这就是-3)
100000000 (最高位的那个1溢出了,8位容器装不下,直接丢弃掉)
留下的正好是:00000000!完美的绝对的 0!
优势二:多出了一个极小值:
在反码和原码中,8 位内存有两套 0,能表示的范围是 -127 到 +127。
而在补码中,原本的 -0 (10000000) 废除了,被硬性规定用来表示 -128。
这就解释了为什么 8 位带符号整数(int8_t)的范围是神奇的:-128 到 127。总是负数比正数多容纳一个!C++ 里的 int(通常是 32位)最小值也是 -2147483648,而最大值是 2147483647。
5. 移码 (Offset Binary / Biased):浮点数的专属配件
Section titled “5. 移码 (Offset Binary / Biased):浮点数的专属配件”移码通常不用于我们常见的整数 int,它主要用于浮点数(如 float, double)底层的“指数部分”。
存在的痛点:
补码虽然加减算术无敌,但是如果你要把两个补码内存直接拿去对比大小,就会非常混乱。
比如:正数 +3 补码是 00000011,负数 -3 补码是 11111101。
如果让计算机按照无符号的本能直接从左到右去比大小,由于负数最高位是 1,它居然会觉得 -3 比 +3 还要大!计算机如果要判断大小,还得专门先剥离符号位,很麻烦。
移码的规则: 把数字在数轴上整体平移,加上一个固定的偏置值(Bias),强行把所有负数都拉上来变成“正数”。
假设在 8 位中,我们加上偏置值 128(即加上二进制 10000000):
- 原本是真值
-3,移码 =-3 + 128 = 125,二进制存为01111101 - 原本是真值
0,移码 =0 + 128 = 128,二进制存为10000000 - 原本是真值
+3,移码 =3 + 128 = 131,二进制存为10000011
移码的优势: 仔细观察上述二进制,在这个系统里,只要真值越大,存进去的二进制看起来也绝对越大!这就意味着,计算机完全可以直接调用毫无技术含量的“无符号比较器”来对移码进行大小判断,速度极快。这就是浮点数(IEEE 754标准)底层指数位采用移码的根本原因。
当你看到 C++ 里的 int x = -10; 时:
- 它在你的代码里是个十进制字面量。
- 编译器编译它时,先算它的长相。
- 它在你的内存里,实际静静躺着的是它的补码形式。