原码的存在是最自然而然的。把人类书写的数按转换成二进制,正常情况下,转换成的二进制有位数要求,比如,转换成8位的,等等。但是如果给出的数是5,转换成二进制是三位,101,这个时候要在左面补0,补齐八位,成为0000 0101。但是,这个时候的二进制数并不是我们所要的原码,还缺了最后一步:符号。如果是正数,最左面的位,也就是最高位要置0,反之,是负数就将其置1。这样5的表示是0000 0101,而-5的表示是 1000 0101。
这样,在读的时候,机器默认8位的最高位是符号位,表示数的正负,而剩余的7位由二进制转换为十进制,即可得到我们惯用的表示。
所以显而易见,如果给出的数是255,他的二进制数是1111 1111,按照我们上面的逻辑,它是正数,应该把最高位置0,但是,0111 1111,机器将会读成127;与我们最初的数并不相同,即数据出错。所以啊,8位能表示的数的范围就是-127到127的整数,如果不在范围内,却转换成8位来存,读错也是没有办法的事情。
事实上,计算机所能存的数受机器字位数的限制,超出以后就发生溢出,将不能保证数据的正确性。除此之外,由于计算机设计的原因,机器字所能表示的范围,不止受机器字位数的限制,也受设计过程中默认的约定的影响,一些特殊的值对应的码可能会被赋予其他含义。
原码表示中有一个特殊的数,0。0有两种表示,+0和-0。其原码分别为1000 0000,和0000 0000。但是,这是原码的一个痛点(然而并不是最大的痛点,最大的痛点是原码,没有减法)。
我所能想到的我们为什么要有反码的原因,就是减法了。
虽然最后证明是补码拯救了定点数的减法,但是我想反码作为一个中间产品出现,虽然有不足,但也解决了原码的某些问题。
首先抛出结论,反码可以解决减法问题,但是有两点瑕疵:
接下来解释一下反码计算的本质,反码计算如果没有循环进位,就什么也不是,而循环进位,并不是因为恰好每当这个时候加一个1就是正确答案,它的根源在于计算的机器字位数有限,能表示的范围有限。
接下来描述机器字有限的后果。例如八位,只能表示-127到+127,如果不把第一位当做符号位,其范围也只是0到255。255再加1时,发生溢出,但是溢出只是因为没有更高的位来存我们的进位,此时现有的八位为0000 0000,若不计溢出,即我们规定最高位没有进位,则此时值为0;以此类推,256=0,257=1,...,511=255,512=0,...;永远在0到255之间循环往复,这就是取模运算。
需要区分的是取模运算和取余运算,两者的计算原理都是一样的,a/b=c......d,其中a、b、c、d都是整数,取余或者取模运算的结果就是d。但是a/b=c......d的结果并不是唯一的,比如 4/8=0......4,或者4/8=1......-4,再比如-7/2=-3......-1,或者-7/2=-4......1。这其中的不同,导致了取余和取模的区分。对于取余运算来说,要求商尽可能趋近0,比如虽然4/8的商可以是0,也可以使1,但是因为我们要趋近0,所以对于取余运算来说,取得的余数就是4;在比如,-7/2的商可以是-3,也可以使-4,但是因为要趋近与0,所以商是-3,余数是-1。取模运算并不是与取余运算完全相反,因为它并不是要求尽量不趋近于0,而是要求尽量趋近于负无穷,所以,对于商在零点以及正半轴来说,跟取余运算是一样的,但是对于负半轴来说,恰好相反,比如,-7/2的商,因为要求趋近于负半轴,所以商是-4,余数是1。事实上,取模运算的整数商要趋于负无穷,这一要求的本质是,要求余数必须是大于等于0的数。也就是说,我们取模运算得到的结果一定是自然数。而自然数对于计算机来说是无符号整数,表示是最简单的了。
所以取模运算是计算机一个十分重要的运算,不仅是因为机器字的有限位决定了取模运算的常见性,更重要的是因为取模运算本身契合了计算机的位数有限,无符号整型的表示。
反码减法的本质是,将有符号整型的减法,转换成无符号整型的加法,而无符号整型的加法是模运算。
(1)z=y-x z'=y-x+256=y+[128+(127-x)]+1 z是我们想要的结果
如果z>=0,则z'%256=z,z'/256=1
如果z<0,则z'%256=256-|z|=128+(127-|z|)+1=z’,所以y+[128+(127- x)]=128+(127-|z|), z'/256=0
(2)z=-y-x z'=256*2-y-x=[128+(127-x)]+[128+(127-y)]+2
z'%256=256-|z|=128+(127-|z|)+1,所以[128+(127-x)]+[128+(127-y)]+1=128+(127- |z|), z'/256=1
总结以上两点,在参与运算时,负数全部化成128+(127-x)的形式,转化后的两数相加得到和,结果为和取模与和除以模的商相加,如果此时的结果大于128,那么说明结果是负数,现在的形式是128+(127-z);反之,说明结果是正数,现在的形式即为结果。这就是反码和循环进位的本来面目了。
说实话,从原码到反码,是一项十分精彩的进步,之后从反码到补码就没有这个精彩了。
补码和反码的亲缘关系很近。补码之所以成为定点数减法的最终解决方案,在于他完美地解决了反码中的两个痛点。
补码的定义:
从反码的本质看补码的产生:
(1)z=y-x z'=y-x+256=y+[128+(127-x+1)] z是我们想要的结果
如果z>=0,则z'%256=z
如果z<0,则z'%256=256-|z|=128+(127-|z|+1)=z',所以y+[128+(127- x+1)]=128+(127-|z|+1)
(2)z=-y-x z'=256*2-y-x=[128+(127-x+1)]+[128+(127-y+1)]
z'%256=256-|z|=128+(127-|z|+1),所以[128+(127-x+1)]+[128+(127-y+1)]=128+(127-|z|+1)
而从无符号整型的角度看负数的补码即为128+(127-x+1)
由上可知反码的本质,也是补码的本质,改变了形式,使得对于商的判断融入了取模运算本身简化了运算的逻辑。然而这一举措的实质是,对于反码来说,-x的反码是(模-1-x),而-x的补码是(模-x),从“模-1”到“模”的变化,是从反码到补码的变化的实质。举个例子,y-x,反码的计算,从无符号整型的角度看,本身是y+[128+(127-x)],即255+(y-x),当(y-x)大于0的时候,由于计算机的模运算,结果将会变为[255+(y-x)]%256=[256+(y-x-1)]%256=y-x-1,即,在存在进位的情况下,反码计算的直接结果,由于为了进位满足模,而从后面的式子里拿走了一个1,导致结果总是少1,所以循环进位需要加1。
而现在,在补码的运算过程中,从无符号整型的角度看,本身是y+[128+(127-x+1)],即256+(y-x),当y-x是正数以及0的情况下,计算机的取模运算直接消除了式子中的256,得到正确结果。所以说,从反码到补码的变化实质,是从“模-1”到“模”的变化。
根据以上解释,稍加思考,就能明白所谓的循环进位,本质是为了弥补进位从后面式子中拿走的1,所以在存在进位的情况下,要为得到的结果加1。而在补码中,并不存在为了进位而从后面的式子里拿1这个操作,所以,进位不进位并不造成影响。由此,消除了循环进位。
同时,由于补码的形式,使得-0的补码与+0的补码相同,即都为0000 0000,消除了0的编码不唯一的缺点(总算是克服了)。同时,由于八位的取值是从-127到127,所以补码也是从256-127到256-0,以及从0到127,并起来,即为[0,127]并[129,256],其中128没有任何一个数的补码是它。约定128是-128(这个本来不存在的数)的补码。
注意:关于定点数的位数扩展,需要注意的是,位数扩展要向远离小数点的方向扩展。扩展时,左边扩展出的位,用符号位填充(纯整数),右边扩展出的位用0填充(纯小数)
算术右移,左边空出来的位置要用符号位补齐;算术左移,右边空出来的位置补0即可。