聊聊二进制中的原码、反码和补码
原码、反码和补码是计算机使用二进制表示整数知识中的三个基础概念。这种概念类的知识往往有这么几个特点:
- 难于理解。本来很简单的知识,封装成概念,复杂性就提高了
- 易于遗忘。即便学会了,过不了几年就又都忘了
- 各类考试很爱考。不管是学校的期末考试,还是社会性的考试如软考,都爱考这种知识
- 缺乏实际价值。原码和反码可以说是计算机发展过程中走的弯路,补码是最终的解决方案
所以,如果你想学到真正有用的知识的话,读到这里就可以关闭了。如果你想学到关于原码、反码和补码的一些没用的知识,你可以继续读下去。我希望可以用一篇文章来助你彻底理解这三个概念,且不会随时间流逝而遗忘。
让一切回到起点
让我们回到起点。现在你正在从头发明计算机,由于基础电路天然适于运算二进制,所以我们已经决定使用二进制来表示数据。为简便起见,我们只讨论用一个字节(8位二进制位)表示整数的情况。
因为整数分正数和负数,我们就想到可以用最高位表示正负(0 表示正,1 表示负),其余位表示数值。如:
| 十进制 | 二进制 |
|---|---|
| 3 | 0000 0011 |
| -3 | 1000 0011 |
| 0 | 0000 0000 |
| -0 | 1000 0000 |
如果不考虑符号位,0000 0011(3)+1000 0011(-3)=1000 0110(-6),这显然不对。
计算的时候还是需要考虑符号位,如一个正数和一个负数相加时需要考虑:
- 谁的绝对值更大(绝对值就是把符号位去掉后的值)
- 绝对值大的一方的符号就是计算结果的符号
- 用绝对值大的一方减去绝对值小的一方就是计算结果的数值
按照这个思路,我们设计的算数逻辑单元(ALU)中除了加法器外还需要:
- 比较器,用来比较绝对值大小
- 减法器,用来计算两个数的差值
是的,最早期的计算机就是这样设计的,上面这种二进制编码方式也被叫做原码。站在现代的角度看,原码存在这么几个问题:
- 符号位不能直接参与运算
- 0 存在 +0 和 -0 两种表示
- 计算逻辑比较复杂
但站在当时的角度,这种解决方案是最容易想到也是最直观的。
进化的发生
原码解决了在计算机中表示整数的问题,但是不够优雅。就像是程序员写了一段很复杂的代码,虽然功能实现了,但心里总是很不得劲。有没有更简便的方式?原码存在的最核心的问题在于:符号位不能参与计算。
假如符号位可以直接相加,那么计算正数和负数相加时,就不再需要比较谁的绝对值大了,ALU 中也就不再需要有比较器了。
如何让符号位可以直接参与运算呢?还是以3+(-3)为例,我们都知道结果是 0.
3 的二进制是 0000 0011,如果把 3 的二进制按位取反,得到 1111 1100,用这个数来表示 -3。那么:
| 十进制 | 二进制 |
|---|---|
| 3 | 0000 0011 |
| -3 | 1111 1100 |
| 0 | 0000 0000 |
| -0 | 1111 1111 |
那么,0000 0011(3)+1111 1100(-3)=1111 1111(-0),这显然对了,毕竟 -0 也是 0 嘛。
再看看其他情况,如 3+(-2)=1。
| 十进制 | 二进制 |
|---|---|
| 3 | 0000 0011 |
| -2 | 1111 1101 |
| 1 | 0000 0001 |
那么,0000 0011(3)+1111 1101(-2)=1 0000 0000,产生了溢出,只取 8 位就是0000 0000也就是 0。
3+(-2)=1,但按这种方式得到 0,显然还是有问题。但是误差已经很小了,和真实结果只差了 1。当时的计算机科学家们还发现了一个规律:如果计算结果没有溢出,计算结果就是正确的;如果有溢出,计算结果+1就是正确的结果。
这种用二进制表示整数的方法被叫做反码。
终极形态
反码虽然并不完美,但它解决符号位不能直接参与运算的问题,同时也让 ALU 大大的简化了:
- 由于不再需要比较绝对值大小了,所以就不需要比较器了
- 正数和负数相加时不需要减法器了,而且减法运算也可以用加法器实现(如
3-2用加法器计算3+(-2)得到),这样也不需要有减法器了
相比这两个优点,反码的溢出时需要+1才能得到正确结果的这个小缺陷也就完全可以忽略不计了。
现在让我们用放大镜来看一下反码会有这个缺陷的根本原因:
| 十进制 | 二进制(反码) |
|---|---|
| 0 | 0000 0000 |
| -0 | 1111 1111 |
在现实世界中,0 就是 0,不存在 +0 和 -0 的区别。软件是对现实世界的建模,这个模型理应和现实世界一致。如果模型和现实世界不一致,那设计就一定是存在问题的,同时实现也不会优雅。
让我们暂时先放下原码和反码的表示方法,也忘记符号位。看一眼现实世界:0-1=-1。那么,0000 0000(0)-0000 0001(1)=1111 1111,那么是否可以用1111 1111来表示 -1?,然后再-1,得到1111 1110,用它表示 -2。以此类推:
| 十进制 | 原码 | 反码 | 重新发明 |
|---|---|---|---|
| 3 | 0000 0011 | 0000 0011 | 0000 0011 |
| 2 | 0000 0010 | 0000 0010 | 0000 0010 |
| 1 | 0000 0001 | 0000 0001 | 0000 0001 |
| 0 | 0000 0000 | 0000 0000 | 0000 0000 |
| -0 | 1000 0000 | 1111 1111 | - |
| -1 | 1000 0001 | 1111 1110 | 1111 1111 |
| -2 | 1000 0010 | 1111 1101 | 1111 1110 |
| -3 | 1000 0011 | 1111 1100 | 1111 1101 |
先看上表中的反码部分,0-1=-0,0-2=-1,和现实世界有个 1 的误差,这个误差是因为多出来的 -0 单独占了一个序列。
再看我们重新发明的这种表示方法,它没有 -0,所以 0-1=-1,0-2=-2,和现实世界完全一致,至此补码就被发明了。
补码建模和现实世界的逻辑完全一致,它还非常巧合的满足了在原码时代设计的符号位用 0 表示正数字 1 表示负数的设定。可以这样说,反码所能解决的问题补码也都能解决,而且使用补码计算时如果产生溢出结果也不需要再 +1。
对于上面的结论,大家可以自行验证一下。
学习之道与术
读到这里,你应该已经可以理解原码、反码和补码产生的历史背景和经过,也能理解为什么我说“原码和反码是计算机发展史上走的弯路”了吧。现在,再看一下下面这类规律是否会觉得简单很多?
- 原码 → 反码:符号位不变,数值位取反
- 反码 → 补码:反码 + 1
最后的话
如果你看到了这里,我不知道是否该恭喜你没用的知识又增加了。我认为学校只教补码即可,因为现代计算机基本上都是以补码的形式来表示整数的,学了补码就已经知道了在计算机中怎么表示整数,而原码和反码这种已经被淘汰(并且也设计不合理的东西)仅作课外了解即可,把它们当作考试内容是万万没有必要的。