计算机里的加法器只懂 +,但我们又必须能做减法。原码、反码、补码就是为这个问题设计的三层「表示同一数字的不同方式」。这一篇从问题出发,把它们的来历、关系和实际计算流程串清楚。

一、计算机怎么表示一个数字

1. 二进制位与「符号位」

假设我们用 8 位二进制表示一个整数。最高位(第 7 位)当作「符号位」:

  • 0 = 正数
  • 1 = 负数

剩下的 7 位存「数值部分」(按二进制位权)。

举例:

十进制 二进制(8 位) 含义
+14 0 0001110 符号位 0(正),数值 0001110 = 14
−21 1 0010101 符号位 1(负),数值 0010101 = 21

为什么符号位只能放最前面? 因为它不参与位权计算,把它放最左/最右都行,统一约定放最左(最高位)方便电路判断正负。

1.1 8 位能表示的范围

编码方式 最小值 最大值 个数
无符号 0 255 256
有符号(任一编码) −128 +127 256

无论用哪种编码,256 个编码对应 256 个数字,总数不变。

二、三种编码

设正数 $x$ 的二进制绝对值是 $\lvert x \rvert$(用 7 位表示),下面把它扩展成 8 位。

2.1 原码

举例:

十进制 原码
+14 0 0001110
−21 1 0010101

2.2 反码

正数的反码 = 原码;负数的反码 = 符号位不变、数值位逐位取反。

举例:

十进制 原码 反码
+14 0 0001110 0 0001110(正数不变)
−21 1 0010101 1 1101010(数值位 0010101 → 1101010)

2.3 补码

正数的补码 = 原码;负数的补码 = 反码 + 1(注意是 8 位运算,超过 8 位要丢掉进位)。

举例:

十进制 原码 反码 补码
+14 0 0001110 0 0001110 0 0001110
−21 1 0010101 1 1101010 1 1101011

正数永远不变——这点很重要,意味着「同一个二进制序列」,正数看就是正数,负数看(用补码规则读回来)才是负数。

2.4 三个特殊的「边界值」

十进制 补码 说明
0 0 0000000 唯一一个「+0 = −0」的编码(反码有 +0 / −0 两个,补码统一成一个)
−128 1 0000000 8 位最小值。没有原码或反码对应(因为原码 / 反码的 8 位放不下 −128)

补码因此被所有现代计算机采用:它在表示范围上没有漏洞、计算规则最简单。

三、为什么减法能变成加法

3.1 直接做减法有多难

CPU 里只有「加法器」硬件,做 14 + (−21) 如果按「减号变加法」理解,需要:

  • 检测到减号;
  • 把第二个操作数取负;
  • 再做加法。

「取负」这一步本身要做一次「按位取反 + 1」(就是补码定义),所以加法器要么能直接识别减号,要么就得拆成两次运算。

3.2 补码的妙处:把「减法」伪装成「加法」

补码有个关键性质:

在 $n$ 位补码里,$a - b$ 等价于 $a + (\text{−}b){\text{补}}$,且结果(如果没溢出)就是 $a + (\text{−}b){\text{补}}$ 的补码。

也就是说,CPU 只需要把减数取补码,然后做普通的二进制加法,得到的还是补码,再按补码规则读出来就是正确答案。

这样硬件只需要:

  1. 加法器(永远做加法);
  2. 取负电路(按位取反 + 1)。

3.3 实际例子:14 + (−21)

我们想知道 14 - 21 等于多少。CPU 内部只做:

1
2
3
4
  +14 的补码:   00001110
+(−21) 的补码: 11101011
————————————
相加结果: 11111001

逐位算:

1
2
3
4
  00001110
+ 11101011
─────────
11111001 (最高位的进位 1 直接丢弃,因为只取 8 位)

结果是 11111001。把它当补码读:

  1. 是负数(首位 1);
  2. 取反:1 1111001 → 1 0000110;
  3. 加 1:1 0000111;
  4. 还原成原码:1 0000111 = −7。

所以 14 - 21 = -7 ✅。

验算:14 + (−21) = 14 − 21 = −7,对上了。

四、补码 ↔ 原码互转

补码还原成原码(也就是还原成一个我们看得懂的「带符号位的二进制」)的规则:

负数:符号位不变,数值位按位取反再加 1(即再做一次「补码化」)。

也就是说,求补码的逆运算就是再做一次「补码化」。

例:11111001(补码)→ 它是负数 → 数值位 1111001 取反得 0000110,加 1 得 0000111 → 原码 1 0000111 = −7。

顺口溜:「负数求补码 = 符号位不变、数值位取反加 1;负数看回原码 = 同一套操作再做一次」。

五、补码的另一个角度:模运算

为什么「a − b 可以通过 a + (−b) 的补码算」?

这本质是模运算。在 8 位二进制里,只能表示 256 个值,相当于「在 $[-128, 127]$ 这个整数环上」做算术:

  • 补码里 11101011 不只是「−21 的表示」,更是「$235$ 这个数字在 $[-128, 127]$ 里的代表」;
  • 14 + 235 = 249,而 $249$ 在 $[-128, 127]$ 中就是 $-7$(因为 $249 - 256 = -7$);
  • 所以 14 + (补码表示的 −21) 就直接得到了 -7。

这就是「补码让减法退化成加法」的数学根据。

六、总结

编码 正数 负数 用途
原码 0 数值 1 数值 直观、人类阅读
反码 同原码 符号位不变 + 数值位取反 过渡到补码用
补码 同原码 符号位不变 + 数值位取反 + 1 计算机实际使用

最值得记住的几条:

  1. 正数三种编码相同;
  2. 补码 = 反码 + 1;
  3. 补码是模运算的产物——它让 a - b 等价于 a + (b 的补码);
  4. 看到补码结果是负数,要看回原码:再做一次「取反 + 1」。

七、易错点清单

  1. 正数变负数忘了保持符号位:「取反」只对数值位取反,符号位必须不变。
  2. 反码 + 1 时忘了丢掉进位:8 位加法,第 9 位的进位必须丢掉,不能写到结果里。
  3. 把补码还原成原码时多取了一次反:「负数 → 补码」和「补码 → 原码」操作相同,再来一次就行,不要写反。
  4. 算 14 - 21 写成了 14 + 21:补码的本质是「减法变加法」,先把减数转补码再做加法。
  5. 结果溢出没检查:8 位补码范围 $[-128, 127]$,100 + 100 = 200 已经溢出,得到的结果是错的——但不会报错,因为 CPU 只把它当成「按 8 位截断后的值」。判断溢出需要更复杂的逻辑(或者用更大的位数)。
  6. 混淆「原码」和「补码」的「取反」:原码的「负数取反」是得到反码;补码的「负数取反加 1」是得到原码。名字一样、动作不一样。