原码、反码、补码:为什么计算机只用「加法」就能做减法
计算机里的加法器只懂 +,但我们又必须能做减法。原码、反码、补码就是为这个问题设计的三层「表示同一数字的不同方式」。这一篇从问题出发,把它们的来历、关系和实际计算流程串清楚。
一、计算机怎么表示一个数字
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)。
3.3 实际例子:14 + (−21)
我们想知道 14 - 21 等于多少。CPU 内部只做:
1 | +14 的补码: 00001110 |
逐位算:
1 | 00001110 |
结果是 11111001。把它当补码读:
- 是负数(首位 1);
- 取反:
1 1111001→1 0000110; - 加 1:
1 0000111; - 还原成原码:
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;
- 补码是模运算的产物——它让
a - b等价于a + (b 的补码); - 看到补码结果是负数,要看回原码:再做一次「取反 + 1」。
七、易错点清单
- 正数变负数忘了保持符号位:「取反」只对数值位取反,符号位必须不变。
- 反码 + 1 时忘了丢掉进位:8 位加法,第 9 位的进位必须丢掉,不能写到结果里。
- 把补码还原成原码时多取了一次反:「负数 → 补码」和「补码 → 原码」操作相同,再来一次就行,不要写反。
- 算
14 - 21写成了14 + 21:补码的本质是「减法变加法」,先把减数转补码再做加法。 - 结果溢出没检查:8 位补码范围 $[-128, 127]$,
100 + 100 = 200已经溢出,得到的结果是错的——但不会报错,因为 CPU 只把它当成「按 8 位截断后的值」。判断溢出需要更复杂的逻辑(或者用更大的位数)。 - 混淆「原码」和「补码」的「取反」:原码的「负数取反」是得到反码;补码的「负数取反加 1」是得到原码。名字一样、动作不一样。