为什么计算机采用补码而不是原码或反码?

1209 2021-11-25 21:37

转自知乎

醉卧沙场
Red Hat 工程师

首先,你要知道什么是补码?或者说为什么补码叫“补码”?然后我们再来说补码相对于反码和原码的优势。

为什么补码叫“补码”

补码的真正名称叫"Two's complement",反码的名称叫"Ones' Complement",原码叫"Sign-Magnitude"。

从名称上我们可以看出其命名时的原始意义:

  • 原码很好理解,就是"Sign"的"Magnitude",有符号的一个量,顾名思义它用一个位来表示一个量的符号(正或者负)。
  • 反 码从英文原意上我们看不出"反"是从哪翻译来的,反而"Complement"更容易让我们理解成“补”,那为什么这个不叫补码而叫反码呢?其实反码的英 文直译过来就是“1的补集”(我自己直译的,并无权威参考)。注意One是带"s"的,说明是一或多个1。其实反码的定义来源于它原来的计算方式,也就是 -x的反码为:
-x = [11111...1] - x

也 就是从一堆1中把-x的绝对值的1去除(有多少1看是多少位的数),剩下的就是-x的反码表示。所以“1的补集”这种说法就是说针对所有字节位都是1的集 合S来说,x的绝对值代表了一个1的集合A,那-x就是子集A在集合S中的绝对补集。比如8位数的-1就是正数1相对于[11111111]集合的一个补 集,也就是[11111110]。可能我们翻译过来的时候觉得就是一个取反的操作,所以叫反码(没有考证)。

  • 补码。有了上面反码的解释,我应该能注意到补码的名称直译好像是"2的补全"(注意complement并没有一个绝对的名词术语定义,其英文用法可以参考 https://www.yourdictionary.com/complement)。那么我们怎么理解这里的"2的补全"呢?

还 是得回到补码的原始计算来探究其名称的来源。首先我们在大学时学到计算补码时,老师教我们的可能是一个负数的补码等于其正数的源码取反加1。“取反加1” 这种操作虽然在计算上可以让我们容易记住(如果你学的不是这个就不用对号入座了)。但是容易留下一个疑问,反码是取反很好理解,为什么补码是取反加1,这 么麻烦计算机不觉得累么?(相信我,这是当年众多第一次学习计算机导论的2B准程序员们的共同困扰!什么?你从没这困扰?那要么是你碰到一个好老师,要么 是你从其他地方事先了解了计算机编码,要么你是万里挑一的天才,要么你就根本不善于发现问题不适合从事计算机)

实际上补码定义的时候的计算不是“取反加1”,而是:

-x = 2^w - x

其实2的w次中的w表示x是一个多少位的数。我们以3位数来举例:

设 -x = -1, w=3, 那么-x的补码表示就是(2^3 - x) = 1000B - 001B = 111B

设 -x = -3, w=8, 那么-x的补码表示就是(2^8 - x) = 100000000B - 00000011B = 11111101B

...

所以这里"Two's"的意思就是"2^w"里的2,所以没有加's'表示复数。而它补的就是这个"2^w"。

(我不明白为什么上面的计算方式也有人要diss一下,我这里只是在试图通过描述一个事物的起源来了解其意义,不是在探讨最初人们定义它的方式是否优劣,那是另外的问题。)

补码的优势

说完补码的由来,接下来说一下为什么计算机普遍采用补码而不是原码或反码。实际上历史上是有采用过反码的计算机的,只是至今为止绝大部分都是补码。为什么呢?因为补码有两大好处:

  • 省去计算机判断符号位或者说判断+/-运算的麻烦。采用补码表示后,不管是加法还是减法都是加法运算。

想 象一下,一个8位数的"-1+3"运算,如果用原码:10000001 + 000000011,这样你不能直接相加,否则10000100的结果不伦不类。你需要检查符号位,甚至可能要对符号位和运算符做调整。如果用反码 11111110 + 00000011,这样直接相加的结果不管是00000001还是10000001都不妥,简直不知所云。还是需要适当的处理符号位和运算方式。如果是补 码:11111111 + 00000011 = 00000010,结果直接就是2。这样就避免了对正负或加减的区别处理。

  • 尽量保证了系统编码的连续性和一致性,同时避免了+/- 0的窘境。

计算机是逻辑严谨的,+/- 0同时存在的这种打破连续性和一致性的问题应该极力规避。而补码可以完美的避开这一Bug,而且还能多表示一个负数(如8位数的-128)。

注意这种保持连续性和一致性的原则是很重要的,它甚至渗透到所有系统的设计之中。不是你表面上看到的浪费一个重复表示这么简单的问题。它会导致从电路设计时就留下的根源性区别和缺陷。有时间的话我们再补充探讨。

 我的总结:

正数的原码就是二进制,负数的原码就是第一位是符号位,符号位填1

反码就是全1减去这个数的绝对值的二进制,符号位填1

补码就是降维打击,这个数的正数加上这个数的负数的补码最后将会得到存储空间升一位的1,也就是2顶到头,同时利用了溢出舍掉多余位。操作上就等同于反码加1.这是术。而第一句话是道。

在C语言中unsigned char里:

-1的补码就是2**8-1=ff=255

 

下载

全部评论

·