乐于分享
好东西不私藏

源码、反码、补码为什么长这样

源码、反码、补码为什么长这样

最近在看《软件设计师教程》这本书,翻到原码、反码、补码这一节。书上把定义和公式都列得很清楚,但我总觉得哪里不对劲,我开启我的费曼学习法,开始自问自答:这三个东西为什么要分开讲?为什么不直接用一种最简单的方式表示负数,非要绕这么大一圈?

我们不妨先自己设计一套方案试试看。要表示负数,最直觉的做法是什么?拿一位当符号位,0 代表正,1 代表负,剩下的位照写数值大小。这就是原码。用 4 位二进制举例,+5 写作 0101,-5 写作 1101。这个思路和我们平时读数字的习惯完全一致,看一眼就懂。

那这套方案够用吗?我们拿一个最简单的算式验证一下:5 加上 -5,结果应该是 0。按二进制加法的规则老老实实算一遍,0101 加 1101,得到的是 10010,跟 0 完全对不上号。

问题出在哪里呢?我们之所以能一眼看懂 1101 是 -5,是因为我们的大脑自动识别了最高位是符号位。可加法器不知道这件事,它拿到的就是一串 0 和 1,按位对齐往上加,根本不管你哪一位代表什么意思。也就是说,原码是设计给人看的表示法,不是设计给电路算的表示法。这是第一个问题。

原码还有第二个问题。我们看 0 怎么表示:+0 是 0000,-0 是 1000。同一个数字,两种编码。这不只是浪费一个位置那么简单,电路要判断"这是不是 0",就得同时比较两种情况,逻辑上多了一道弯。

那我们能不能改进一下?既然原码的麻烦出在直接做加法上,一个自然的想法是:把负数换一种编码方式,让加法能顺利跑通。反码就是这么来的——负数用原码取反,符号位不动,其余位全部翻转。-5 在反码里就是 1010。

我们再验算一次 5 加 -5:0101 加 1010,结果是 1111。这个数在反码体系里恰好代表 -0。看起来问题解决了,但我们仔细想想就会发现,正负零的麻烦其实还在,只是换了一种编码形式,并没有真正消失。而且反码在处理进位溢出的时候,还得把最高位溢出的那一位加回最低位,这个操作叫循环进位,电路上又多了一道工序。所以反码更像是一个过渡方案,它把原码的毛病挪了个位置,没有从根上解决。

那问题到底出在哪儿?我们一直在纠结"怎么给负数编码",但也许该换个角度想:减法这件事,本身是不是可以用另一种方式理解?

我们平时想减法,想的是"拿走多少"。但减法还有另一种等价的理解方式——往前走一大圈,绕回来了。想象一个只有 12 个刻度的钟表,指针停在 3 点,我们想倒退 5 小时,也就是算 3 减 5。正常思路是往回拨 5 格,停在 -2,也就是 10 点。但我们也可以往前拨 7 格,同样会停在 10 点,因为钟表走满 12 格就会自动归零。也就是说,在这个循环的世界里,倒退 5 小时和前进 7 小时是同一件事。这个 7,正是 5 的补数——它是 5 想要绕一圈回到起点还差的那部分。补码这个名字,说的就是这个"补"。

这个思路能不能搬到二进制上?我们看 4 位二进制能表示的范围,0000 到 1111,一共 16 个刻度,走满 16 同样会溢出归零,跟 12 小时的钟表是一回事,只不过表盘换成了 16 格。那么 -5 在这个 16 格的钟上,需要补多少才能绕回 0 呢?16 减 5 等于 11,11 的二进制写出来是 1011,这和教科书上"反码加一"算出来的补码结果完全一致。这不是巧合,是同一件事的两种算法。

我们把 5 加 -5 再验算一遍:

  0101   (+5)
+ 1011   (-5 的补码)
------
 10000   (最高位溢出,直接舍弃)

舍掉溢出的那一位,剩下 0000,正好是 0。而且这一次,0 只有一种编码,没有正负零的问题。三种表示法放在一起比一比,差别就很清楚了:

表示法
5 + (-5) 算得对吗
0 的编码
加法器要不要额外处理
原码
不对
两种(+0/-0)
要先判断符号
反码
对,但要处理进位
两种(+0/-0)
要处理循环进位
补码
直接对
一种
不需要

原码需要电路先判断符号再决定加还是减,反码算完还要处理进位循环,补码则完全不用,加法器一套逻辑就能把加减法全部包圆。这就是补码真正解决的问题:它让减法可以直接转换成加法来算,a 减 b 等价于 a 加上负 b,CPU 不用再单独设计一套减法电路。

我后来查了一下,这套"走一圈自动归零"的数学思路其实很早就有了,数学上叫同余,高斯那会儿就系统研究过。计算机工程师是后来发现,这套现成的数学工具刚好能解决减法电路的硬件难题,就直接借了过来,变成了今天几乎所有 CPU 都在用的补码。

在这篇里,我们先看到原码在做加法时会算错,还带着正负零两种编码这个问题;接着看到反码虽然让 5 加 -5 算对了,但循环进位和正负零的问题依然没有根除;最后通过钟表倒拨指针这个例子,理解了补码的本质——把减法重新定义成一个循环系统里的加法,从而让加法器同时承担加减法两种运算,这才是补码在硬件设计上真正的价值。想通这一层,书上那些数值范围和公式,倒也不用死记了。

本文有AI帮忙润色!