1.为什么用补码
首先肯定一点,计算机中非浮点数(浮点数没研究过)都是用补码表示二进制数的,为什么用补码而不用原码或者反码,是因为补码系统的最大优点是可以在加法或减法处理中,不需因为数字的正负而使用不同的计算方式。只要一种加法电路就可以处理各种有号数加法,而且减法可以用一个数加上另一个数的补码来表示,因此只要有加法电路及补码电路即可完成各种有号数加法及减法,在电路设计上相当方便。减法电路得设计相对而言更复杂。
2. 为什么补码可以实现减法
要想理解这一点,首先必须知道同余的概念。想象一个时钟,如下图
现在是8点钟,如果我想时间变到6点中,那么我有两种方法,第一种顺时针10个小时,第二种逆时针旋转2个小时,都可以得到6点钟。但是就本质而言,这两种方法得到的6点钟,其实是不同的。如果8点是上午8点,那么第一钟方法得到的其实是下午的6点,但第二钟方法得到的是上午的6点。但是对于这个表而言,它只有1点到12点,并没有上午下午之分,对于它而言,这两张方法得到的都是6点钟(时钟指针都是指向6),并没有区别。
换成计算的方式,第一中方式相当于8+10=186,第二种方式相当于8-2=6。
在这个时钟的例子里,一个时钟最多只能表示12个小时,即从1点到12点。上例中的8点加十个小时,计算结果是18时,但是时钟最多只能表示12时,这种情况下就是溢出了,超出了时钟能表示的范围,最后结果是6点。
那么在这种情况情况下的加减运算都是模12的加减运算,12就是时钟能表示的数字范围。而同余表示两个数除以12,余数相同,称之为同余,用表示。这个概念以及符号最早是高斯提出来的,果然是个伟人。
3.补码的计算
当你查阅资料时,尤其是用百度(程序员还是用google好),你会发现大部分人写的补码的计算方法都是:符号位不变,其他位取反,末位加1。也就是说要算补码,先拿到反码。这里补充一下原码、反码、补码的知识。原码就是二进制数,比如2,用8位二进制就是0000 0010,那反码就是加入符号位(0表示正数,1表示复数),最高位表示符号位,比如-2,用8位二进制就是1000 0010,最后补码,比如-2,就是-2的符号位不变,其他位组为取反,即1111 1101,然后末位加1,即1111 1110。正数的原码反码补码相等,都是原码。
但是如果你感兴趣,自己琢磨,你会发现有个特殊的数字0,按照上述的方法计算会纠结,0的原码是0000 0000,反码是1000 0000,如果除符号位逐位取反,即1111 1111,再加1的话,会产生进位,这个进位,要不要进给符号位,如果给,那就是0000 0000,如果不给,那这个进位浪费掉,最后是1000,0000。而实际的结果是多少呢,是0000 0000,符号位参与运算。而也就是说0的补码还是0。而这个1000 0000其实也是有特殊用途的(后面说)。在整个补码的计算过程中,只有计算0的补码会产生溢出。
上面说了0计算的补码还是0,其实还有一个特殊的补码,即1000 0000,这个补码,你找不到原码,不信你找找看。
其实如果你想得多,你会发现这两个特殊的数。就拿8位的二进制来说,能表示256个数。如果按照上述说的补码来表示的话,正数能表示0-127,刚好128个数,256的一半,那么负数必然也能表示另外一半,难道是(-0)-(-127)?很明显,-0是没有的,因为0的补码还是0,那么(-1)-(-127)只有127个数,剩下一个是什么?就是那个无法计算补码的数1000 0000。既然(-0)没意义,那么这个多出来的数就表示-128,这样刚好不浪费。(补一句,如果用反码表示,那还真的有-0,等于浪费一个数)。这个现象如果你写代码一定能发现,比如java的int类型或者long类型,最小负数的绝对值一定比最大正数大1,不信你试试看。
上述的计算补码的方式其实说复杂了,简单的方法是直接逐位取反,末位加1即可。而且这个反过来也可以,即知道补码求原码,也能这样算。
补码的计算为什么是逐位取反,末位加1,拿8位二进制举例,其最大能表示=256个数,那么一个数逐位取反后,这个取反后的数和原数相加必定等于257,比如1001 0010,逐位取反后,是0110 1101,这两个数相加一定是1111 1111,即257,再加上1,就是258。所以一个数的补码的计算可以概括为一下公式,如果一个数是a,二进制位数是n,那么补码就是-a。
现在,如果你已知一个补码b,让你求原码,你怎么计算,按照逐位取反,末位加1的反向计算可不可以,当然可以,先减一,再逐位取反(符号位不变)。但是如果你将上面的那段推理看明白的话,其实同样按照逐位取反,末尾加1的计算方式即可。(回头看看那个无法求原码的补码1000 0000,按照逐位取反,末位加1,得到的还是1000 0000)
这个模型就是a+b=,那么a=-b,b=-a。
4 补码的同余
对于一般的计算,a的补码我们认为是-a,为什么可以这样。对于a,根据计算公式,其补码为-a,我们需要认定的是-a与-a同余即可。
-a=-a-(*(-1))...-a,即商取-1,得到余数是-a
对于-a,商取0即可,得到余数是自身,与-a的余数相同,得到 -a-a
从而用这种方式计算补码,表示负数。(对于负数取余,各个语言有些差异,主要在取商上面,请看参考文档)
以上,我们得出结论,a的补码即为-a
5 运算
补码只是解决两数相减的运算,对于两正数相加,两负数相减的溢出问题,任然还是问题。也就是说溢出了,就超出了能计算的范围,答案就是错的。
观察一下补码的运算,一个正数减去另一个正数,或者说一个正数加上一个负数。对于补码,以8位举例,最大的数-1,是1111 1111,最小的数-128,是1000 0000,我们看,最大的数是最容易溢出,溢出后表示的就是正数,而计算结果也正是正数。而最小的数反而是最不容易溢出的,那相加的数没溢出,表示的数还是负数。
6 番外
注意,8位二进制能表示256个数是没有质疑的,但是能表示哪256个数,却是人为定义的。
比如负数,表示(-1)-(-128)很容易理解,但是,根据上面的同余概念,(-1)与(-257)同余,
(-128)与(-384)同余。那么,负数部分表示(-257)-(-384)可不可以?当然可以,但是
正数部分表示的数也得相应改变(由(0)-(127)变为(256)-(383)),这样才能保证计算结果结果正确。
参考资料:
https://zh.wikipedia.org/wiki/%E4%BA%8C%E8%A3%9C%E6%95%B8
http://ceeji.net/blog/mod-in-real/