2.2 浮点数类型
计算机也需要运算和存储数学中的实数。在计算机的发展过程中,曾产生过多种存储实数的方式,有的现在已经很少使用了。不管如何存储,我们都可以划分为定点实数存储方式和浮点实数存储方式这两种。所谓定点实数,就是约定整数位和小数位的长度,比如用4字节存储实数,我们可以约定两个高字节存放整数部分,两个低字节存储小数部分。这样的好处是计算的效率高,缺点也显而易见,存储不灵活,比如我们想存储65536.5,由于整数的表达范围超过了2字节,用定点实数存储方式就无法实现了。对应地,也有浮点实数存储方式,道理很简单,就是用一部分二进制位存放小数点的位置信息,我们可以称之为“指数域”,其他的数据位用来存储没有小数点时的数据和符号,我们可以称之为“数据域”、“符号域”。在访问时取得指数域,与数据域运算后得到真值,如67.625,利用浮点实数存储方式,数据域可以记录为67625,小数点的位置可以记为10的-3次方,对该数进行访问时计算一下即可。浮点实数存储方式的优缺点和定点实数存储方式的优缺点是相反的。在80286之前,程序员常常为实数的计算伤脑筋,而后来推出了浮点协处理器,可协助主处理器分担浮点运算,程序员计算实数的效率也就提升了,于是浮点实数存储方式也就普及开来,成为现在主流的实数存储方式。但是,在一些条件恶劣的嵌入式开发场合,仍可看到定点实数的存储和使用。
在C/C++中,使用浮点方式存储实数,用两种数据类型来保存浮点数:float(单精度)、double(双精度)。float在内存中占4字节空间,double在内存中占用8字节空间。由于占用空间大,double可描述的精度更高。这两种数据类型在内存中同样以十六进制方式进行存储,但与整型类型有所不同。
整型类型是将十进制转换成二进制保存在内存中,以十六进制方式显示。浮点类型并不是将一个浮点小数直接转换成二进制数保存,而是将浮点小数转换成的二进制码重新编码,再进行存储。C/C++的浮点数是有符号的。
在C/C++中,将浮点数强制转换为整数时,不会采用数学上四舍五入的方式,而是舍弃掉小数部分(第4章会提到的“向0取整”),不会进位。
浮点数的操作不会用到通用寄存器,而会使用浮点协处理器的浮点寄存器,专门对浮点数进行运算处理,见2.2.2小节。Microsoft Visual C++6.0在使用浮点数前,需先对浮点寄存器进行初始化,然后才能正常运行。未初始化浮点寄存器的代码如代码清单2-1所示。
代码清单2-1 未初始化浮点寄存器
int main(int argc, char*argv[])
{
//在未使用到浮点数情况下,
//在Visual C++6.0中输入小数会报错,因没有对浮点寄存器进行初始化
int nInt=0;
scanf("%f",&nInt);
}
在代码清单2-1所示的运行程序中输入小数将会导致程序崩溃,这是由于在浮点寄存器没有初始化时使用浮点操作,将无法转换小数部分。
解决办法是:在代码中的任意位置定义一个浮点类型的变量即可对浮点寄存器进行初始化。
2.2.1 浮点数的编码方式
浮点数编码转换采用的是IEEE规定的编码标准,float和double这两种类型数据的转换原理相同,但由于表示的范围不一样,编码方式有些许区别。IEEE规定的浮点数编码会将一个浮点数转换为二进制数。以科学记数法划分,将浮点数拆分为3部分:符号、指数、尾数。
1.float类型的IEEE编码
float类型在内存中占4字节(32位)。最高位用于表示符号;在剩余的31位中,从右向左取8位用于表示指数,其余用于表示尾数,如图2-2所示。
图 2-2 float类型的二进制表示说明
在进行二进制转换前,需要对单精度浮点数进行科学记数法转换。例如,将float类型的12.25f转换为IEEE编码,需将12.25f转换成对应的二进制数1100.01,整数部分为1100,小数部分为01;小数点向左移动,每移动1次指数加1,移动到除符号位的最高位为1处,停止移动,这里移动3次。对12.25f进行科学记数法转换后二进制部分为1.10001,指数部分为3。在IEEE编码中,由于在二进制情况下,最高位始终为1,为一个恒定值,故将其忽略不计。这里是一个正数,所以符号位添0。
12.25 经IEEE转换后各位的情况:
符号位:0
指数位:十进制3+127,转换为二进制是10000010
尾数位:10001 000000000000000000(当不足23位时,低位补0填充)
由于尾数位中最高位1是恒定值,故省略不计,只要在转换回十进制数时加1即可。为什么指数位要加127呢?由于指数可能出现负数,十进制数127可表示为二进制数01111111。IEEE编码方式规定,当指数域小于0111111时为一个负数,反之为正数,因此01111111为0。
12.25 f转换后的IEEE编码按二进制拼接为01000001010001000000000000000000。转换成十六进制数为0x41440000,内存中以小尾方式进行排列,故为00 00 44 41。分析结果如图2-3所示。
图 2-3 单精度浮点数12.25f转换为IEEE编码
上面演示了符号位为正,指数位也为正的情况。那么什么情况下指数位可以为负呢?根据科学记数法,小数点向整数部分移动时,指数做加法。相反,小数点向小数部分移动时,指数需要以0起始做减法。浮点数-0.125f转换IEEE编码后,将会是一个符号位为1,指数部分为负的小数。-0.125f经转换后二进制部分为0.001,用科学记数法表示为1.0;指数为-3。
-0. 125fIEEE转换后各位的情况:
符号位:1
指数位:十进制127+(-3),转换为二进制是01111100,如果不足8位,则高位补0
尾数位:00000000000000000000000
-0. 125f转换后的IEEE编码二进制拼接为10111110000000000000000000000000。转换成十六进制数为0xBE000000,内存中显示为00 00 00 BE,分析结果如图2-4所示。
图 2-4 单精度浮点数-0.125f转换为IEEE编码
上面的两个浮点数小数部分转换为二进制时都是有穷的,如果小数部分转换为二进制时得到一个无穷值,则会根据尾数部分的长度舍弃多余的部分。单精度浮点数1.3f,小数部分转换为二进制就会产生无穷值,依次转换为:0.3、0.6、1.2、0.4、0.8、1.6、1.2、0.4、0.8……转换后得到的二进制数为1.01001100110011001100110,到第23位终止,尾数部分无法保存更大的值。
1.3 f经IEEE转换后各位的情况:
符号位:0
指数位:十进制0+127,转换二进制01111111
尾数位:01001100110011001100110
1.3 f转换后的IEEE编码二进制拼接为00111111101001100110011001100110。转换成十六进制数为0x3FA66666,内存中显示为66 66 A6 3F。由于在转换二进制过程中产生了无穷值,舍弃了部分位数,所以进行IEEE编码转换后得到的是一个近似值,存在一定的误差。再次将这个IEEE编码值转换成十进制小数,得到的值为1.2516582,四舍五入之后为1.3。这就解释了为什么C++在比较浮点数值是否为0时,要做一个区间比较而不是直接进行等值比较。正确浮点数比较的代码见代码清单2-2。
代码清单2-2 正确浮点数比较
float fTemp=0.0001f;//精确范围
if(fFloat>=-fTemp&&fFloat<=fTemp)
{
//fTemp等于0
}
2.double类型的IEEE编码
前文讲解了单精度浮点类型的IEEE编码。double类型和float类型大同小异,只是double类型表示的范围更大,占用空间更多,是float类型所占用空间的两倍。当然,精准度也会更高。
double类型占8字节的内存空间,同样,最高位也用于表示符号,指数位占11位,剩余42位用于表示位数。
在float中,指数位范围用8位表示,加127后用于判断指数符号。在double中,由于扩大了精度,因此指数范围使用11位正数表示,加1023后可用于指数符号判断。
double类型的IEEE编码转换过程与float类型一样,读者可根据float类型的转换流程来转换double类型,此处不再赘述。