本文尝试用更加浅显和本质的方式去理解补码。
1. 原码与补码
人类习惯于用十进制数进行运算,而计算机的每个位却只有0和1两种状态,换句话说,计算机采用的进制是二进制。
因此,我们面临的第一个问题就是计算机如何用二进制来表示十进制数字。
对于正数而言,可以直接用该数的二进制形式来表示,例如,十进制数2,其在计算机中的表示为(假设计算机的字长为8):
0000 0010
但是这样做会带来一个问题,如何表示负数呢?为此,我们采用牺牲最高位的办法,将其视作符号位,并用1表示负,0表示正(这样做是可以接受的,因为虽然正数的表示范围由之前的 [1, 255] 缩小到了现在的 [1, 127], 但是我们却因此收获了 [-127, -1] 的负数表示范围;另一方面,引入符号位后,[1, 127] 内的正数的表示方式并没有发生改变,换句话说,引入符号位前后,正数的表示方法是相洽的)。
例如,十进制数-2,其二进制表示现在变为:
1000 0010
这里,我们便可以引入原码的概念
原码:原码是指一个二进制数左边加上符号位后所得到的码,且当二进制数大于0时,符号位为0;二进制数小于0时,符号位为1。[1]
原码很直观,人类可以直接理解原码。
但是原码是不是完美的呢?很显然,它是有缺陷的。
1)十进制数0的原码并不唯一,以下两个都是0的原码。
0000 0000
1000 0000
2)当进行有负数参与的运算时,原码将暴露更大的缺陷,例如:-3+2:
1000 0011 #-3的原码
+ 0000 0010 #2的原码
------------
1000 0101 #-5的原码
我们得到了-5,这显然是个错误结果。
为此,我们需要介绍补码这个新概念
补码:正数和0的补码就是该数字本身。负数的补码则是将其对应正数按位取反再加1。[2]
例如,2的补码即为:
0000 0010
-2的补码为:
# 1. 将-2对应的正数2,按位取反得到:1111 1101
# 2. 1111 1101再加1即得到: 1111 1110,为-2的补码
1111 1110
0的补码只有一个,为:
0000 0000
现在我们再来验证一下补码在进行加减运算时是否能得到正确结果,同样是-3+2:
1111 1101 #-3的补码
+ 0000 0010 #2的补码
------------
1111 1111 #-1的补码
可以看到,我们得到了正确答案。
事实上,补码是计算机真正用来表示二进制数的方式,验证如下:
#include<iostream>
using std::cout;
int main()
{
unsigned int a = -1;
cout << a; //输出结果为255,即1111 1111, 而1111 1111正是-1的补码
return 0;
}
2. 为什么补码能起作用?
我们知道数轴上的点与实数是一一对应的,先来看看我们最熟悉的十进制数在数轴上的排列:
因此,对于数轴上的任意一个数,其加上一个正数,则表示其在数轴正方向上移动了一段距离;而减去一个正数(等价于加上一个负数),则表示其在数轴负方向移动了一段距离。这是确保我们在进行数的加减时得到正确结果的关键。
当我们引入了原码,那么数在数轴上又是如何排列的呢,可以验证,它们的排列变成了下面这个样子:
注意:0在这里是一个关键的分界线,对于负数而言,它们的“0” 是1000 0000;而对于正数而言,它们的“0”是0000 0000。
仔细观察,我们会发现,这个数轴有两个正方向(这里我们把加上一个正数,数字移动的方向定为正方向):当一个负数加上一个正数,数字往左边移动,例如:-3+2,结果变成了-5;当一个正数加上一个正数,数字往右边移动,例如3+2,我们得到5. 这就解释了为什么原码在加减运算中会出错的原因。
回到补码,如前所述,一个负数的补码就是是将其对应正数按位取反再加1。但是这里对于补码的叙述并不直观,甚至会让人有点摸不着头脑,为了更好地理解补码究竟在做什么,我们不妨将-2的补码与其对应正数2的补码相加,看看会得到什么结果:
0000 0010 # 2的补码
+ 1111 1110 #-2的补码
------------
1 0000 0000
这样,我们就发现了对一个负数求补码的本质:
负数 A 补 A_补 A补 = 模-绝对值(A)
模的值,等于 2 n 2^n 2n,n为字长。例如,对-2求补码:
-2补= 2^8 - 0000 0010#2的补码
= 10000 0000 - 0000 0010
= (1111 1111 - 0000 0010) + 1
#即等于2先按位取反,再加1
因此,引入补码后,数字在数轴上的排列变成了下面这个样子:
这里,不仅0只有一个表示,而且数轴也只有唯一一个正方向,从而保证了运算的正确性。而这个保证,仔细思考一下就会发现,正是利用了负数A的绝对值与其补码之和等于模,从而将负数部分的数轴方向调转。
注:考虑到计算机存在“溢出”的现象,因此更为正确的表示应该采用数环,而不是数轴,不过,理解用的话,数轴足够了。
总结
-
为什么要引入补码?
答: 1. 0的原码有两个,处理起来不方便;2. 原码不能直接相加减; -
引入补码的好处?
答:1. 0的补码只有一个;2. 变减法为加法,即减法和加法共用一套底层电路;3. 补码之间可以直接相加减,最高位可以接受进位。
参考
[1] https://zh.wikipedia.org/wiki/原码
[2] https://zh.wikipedia.org/wiki/补码