关于定点数、原码、反码、补码的理解定点数: 纯整数、纯⼩数
名字由来: ⼩数点的位置是约定的;纯整数约定虚拟的⼩数点位于最后⼀位之后;纯⼩数约定虚拟的⼩数点位于第⼀位之前;由 于假想了⼀个⼩数点的位置,⽽且这个位置是约定好就不变的,所以叫做定点数
定点数的表⽰:原码、反码、补码
原码
原码的存在是最⾃然⽽然的。把⼈类书写的数按转换成⼆进制,正常情况下,转换成的⼆进制有位数要求,⽐如,转换成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. 互为相反数的两个数,其反码各位均异,导致其相加结果为-0(原码中痛点的延续,此时成为了最⼤的痛点,这就是主要⽭盾消失
了,次要⽭盾就会显现出来),⽽-0与+0的同时存在使得同⼀个数值对应不同的编码,造成很多⿇烦;
2. 使⽤反码计算减法,要使⽤循环进位,即最⾼位有溢出,则加1,否则,不加1。这⼀点使得在正数原码加法的基础上要求新的硬件结
构。
接下来解释⼀下反码计算的本质,反码计算如果没有循环进位,就什么也不是,⽽循环进位,并不是因为恰好每当这个时候加⼀个1就是正确答案,它的根源在于计算的机器字位数有限,能表⽰的范围有限。
接下来描述机器字有限的后果。例如⼋位,只能表⽰-127到+127,如果不把第⼀位当做符号位,其范围也只是0到255。255再加1时,发⽣溢出,但是溢出只是因为没有更⾼的位来存我们的进位,此时现有的⼋位为0000 0000,若不计溢出,即我们规定最⾼位没有进位,则此时值为0;以此类推,256=0,257=1,...,511=255,512=0,...;永远在0到255之间循环往复,这就是取模运算。
需要区分的是取模运算和取余运算,两者的计算原理都是⼀样的,a/d,其中a、b、c、d都是整
数,取余或者取模运算的结果就是d。但是a/d的结果并不是唯⼀的,⽐如 4/4,或者4/-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. 负数的反码加1
2. 负数的原码,从右到左到第⼀个1,以此为分界线,右边的0都不变,⽽左边除符号位按位取反
从反码的本质看补码的产⽣:
(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即可。