在本文开始前,先抛出一个问题:在计算机系统中,数字一律用补码的形式来表示和存储。为什么?
希望通过本文能够解释这个问题。
重要:为了简洁说明,约定本文以下内容中都使用一个字节,也就是 8 个 bit 来表示二进制数。
原码
原码(True form)是指“未经更改”的码,是指一个二进制数左边加上符号位后所得到的码。
计算机中所有的数字均用 0 和 1 编码表示,数字的正负号也不例外,如果一个机器数字长是 n 位的话,约定最左边 1 位用作符号位,其余 n - 1 位用于表示数值,这部分也称为数值域。
因此,当二进制数大于 0 时,符号位为 0;当二进制数小于 0 时,符号位为 1;当二进制数等于 0 时,符号位可以为 0 或 1 (+0 / -0)。
举个例子:
十进制 | 原码 |
---|---|
2 | 0000 0010 |
-2 | 1000 0010 |
可以看到,一个数字用二进制原码表示的话,其取值范围是 -111 1111 ~ +111 1111,换算成十进制就是 -127 ~ +127。
为了便于算术逻辑单元(Arithmetic Logic Unit, ALU)的设计,后面又发展出了反码、补码等经过转换过的码。
反码
对于二进制数而言,对其进行取反操作就是将 0 变为 1,1 变为 0。
正数的反码(Ones’ complement)等于其原码,负数的反码则通过保留其符号位,然后将原码的数值位取反得到。
举个例子:
十进制 | 原码 | 反码 |
---|---|---|
2 | 0000 0010 | 0000 0010 |
-2 | 1000 0010 | 1111 1101 |
为什么会出现反码?
在数学中有加减乘除四则运算,但这对于计算机而言却太麻烦了。我们希望最好能够只有一种情况,比如只有加法,这样才能够让计算机变得简单高效。
而这完全可以做到。举个例子,在数学中 5 - 3 = 2,它完全等价于 5 + (-3) = 2,这样就做到了用加法来表示减法,而乘法是加法的累积(例如 3 * 5 = 3 + 3 + 3 + 3 + 3 = 15),除法是减法的累积(例如 10 / 3 = 10 - 3 - 3 - 3 = 1 mod 3)。所以在计算机中只有加法就够了。
但是,如果直接用原码来进行加法,计算机需要先识别该二进制原码的符号位,来确定该数字是正数还是负数,然后再进行加法运算,这样显然效率不高。
为了提高效率,让计算机能够在运算时统一处理符号位和数值域,或者说让符号位也能参与运算,所以就产生了反码。
至此,问题好像得到了解决。接下来我们验证一下。
仍然以上面的 5 - 3 为例:5 的原码为 0000 0101,反码为 0000 0101;-3 的原码为 1000 0011,反码为 1111 1100。
1 | 5 - 3 |
结果竟然差了 1 ?!
不急,我们再来看两个特殊的运算:
1 | 1 - 1 |
1 | 0 + 0 |
可以看到,虽然 -0 和 0 是一样的,但是在反码中却有两种表示方式。也可以这么理解:当用一个字节表示数字的取值范围时,这些数字中多了个 -0。
因此,虽然用反码进行运算时符号位可以直接参加运算,但是结果却是不对的。
补码
补码(Two’s complement)的出现就是为了解决上面反码的问题。
正数的补码和其原码、反码一样,负数的补码是其反码 + 1。
举个例子:
十进制 | 原码 | 反码 | 补码 |
---|---|---|---|
2 | 0000 0010 | 0000 0010 | 0000 0010 |
-2 | 1000 0010 | 1111 1101 | 1111 1110 |
同样,接下来我们用补码验证一下。
仍然以 5 - 3 为例:5 的原码为 0000 0101,反码为 0000 0101,补码为 0000 0101;-3 的原码为 1000 0011,反码为 1111 1100,补码为 1111 1101。
1 | 5 - 3 |
结果没问题。再来看那两个特殊的运算:
1 | 1 - 1 |
1 | 0 + 0 |
结果仍然没问题,至此,问题终于得到了解决。
同时这也解释了一开始的问题,在计算机系统中,数字一律用补码的形式来表示和存储。
原码、反码、补码,你搞懂了吗?