【CC++语言入门篇】-- 剖析浮点数 | ||||||||||||||||
在IEEE标准中,浮点数是将特定长度的连续字节的所有二进制位分割为特定宽度的符号域,指数域和尾数域三个域,其中保存的值分别用于表示给定二进制浮点数中的符号,指数和尾数。这样,通过尾数和可以调节的指数(所以称为"浮点")就可以表达给定的数值了。具体的格式: 符号位阶码尾数长度 我们都知道浮点数在32位机子上有两种精度,float占32位,double占64位。很多朋友喜欢把double用于8字节的数据存储。从这点我们应该不要特殊看到浮点数的内存存储形式,他跟整数没有什么区别,只是在这4字节或者8字节里有3个区域,整数有符号只有符号位及后面的数值,之所以最高位表示有符号数的符号位。原因之一在于0x7fffffff位最大整数,为整个32位所能表示的最大无符号整数0xffffffff的一半减一,也就是:比如1字节:无符号是:0xff,有符号正数为:(0,127],负数为[-128,0)。在8位有符号时,肯定内存值大于等于:0x80。二进制就是10000000,比他大,只会在低7位上变化,最高位已经是1了,变了就变小了。所以这里也是一个比较巧用的地方,一举两得。 1.从浮点数到16进制数 floatvar=5.2f; 就这个浮点数,我们一步一步将它转换为16进制数。 首先,整数部分5,4位二进制表示为:0101。 其次,小数部分0.2,我们应该学了小数转换为二进制的计算方法,那么就是依次乘以2,取整数部分作为二进制数,取小数部分继续乘以2,一直算到小数结果为0为止。那么对0.2进行计算: 0.2*2=0.4*2=0.8*2=1.6(0.6)*2=1.2(0.2)*2=0.4*2=0.8*2=1.6(0.6)*2=1.2...... 00110011...... 因此,这里把0.2的二进制就计算出来了,结果就为:0.00110011......这里的省略号是你没有办法计算完。二进制序列无限循环,没有到达结果为0的那一天。那么此时我们该怎么办?这里就得取到一定的二进制位数后停止计算,然后舍入。我们知道,float是32位,后面尾数的长度只能最大23位。因此,计算结束的时候,整数部分加上小数部分的二进制一共23位二进制。因此5.2的二进制表示就为: 101.00110011001100110011 一共23位。 此时,使用科学计数法表示,结果为: 1.0100110011001100110011*22 由于我们规定,使用二进制科学计数法后,小数点左边必须为1(肯定为1嘛,为0的话那不就是0.xxxx*sxxx了,这样没有什么意义),这里不能为0是有一个很大的好处的,为什么?因为规定为1,这样这个1就不用存储了,我们在从16进制数换算到浮点数的时候加上这个1就是了,因为我们知道这里应该有个1,省略到这个1的目的是为了后面的小数部分能够多表示一位,精度就更高一些了哟。那么省略到小数点前面的1后的结果为: .01001100110011001100110*22 这里后面蓝色的0就是补上的,这里不是随便补的一个0,而是0.2的二进制在这一位上本来就应该为0,如果该为1,我们就得补上一个1.是不是这样多了一位后,实际上我们用23位表示了24位的数据量。有一个位是隐藏了,固定为1的。我们不必记录它。 但是,在对阶或向右规格化时,尾数要向右移位,这样被右移的尾数的低位部分会被丢掉,从而造成一定的误差,因此要进行舍入处理。常用的舍入方法有两种:一种是“0舍1入”法,即如果右移时被丢掉数位的最高位为0则舍去,为1则将尾数的末位加“1”,另一种是“恒置1”,即只要数位被移掉,就在尾数的末位恒置“1”。 举个例子: 123.456的二进制表示: 123.456的二进制到23位时:1111011.011101001011110001... 后面还有依次为01...等低位,由于最高位的1会被隐藏,向后扩展一位如果不做舍入操作则结果为: 1.11101101110100101111000*26 但是经过舍入操作后,由于被舍掉的位的最高位是1,或者“恒置1”法,最后面的0都应该是1。因此最终就应该是: 1.11101101110100101111001*26 在这里需要说明,不管是恒置1,还是0舍1入法,其根本都是为了减小误差。 好了,尾数在这里就计算好了,他就是01001100110011001100110。 再来看阶数,这里我们知道是2^2次方,那么指数就是2。同样IEEE标准又规定了,因为中间的阶码在float中是占8位,而这个阶码又是有符号的(意思就是说,可以有2^-2次方的形式)。 float类型的偏置量Bias=2k-1-1=28-1-1=127,但还要补上刚才因为左移作为小数部分的2位(也就是科学技术法的指数),因此偏置量为127+2=129,就是IEEE浮点数表示标准: V=(-1)s×M×2E E=e-Bias 中的e,此前计算Bias=127,刚好验证了E=129-127=2。 因此,拼接起来后: 1000000101001100110011001100110 |←8位→||←-------------23位-------------→| 一共就是31位了,这里还差一位,那就是符号位,我们定义的是5.2,正数。因此这里最高位是0,1表示负数。 而后结果就是: 01000000101001100110011001100110 1位|←8位→||←--------------23位------------→| 到这里,我们内存里面的十六进制数产生了,分开来看: 01000000101001100110011001100110 40A66666 2.从十六进制数到浮点数 我们还是可以用上面5.2的例子,再将0x40A66666换算回去,用同样一个例子,结果更直观,逆运算更好理解。那我们就开始吧。 首先,要还原回去,必须将这个16进制用我们的计算器换算成二进制: 01000000101001100110011001100110 我是COPY上面的。这里颜色已经很明显了,我划分成了3个区域。 其次看绿色的8位,换成10进制就是:12910 我们逆运算,知道这里需要129-127=2得到指数,得到了指数,我们便知道我们小数点是向哪个方向移动了好多位。脑子里已经有了一个科学计数法的锥形。 再次把红色的23位提取出来,这里不把它换成10进制,因为我们指数是表示的二进制上移动了多少位,底数是2,而不是10。 这里因为之前我们都知道有个固定的1给省略了,因此这里要给加上去。加上去之后: 101001100110011001100110 这里是24位,我们先不管,小数点添进去: 1.01001100110011001100110*22 101.001100110011001100110 到这里,就真正可以把整数部分换成十进制了: 101.001100110011001100110 5.xxxxxxxxxxxxxxxxxxxxxxxxxxxxx 我们知道了,整数部分是5,后面的小数部分再进行逆运算: 这里我们就应该想想小数到二进制数是乘法,这里逆运算就应该除以2,因此就可以表示为: 0.001100110011001100110 0+0*2-1+0*2-2+1*2-3+1*2-4+0*2-5+0*2-6+1*2-7+......+0*2-21这样一个式子,我们算出结果来,放在浮点数里: 5.1999998。 因此我们可以看到精度已经有损失了。 问题一:写写-5.2的16进制数? 再来看一个例子: floatvar=0.5,算16进制数。 首先,0.5整数部分为0,这里就不处理了。 其次,0.5小数部分,二进制表示为:0.1 这里是0.1,将尾数补满23位则是: 0.1000000000000000000002 由于小数点左边是0,因此需要向右移动一位,因此: 1.0000000000000000000002*2-1 这里1又被省略掉,所以23位全部变成了0,因此: .00000000000000000000002*2-1 然后,因为这里指数是-1,因此阶码就是:-1+127=126=011111102 这样一来,阶码就有了,由于又是正数,那么组合起来: 00111111000000000000000000000000 这样一来,最终的16进制数则为:0x3f000000. 是不是很简单啊。 这里就不再具体说明怎么换算的了,只需要提到2个地方: 一是,中间的阶码在double中占有11位,因此就不是+127了,而是加上1023,因为11位能表示的最大无符号数是2047,因此有符号范围[-1024,1023]。 二是,尾数是52位,因此精度更高,能表示的数也就越大。我们在换算5.2的时候,后面的小数二进制+前面的5的二进制再省略一位后的总位数要填满52位。 好了,浮点数也没有太多要说的,就到这里吧,在用的时候注意精度和范围就可以了。 最后在提一个问题: 问题二: floatvar2=50000.2; floatvar3=5000000.2; 观察这几个数,加深一下那三个域的计算方式,并说出这些数据有什么规律? |