为什么使用补码

2019-04-13 16:12发布

前言: 我在学补码的时候最不明白的是为什么符号位也可以跟后面的位那样做加运算且不对结果造成影响,毕竟符号位代表的是负权重(负的模值)。   补码来历可总结如下:
  • 计算机里面,只有加法器,没有减法器,所有的减法运算,都必须用加法进行。
  • 用补数代替原数(补一个模),可把减法转变为加法。出现的进位就是模,此时的进位,就应该忽略不计(减一个模)。
  • 二进制下,有多少位数参加运算,模就是在 1 的后面加上多少个 0。
  • 补码就是按照这个要求来定义的:正数不变,负数即用模减去绝对值(等价于取反加一)。
首先补充解释一下“模”的概念(不准确):
考虑时钟上时间的计算,假设现在时针指向数字3,若问“6小时前时针指向的数字是几”,则可以:
1. 将时针逆时针拨动6格。
2. 将时针顺时针拨动12 - 6 = 6格。
两者的结果是一样的。这里称12为“模”。
故有 3时 - 6个小时 = 3时 + (12 - 6个小时),这里可以看到将减法转换成加法的过程,即“加上模减去绝对值的差”。 将负数用补码表示,实际上是实现了一种从[-128, 127]到[0, 255]的映射。 按上述思路,如果以4bits长度对-6进行编码,需要补一个模的值,那么其补码就是10000-0110=1010。通过原码求“补码”就是一个补模运算,所以把它叫做“补码”。在将减法转化为加法的运算后,可能会产生进位,这个进位的本质就是我们在对-6编码时补上的那个模,所以直接舍去得到的才是正确的结果。 这时候引出了一个新的问题:难道我们在内存中就以1010来保存-6吗?显然不对,因为如果这样的话我们无法区分它到底是某个正值(10)的原码,还是某个负数(-6)的补码。 事实上,这里得到的1010还不是严格意义上的补码(这就是为什么上面的描述中打上了双引号),它其实应该叫作补数。不过已经很接近了。我们为了使补码满足将减法转化为加法这一性质而定义出了补码的构造方式,但却使得补码本身丧失了直观的意义(使得我们无法直接从补码看出它所对应的负数值)。为此,我们添加一个负权重的最高位:将1010变成11010,相当于把之前编写补码时补上的模值又减去了。这下我们就可以说我们得到的这个5bits的11010就是-6最终的补码了,而且可以这样来计算:-2^4(最高位是负权重)+(2^3+2^1)(补数的值)。    计算3-6。补码的产生是源于机器本身的特性:一方面,机器只能进行加法操作,为此,我们最初的动机是必须设计一种编码方式可以将“减去一个数”这一运算变成“加上一个数”的运算。另一方面,机器的加法运算还有一个特性是会发生溢出,以3bits为例,当加和为111后将跳转为000(意味着无法继续进位,最高位被舍弃)再重新开始一个进位周期。 了解到机器的这一特性,我们应该设计一种编码方式,它应满足:一方面我们想要实施的减法在机器上应体现为编码的加法(减法运算应该以“执行加法”的形式被操作);另一方面,要具有表达正数和负数的能力(尤其是当值为负数的时候能从编码正确解析之)。 计算6-3。-011(-3)被编码为两部分(4bits):符号位+补数位。补数为101(5),就是-3补上一个模(8)。这样,为了计算110(6)-011(3),在机器时实际上是执行补数的加法运算“110(6)+101(5)=[1]011”,舍去高位1得到011(3),恰好就是正确结果。加上一个模值在机器上就是走过了一个进位周期又回到了原状。从-011编码成补数(101)后,只是为了使得实际上的减法被转换位机器上的加法好执行,但编码101本身没有体现出-011应该有的负数值。由于编码补数的实质是从-011上加了一个模值,所以我们可以在101上添加一个最高位并设为负权重(即增加一个负的模值),得到新的编码为:1101(-5)。这样,最终的这个编码(称为补码)就满足了上面提到过的两点要求:一是实际上的减法体现为补码的加法(具体地说是补数的加法);二是负数补码运算的结果能被正确解析。 计算3-6。-110(-6)被编码为两部分(4bits):符号位+补数位。先解释补数位:补数位是010(2),就是-6补上一个模(8)。这样,为了计算“011(3)减去110(6)”,在机器上实际上是执行补数的加法运算“011(3)加上010(2)=101(5)”。再解释符号位:从-110抽取出补数位部分后,虽然完成了从“减法”到“加法”的转变,但同时也将110变成了010,也就是它的字面值被改变了。为此,我们必须把求补数时加上的模值再减去。为了不对补数位上的运算造成影响,我们添加一个符号位在最高位上,设该位为负模值。也就是将-6编码为:1010,它实际上的含义是:1000 + 010。1000是符号位,表示-2^3,010是补数位,表示6在模8下的补数2。 符号位跟补数采用一样的加法和进位,符号位体现的是大的负值的权重,补数位体现的是小的正值的权重。两个负数的补码相加:符号位必然进位,如果补数位求和后没有产生进位那么就负得太多而没有得到小正值进位的补偿,叫负溢出。一正一负相加:符号位保持不变,若补数位求和后没有产生进位,不影响大负值权重,结果肯定对;若补数求和产生了进位,这个进位体现的是正的权重,而原先的符号位体现的是负的权重,1+1再继续进位,从而符号位变为0,达到的效果恰好就是正权重与负权重抵消,得到正确结果。 一个正数与一个负数相加,其值必然位于这两个数之间,符号位不会进位维持其负权重,补数相加有可能会进位。若进位了则符号位变为0,意味着求和为正值。 两个负数相加,两个负权重的符号位相加导致了较大的负值,此时正常情况下补数之和应该要产生进位。如果两个补数都偏小(对应两个补码值负得较厉害),无法产生进位的话,和的符号位为0,称为负溢出。   另一种理解方法:一般的思路得到的编码方法在计算正数+正数或者负数+负数时都没有问题,因为符号位都不发生变化。关键在于处理正数+负数时不好处理。自然的想法是把负数拆成一个大的负数+一个小的正数。大负数放在高位,小正数放在低位。那么正数+负数就转化为正数+小正数+大负数。由于大负数占据高位,而正数+小正数可能有两种结果:一是没有产生向高位的进位,则不影响大负数,结果肯定对;二是产生了进位,这个进位体现的是正的权重,而原先的高位体现的是负的权重,两者相加为零(舍去向更高位的进位)就抵消了,结果也是对的。