揭秘浮点数
引言
下面有几道关于浮点数的题,大家可以看看能否很快做出来
double d = 1.25d;
float f = 1.25f;
System.out.println((a - b) == 0.0);
double c = 0.2;
double d = 0.3;
double e = 0.4;
那么 c-d 与 d-e 是否相等?
A. true B. false
System.out.println(1.0 / 0.0); 的结果是什么?
A. 抛出异常 B. Infinity C. NaN
System.out.println(0.0 / 0.0); 的结果是什么?
A. 抛出异常 B. Infinity C. NaN D. 1.0
public void g(double i) { }
public void g(float d) { }
public void g(long l) { }
public void g(short l) { }
public void g(Double dd) { }
g(1);会调用哪个方法?
大家是否能没有疑惑地得出正确答案呢,如果不能,那我们就往下看吧。
本文主要讲解浮点数的性质和运算,定点数没有做过多说明,有兴趣的童鞋可以自行google。
基本概念
机器数:数字在计算机中的二进制表示形式
真值:机器数所代表的的实际值
原码:机器数最高位表示符号位,其余各位表示二进制数的绝对值
补码:若原码为正数,补码=原码;若原码为负数,补码=2^n+原码
移码:常用来表示浮点数的阶码。它只能表示整数,代表将一个数向左或右偏移若干单位
根据小数点位置是否固定,在计算机中有两种数据格式:定点数和浮点数
定点数和浮点数的区别:
若定点数和浮点数的字长相同,则浮点表示法所能表示的数值范围将远远超于定点表示法;虽然浮点数扩大的数的表示范围,但精度降低了;浮点数运算更复杂;定点数超出数的表示返回就会溢出,而浮点数超出阶码所能表示的范围才会溢出;
溢出:指运算结果超出了数的表示范围
单符号位溢出判别法:参加操作的两个数符号位相同,结果却与原操作数符号不同,则表示结果溢出。
双符号位溢出判别法(s1s2):
- s1s2 = 00,表示结果为正数,无溢出
- s1s2 = 01,表示结果为正数,发生正溢出
- s1s2 = 11,表示结果为负数,无溢出
- s1s2 = 10,表示结果为负数,发生负溢出
浮点数表示
通常,浮点数的表示格式为
r是浮点数阶码的底,E和M都是有符号的定点数,E称为阶码(整数),M称为尾数。格式如图:
阶符和阶码的位数共同反映了浮点数的表示范围及小数点的实际位置;数符代表浮点数的符号;尾数的位数反映了浮点数的精度。
规格化
为了提高运算的精度,需要充分利用尾数的有效位,通常采用浮点规格化形式,即规定尾数的最高位必须是一个有效值。
所谓规格化操作,就是指通过调整一个非规格化浮点数的尾数和阶码的大小,使非零浮点数在尾数的最高位上保证是一个有效值。
左规:当浮点数运算结果出现非规格化时,将尾数左移一位,阶码减1(r = 2 时)。可能需要多次左规。
右规:当浮点数的运算结果尾数出现溢出(双符号位为 01 或 10 )时,将尾数右移一位,阶码加1(r = 2 时)。只需要一次右规。
IEEE 754 标准
按照IEEE 754标准,浮点数格式如图:
根据国际标准IEEE 754,任意一个二进制浮点数真值N可以表示为:
- (-1)^s 表示符号位,s=0为正数,s=1为负数
- 1.M 表示有效数字,大于1小于2,fM表示尾数,尾数用原码表示
- 2^E 为指数位,E表示偏移位数。阶码用移码表示,阶码 = 指数值E + 偏置值
IEEE 754 标准规定常用浮点数共有短浮点数(单精度float)、长浮点数(双精度double)、临时浮点数(无隐含位)
浮点数格式 | 阶码 | 尾数 | 指数值E的范围 |
---|---|---|---|
短浮点数 | 8 | 23 | |
长浮点数 | 11 | 52 | |
临时浮点数 | 15 | 64 |
以短浮点数为例,最高位为数符位,其后8位是阶码,以2为底,用移码表示,阶码的偏置值为 2^(8 - 1) = 127 ;其后 23 位是原码表示的尾数数值位。
对于规格化的二进制浮点数,数值的最高位总是1(默认整数位始终是1,不会在机器数中体现),为了使尾数能够多一位有效位,将这个1隐含,所以尾数数值实际是24位。
示例
写出12的浮点表示(8位阶码)
(12)10 -> (1100)2 -> 1.1 * 2^3
其二进制形式:
写出12.5的浮点表示(8位阶码)
(12)10 -> (11001)2 -> 1.1001 * 2^3
其二进制形式:
特殊值
- denormalized
非规格化数。阶码全 0 时,不是有效数字,此时需要规格化操作。
- NaN
浮点数的异常表示,通常用于表示非法计算。Not-a-Number,非法数字。
是阶码全为1,尾数不全为0的二进制数族,即 >= 0x7f800000 的数。
0.0 / 0.0
结果为NaN
- Infinity
浮点数的异常表示,无穷大。是阶码全为1,尾数全为0的二进制数。
1.0 / 0.0
结果为Infinity (常数除以无穷小结果是无穷大)
浮点数的计算
步骤
浮点数的计算分为阶码运算和尾数运算两个步骤,加减运算一律采用补码。
对阶的目的是使小阶和大阶对齐,即让两个数的阶码相等。阶码小的尾数向右移阶差位。注意IEEE中隐含的1在计算中也是要带上的。
在对阶和右规的过程中,尾数右移时可能会丢失精度,需要做舍入操作,舍弃的一位为1时需要在最后一位加上1;
运算结果超出尾数所能表示的范围不会溢出,只有规格化后的阶码超出所能表示的范围是才会发生溢出;
示例
有两个数 X = 12 和 Y = 20 ,根据IEEE单精度浮点数规则,计算浮点数 X + Y。
# 计算规格化真值及其浮点的二进制形式
X = 12 = 1100 = 1.1 * 2^3
Y = 20 = 10100 = 1.01 * 2^4
X阶码:2^7 - 1 + 3 = 10000010
Y阶码:2^7 - 1 + 4 = 10000011
X = 0 10000010 (1) 10000000000000000000000
Y = 0 10000011 (1) 01000000000000000000000
# 对阶
可看出Y大于X,Y-X=1,此时X需向Y对齐,X阶码加1,同时X的尾数向右移1位,得:
X = 0 10000011 0 11000000...
Y = 0 10000011 1 01000000...
# 尾数求和
X,Y 都是正数,尾数补码即为原码
00 0 11000000...
+ 00 1 01000000...
-----------------------
01 0 00000000...
# 规格化
尾数溢出,右规,阶码加1;
此时的溢出是正常的的,不会发生溢出中断,只有阶码溢出才是异常溢出;
规格化后的尾数求和结果:00 1 0000000...
0 10000100 0000000...
指数E = 2^7 + 2^2 - (2^7 - 1) = 5
X + Y 的规格化真值 = 1.0 * 2^5 = 100000 = 32
可以发现这个例子阶码未溢出,且未发生舍入
浮点数的误差
整数化为二进制数不会产生误差;
小数化为二进制时可能会产生精度丢失的情况,由于二进制的特性,就像不断对折的折纸一样,只能表示1/(2^n)的倍数的小数。可见下表示例:
二进制 | 十进制小数 |
---|---|
0.00 | 0.0 |
1.00 | 1.0 |
0.10 | 0.5 |
0.01 | 0.25 |
0.11 | 0.75 |
0.111 | 0.775 |
类型转换
从窄范围到宽范围,比如从float到double的转换不会丢失精度;
编译器默认隐式转换从窄到宽,从宽到窄需显式声明;
结语
文章开始的几道题主要考察的点是浮点数为了表示更大的范围而选择丢失部分精度,以及浮点数的一些特殊值。看完以上浮点数的解释后,大家再回过头看题,不知是否恍然大悟呢。之前对浮点数了解比较模糊,经过这次深入学习,感觉相关点都比较通透。
下一篇: C语言入门(全)