关于补码.
世界上有 10 种人, 一种是懂二进制的, 一种是不懂二进制的. well, 我知道这很老掉牙, 但也许可以用来做一个不错的开头 ^_^.
1. 计算机表示整数的方式
计算机表示一个整数(包括正整数和负整数)的方法有 3 种, 原码, 反码和补码.
以十进制整数 5 为例. 转换为二进制形式是 101. 如果将它存储在一个 Byte 中, 那么
原码表示方式(最高位作为符号位, 0 表示正数, 1 表示负数):
+5: 0000 0101
-5: 1000 0101
反码表示方式(负整数就是正整数按位取反):
+5: 0000 0101
-5: 1111 1010
补码表示方式(负整数是正整数按位取反再加 1):
+5: 0000 0101
-5: 1111 1011
对于负整数的表示, 看上去原码表示方式最符合”人类的直觉”. 反码表示方式怪怪的. 补码表示方式某种程度上更加奇怪… 难道你从来没有这么觉得过? …
但是偏偏(绝大多数)计算机使用补码形式来表示整数. 为什么要使用补码这种奇怪的形式? 用原码不是很好么?
当人类觉得困惑的时候, 总是试图寻求一个答案或者心理安慰. 比如 “我们从哪里来? 我们要到哪里去?” 这种问题即便在人类自诩文明昌达的今天, 也通常是难以回答的, 迫不得已时只好求助于上帝; 但是 “计算机中为什么要使用补码形式表示整数?” 这种问题则显然和前面的问题不是同一个类型. 实话说我是非常笨的那种类型, 念书刚接触补码时困惑了许久…
2. 假如我们只表示正整数
不妨假设只有 4 个二进制位用来表示整数, 二进制从 0000 到 1111, 一共能表示 2 ^ 4 即 16 个整数. 把它们放到一个有 16 个刻度的表盘上:
图 1
如 图1 所示, 由于只有 4 个二进制位, 因此只有 16 种 位模式, 表盘上 16 个刻度每个刻度对应于一个 位模式. 很显然每个位模式可以用来对应表示一个整数. 而如果它们都用来表示正整数的话, 只能表示 0 到 15.
3. 表示正数 / 负数的第一种方案, 原码
现在的问题是, 整数有正有负. 那怎么表示负数呢?
嗯, 反正 4 个二进制位只能表示 16 个数, 如同 图1 中的表盘只有 16 个刻度. 为了公平起见, 正数负数各分一半的刻度. 不妨将这个表盘切成两半, 一半表示正数, 一半表示负数. 如果我们规定最高位表示符号的正负, 那么能表示数值的位就只剩下 3 个二进制位, 3 个二进制位表示数值只能表示 8 个, 从 +0 –> +7, 或者 -0 –> -7. 这正是原码表示整数的方法:
图 2
如 图 2 所示. 这种表示方式在”规定”上看来很自然: 最高位是符号位, 剩下的是数值位. 其实不然: 原来用于表示正整数 8 的位模式 1000 现在用来表示 -0. 原来用来表示正整数 15 的位模式 1111 现在用来表示 -7. 如果我说, 在只有 4 个二进制位的情况下, 使用原码来表示整数(有正有负)时, 8 ==> -0, 15 ==> -7, 11 ==> -3, …(而不是告诉你最高位作为符号位什么的, 忘掉符号位的规定), 你会不会(像补码表示一样)感觉到有点 “make no sense” 呢.
嗯. 好啦这就是原码表示法啦. 在原码表示法中, 0 有两种表示, 0000 和 1000, 即 +0 和 -0.
4. 表示正数 / 负数的第二种方案, 反码
接下来看看反码. 想法是一样的: 把表盘切开, 各分一半. 只不过这种各分一半的分法是直接将 位模式 取反 — 每个位模式都有一个相反的位模式不是么:
图 3
如 图3 所示. 原本用来表示正整数 15 的位模式 1111 现在用于表示 -0. 原本用来表示正整数 8 的位模式现在用于表示 -7. 同样, 在反码表示法中, 0 有两种表示, 0000 和 1111, 即 +0 和 -0.
反码的英文称呼是 one\’s complement. 为什么叫 one\’s complement 呢?
我们不妨将正整数 5 的二进制位模式取反过程看一下(同样我们只用 4 位表示). 0101 按位取反得 1010. 如果用另外一种方式来表示取反过程的话:
1111
– 0101
————
1010
嗯, 我们发现对一个数 n 取反的操作等价于用所有位都为 1 的数减去 n. 再看看反码表示法中, 1111 这个位模式用来表示多少呢, 表示 -0. 再看将 +5 的位模式 0101 取反后得到的位模式 1010 用来表示多少呢? -5. 也就是说如果我们使用 one\’s complement 这种表示法的话, 可以很自然的(通过位模式相减)得到 -0 – 5 == -5 这样的等式, 这是使用原码表示法所做不到的事情哦!(比如原码的 -0为 1000, 5 为 0101, 1000 – 0101 == 0011, 而 0011 在原码中表示 3). 然而由于在反码表示法中 0 有 +0 和 -0 两种表示, 我们可以得到 -0 – 5 == -5 这个等式, 却得不到 +0 – 5 == -5 的等式.
5. 表示正数 / 负数的最终方案, 补码
好吧. 其实原码和反码这两种尝试都是失败的. 接下来自然轮到补码登场了.
各分一半, 不用说了吧:
图 4
如 图4 所示, 这就是补码表示法啦. 正数 0 –> 7 共 8 个位模式, 负数 -1 –> -8 共 8 个位模式. 原本用于表示正整数 15 的位模式 1111 用于表示 -1. 原本用于表示正整数 8 的位模式 1000 现在用于表示 -8. 0 只有一种表示方式, 就是 0000. 即在这种表示法中, 15 ==> -1, 8 ==> -8, 12 ==> -4, 这个突然就 “make no sense” 了? 那对原码表示法为什么没有突然 “make no sense” 呢… 大约因为某个扯淡”定理”比较容易让人脑子短路的原因吧…
补码的英文称呼是 two\’s complement. 原因很简单, 同样以 +5 为例:
1 0000
– 0101
————-
1011
在补码表示法中, 位模式 0101 表示 +5, 位模式 1011 表示 -5. 位模式 10000(共 5 位) 呢? 它无法在我们的表盘上表示. 然而如果我们非要在表盘上表示它的话, 那么我希望你能理解, 它和 0000 这个位模式实际上是重合的(可以想象一个时针, 每向前走一个刻度表示 +1, 在这个表盘上, 当它到达 16 时又归零. 实际上就是 % 16. (mod 16, 模 16 运算)).
也就是说, 在补码表示中, 我们很自然地(通过位模式相减)得到了 0 – 5 == -5. 由于补码表示法中表示 0 的方法只有一种, 所以它没有反码的 +0 和 -0 的问题.
接下来看看 “一个负整数的补码表示 等于将 其所对应的正整数的补码表示 取反再加一” 这个扯淡 “定理”.
也即, +5 的补码表示为 0101, 则 -5 的补码表示为 ~0101 + 0001 == 1011. (~ 表示取反, C 语言中的按位取反运算符).
1111
– 0101
————
1010
+ 0001
————
1011
看上面的运算过程. 1111 就是补码表示中的 -1. 补码表示中的 -1 是通过位模式 0000 – 0001 得到的(忽略借的一位, 那位在表盘上是不可表示的).
设有正整数 n, 由于 1111 就是 -1, 自然 ~n == -1 – n. 那么自然就有 -n == ~n + 1 这个”定理”了.
6. 同模同余
最后还值得看一下的是这个:
图 5
如 图5 所示. 时针从 -4 走到 -7(如果我们坚持认为这个表盘一半的位模式表示负数一半表示正数并采用补码表示), 或者从 12 走到 9(如果我们认为所有的位模式都用来表示正数), 好吧更明确地说从位模式 1100 走到位模式 1001, 可以走两条路, 逆时针走 3 格, 或者顺时针走 13 格. 如果逆时针表示减法而顺时针表示加法的话, 那么我们有:
-4 – 3 == -7 (1)
或者
12 + 13 == 9 (2)
在(只有 4 位二进制位的情况下的)补码表示法中, 既然 -4 和 12 的位模式是一样的, -3 和 13 的位模式也是一样的, -7 和 9 的位模式也是一样的, 那这两种运算实际上没有差别. 实际上 -4 – 3 == -7 % 16 == -7, 12 + 13 == 25 % 16 == 9, -7 和 9 和 25 是一回事(在这个表盘中表示它们的话, 它们在同一个刻度上), 它们共模同余. 共模的模是 16.
因此使用补码表示整数的计算机在完成整数的加减运算时, 实际上可以根本就不管符号, 只管做加法就行. 这使得运算电路中只要加法器就可以了. 怎么解释内存中的一个位模式究竟是正数? 还是看着负数? 是软件的事情. 当然计算机在运算过程中同样会依据运算的结果进行是否进位/借位的置位. 软件可以通过标志位进行判断. 由于补码这种表示形式可以简化硬件电路的设计, 因此现在的计算机表示整数基本都使用补码表示形式.
最后… 当然, 如果用 1 个 Byte 表示一个整数, 那么其模是 2 ^ 8; 如果用 4 个 Byte 表示一个整数, 那么其模是 2 ^ 32. 这种情况下只需要画一个更加大的表盘, 有更精细的刻度就可以了, 其基本解释和只有 4 位表示一个整数的情况是一样的.