转自:http://blog.csdn.net/techx/article/details/43830781
浮点概念的引入
在计算机系统的发展过程中,曾经提出过多种方法表达实数。比如定点数表示法, 这种表示方法将小数点的位置固定在某一个位置,比如: 11001000.00110001,这个16位(2字节) 的定点数用前面8位表示整数部分,后面8位表示小数部分,这种方法直观,但是固定的小数点位置决定了固定位数的整数部分和小数部分,不利于同时表达特别大的数或者特别小的数。最终,绝大多数现代计算机遵循IEEE754,即IEEE二进制浮点数算数标准,利用科学计数法来表达实数,即用一个尾数(Mantissa or significand),一个基数(Base),一个指数(Exponent)以及一个表示正负的符号来表达实数。比如 123.45 用十进制科学计数法可以表达为 1.2345 × 10^2 ,其中1.2345 为尾数,10 为基数,2为指数。浮点数利用指数达到了浮动小数点的效果,从而可以灵活地表达更大范围的实数。
浮点数在内存中的格式
IEEE 754标准定义了五种浮点数格式,使用基数和该格式占用的内存比特数命名,五种格式分别为:
- 二进制32位浮点数 (binary32)
- 二进制64位浮点数 (binary64)
- 二进制 128位浮点数(binary128)
- 十进制64位浮点数(decimal64)
- 十进制128位浮点数(decimal128)
各种编程语言和编译器一般都支持前两种格式,分别对应于float和double,对于后一种二进制高双精度128位浮点数,一般编译器使用80位表示,也有极个别的编译器将他们视为128位。
在IEEE标准中,浮点数在内存中的表示是将特定长度的连续字节的所有二进制位按特定长度划分为符号域,指数域和尾数域三个连续域。 例如,对于float类型的数据,其在内存中的表示为:
对于double类型,其表示形式如下:
从上面可以看出,float类型在内存中占用的位数为: 1+8+23=32bits, double 类型在内存中占用的比特数为:1+11+52=64bits。
- 第一位s代表符号为,1代表负数,0代表正数。
- 第二个域是指数域,对于单精度float类型,指数域有8位,可以表示 0-255个指数值。指数值规定了小数点的位置,小数点的移动代表了所表示数值的大小。但是,指数可以为正数,也可以为负数。为了处理负指数的情况,实际的指数值按要求需要加上一个偏差(Bias)值作为保存在指数域中的值,单精度数的偏差 值为 -127,而双精度double类型的偏差值为 -1023。比如,单精度指数域中的64 则表示实际的指数值 -63。 偏差的引入使得对于单精度数,实际可以表达的指数值的范围就变成-127 到 128 之间(包含两端)。我们不久还将看到,实际的指数值-127(保存为 全 0)以及 +128(保存为全1)保留用作特殊值的处理。这样,实际可以表达的有效指数范围就在 -126 和 +127 之间。
- 第三个域为尾数域,其中单精度数为 23 位长,双精度数为 52 位长。比如一个单精度尾数域中的值为: 00001001000101010101000, 第二个域中的指数值则规定了小数点在尾数串中的位置,默认情况下小数点位于尾数串首位之前。 比如指数值为 -1,则该float数即为:.000001001000101010101000,如果为+1,则该float 数值为:0.0001001000101010101000。我们知道引入浮点数的目的在于用尽可能少的位数表示既高精度又大范围的实数,其中的范围大小是由指数域位长确定的,而尾数域的长度则确定了所能表示实数的精度,所以double比float数的精度更高,范围更大,相应的也就占用更多的内存。 刚才我们介绍的对尾数域中的值的解释并不能实现这个精度最大化的目标,因为在尾数串第一个”1”之前还有4个”0”,这4个”0”实际上是多余的,因为我们把小数点向前移动时,前端的"0"是自动添加的,所以可以把这4个“0”删除,然后尾数域末端多出4个位来表示更高精度的数值。也就是说尾数的第一位一定是"1",那么既然第一位一定是"1",那么我们也就没有必要把它存储在尾数域中,而是直接默认尾数为1.xxxx…xxx的形式。尾数的首位从小数点后开始。那么上面的例子所表示的尾数就是:
1.00001001000101010101000。 用23位表示了24位的信息 (小数点不占位置).
浮点数转换成实数
我们已经明白了浮点数在内存中的储存形式。那么下面就用一个例子来强化一下我们对浮点数的理解。 假设我们内存中有一个32位的浮点数:
解释:
s: 1-表示负数
e: 10000010->130->(130-127)=3,也即尾数部分小数点向右移3位。
m: 10110000000000000000000,尾数加上前面省略的1和小数点为:1.10110000000000000000000.
现在三个部分都已经算出来了,通过指数值调整小数点的位置,得到结果为:
1101.10000000000000000000
转换成10进制,整数部分为:1×23+1×22+1×20=13; 小数部分为:1×2−1=0.5.
最后整数部分加上小数部分,再加上符号,所表示的实数就是:−13.5
特殊值
我们已经知道,指数域实际可以表达的指数值的范围为-127 到128 之间(包含两端)。其中,值-127(保存为 全 0)以及+128(保存为全1)保留用作特殊值的处理。
浮点数中的特殊值主要用于特殊情况或者错误的处理。比如在程序对一个负数进行开平方时,一个特殊的返回值将用于标记这种错误,该值为NaN(Nota Number)。没有这样的特殊值,对于此类错误只能粗暴地终止计算。除了NaN 之外,IEEE标准还定义了±0,±∞ 以及非规范化数(DenormalizedNumber)。
对于单精度浮点数,所有这些特殊值都由保留的特殊指数值-127 和128 来编码。NaN的指数为128(指数域全为1),且尾数域不等于零。IEEE 标准没有要求具体的尾数域,所以NaN 实际上不是一个,而是一族。不同的实现可以自由选择尾数域的值来表达NaN,比如Java 中的常量Float.NaN 的浮点数可能表达为01111111110000000000000000000000,其中尾数域的第一位为 1,其余均为 0(不计隐藏的一位),但这取决系统的硬件架构。Java 中甚至允许程序员自己构造具有特定位模式的 NaN 值(通过Float.intBitsToFloat() 方法)。比如,程序员可以利用这种定制的 NaN 值中的特定位模式来表达某些诊断信息。
和NaN一样,特殊值无穷的指数部分同样为128,不过无穷的尾数域必须全部为0。无穷用于表达计算中产生的上溢(Overflow)问题。比如两个极大的数相乘时,尽管两个操作数本身可以用保存为浮点数,但其结果可能大到无法保存为浮 点数,而必须进行舍入。根据 IEEE 标准,此时不是将结果舍入为可以保存的最大的浮点数(因为这个数可能离实际的结果相差太远而毫无意义),而是将其舍入为无穷。对于负数结果也是如此,只不 过此时舍入为负无穷,也就是说符号域为 1 的无穷。
因为IEEE 标准的浮点数格式中,小数点左侧的1 是隐藏的,而零显然需要尾数必须是零。所以,零也就无法直接用这种格式表达而只能特殊处理。
实际上,零保存为尾数域为全为0,指数域为emin - 1 = -127,也就是说指数域也全为 0。考虑到符号域的作用,所以存在着两个零,即 +0 和 -0。不同于正负无穷之间是有序的,IEEE 标准规定正负零是相等的。
零有正负之分,的确非常容易让人困惑。这一点是基于数值分析的多种考虑,经利弊权衡后形成的结果。有符号的零可以避免运算中,特别是涉及无穷的运算 中,符号信息的丢失。举例而言,如果零无符号,则等式 1/(1/x) = x 当x = ±∞ 时不再成立。原因是如果零无符号,1 和正负无穷的比值为同一个零,然后1 与 0 的比值为正无穷,符号没有了。解决这个问题,除非无穷也没有符号。但是无穷的符号表达了上溢发生在数轴的哪一侧,这个信息显然是不能不要的。零有符号也造 成了其它问题,比如当 x=y 时,等式1/x = 1/y 在 x 和 y 分别为 +0 和 -0 时,两端分别为正无穷和负无穷而不再成立。当然,解决这个问题的另一个思路是和无穷一样,规定零也是有序的。但是,如果零是有序的,则即使 if(x==0) 这样简单的判断也由于可能是 ±0而变得不确定了。两害取其轻者,零还是无序的好。