《Java从入门到失业》第三章:基础语法及基本程序结构(五):基本算数运算符(2)
- 2020 年 8 月 30 日
- 筆記
- 《Java从入门到失业》, Java从入门到失业
3.7.1.4浮点数运算
要讨论浮点数运算,牵涉到的知识比较多,下面一点一点的来逐步展开。为了便于同时讨论十进制和二进制数,我们做一个约定,我们把十进制数简写为N10,把二进制数简写为N2。
3.7.1.4.1小数的二进制
前面我们学过了二进制,但是都是局限于整数,那么小数的二进制是怎么样的?其实二进制的小数和十进制类似,例如1101.01012。那么这个二进制转化为十进制是多少呢?还记得二进制转十进制的公式吗?
N=an*2n-1 + an-1*2n-2 + … + a1*20
对于小数部分,我们可以继续拓展一下,变成:
N=an*2n-1 + an-1*2n-2 + … + a1*20 + b1*2-1 + b2*2-2 + … + bm*2-m
b表示小数点后面的数字。那么:
1101.01012=23 + 22 + 20 + 2-2 + 2-4 = 8 + 4 + 1 +0.25 + 0.0625 = 13.312510
好了,到此我们已经知道了整数和小数的二进制表示以及转化为十进制的方法。那么对于一个十进制数,怎么转化为二进制呢?我们先看十进制整数的转化。对于一个十进制整数,我们知道公式:
N10=an*2n-1 + an-1*2n-2 + … + a1*20
我们把N10不断的除以2,即
N10/2=(an*2n-1 + an-1*2n-2 + … + a1*20)/2= an*2n-2 + an-1*2n-3 + … + a2*20,余a1
N10/4= an*2n-3 + an-1*2n-4 + … + a3*20,余a2
……
最终,会余an 。然后我们把每一次的余数从右往左排列,anan-1……a1,其实就是对应的二进制数。因此对于一个十进制整数,我们只需要不断的除以2,把余数记录下来,然后从右往左排列,即为二进制。我们称之为“除2取余,逆序排列法”,例如对于1310
13/2,商6,余1
6/2,商3,余0
3/2,商1,余1
1/2,商0,余1
所以1310=11012
对于十进制小数,公式为:
M10= b1*2-1 + b2*2-2 + … + bm*2-m
由于指数是负数,所以我们采用不断乘以2(整数部分不乘),得到下面序列:
b1+ b2*2-1 + … + bm*2-(m-1),b1是一个整数
b2+ … + bm*2-(m-2) ,b2是一个整数
……
bm
我们把b1到bm排列起来,就是对应的二进制小数部分。,举例:
0.3125*2=0.625,整数部分是0
0.625*2=1.25,整数部分是1
0.25*2=0.5,整数部分是0
0.5*2=1,整数部分是1
因此0.312510=0.01012
3.7.1.4.2科学记数法
我们知道,把一个十进制数的用科学记数法(scientific notation)可以表示为a*10n或者aEn,其中0<=|a|<10,n是自然数。例如:
118.0625=1.180625*102=1.180625E2
0.0375=3.75*10-2=3.75E-2
对于二进制,我们同样可以采用类似的科学记数法,只不过把10换成2,例如:
0.00101=1.01*2-3
我们可以把科学记数法看成由3个部分组成:符号部分、有效数字部分、指数部分,示意图如下:
3.7.1.4.2IEEE754标准
我们在讨论浮点型的时候,提到过float和double的运算都遵循IEEE754标准,当时大家肯定想知道,什么是IEEE754标准。这里不打算对这个标准做全盘讲解,只尝试通过通俗的语言和简单的示例,让大家理解和明白浮点数时如何存储的,浮点数是如何计算的。
先来讨论一下浮点数是如何存储的。IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。只有32位模式有强制要求,其他都是选择性的。Java中使用了前2种。在内存中,是采用科学计数法存放的,分别对应科学计数法的3个部分:符号部分、有效数字部分和指数部分,具体规定为:
float:符号(1bit)、指数(8bit)、有效数字(23bit)
double:符号(1bit)、指数(11bit)、有效数字(52bit)
其中符号位0代表正,1代表负
示意图如下:
下面我们以单精度浮点数0.15625讲解浮点数的存储过程:
0.15625转化为二进制就是0.00101,然后将该数写成科学计数法:
0.15625 =
0.00101 = 1.01 * 2-3
有效数字部分是1.01,指数部分是-3。接下来就是把1.01和-3变成二进制,存放到对应的位置就可以了。但是这里有2个问题:
- 对于二进制来说,有效数字部分的整数部分只能是1,是不是可以不用存?
- 指数部分是-3,这是十进制数,前面我们学习过用补码表示负数,这里也用补码吗?
对于第1个问题,IEEE754标准规定,整数部分的的1不用存(读取的时候再补上,相当于有效数部分左边有一个隐藏位,值为1),这样可以节省一个bit的空间。因此只需要存放.01中的01即可,变成二进制为:010
0000 0000 0000 0000 0000
对于第2个问题,采用补码存放,理论上没有问题,但是IEEE754标准采用了另一种方法。我们知道float采用8位存放指数,范围是0~255,但是因为指数是有正负的,因此规定一个偏移量127,真正的指数值等于存放的值减去这个偏移量。这样一来,指数的范围就变成-127~128了。double采用11位存放指数,范围是0~2048,偏移量是1023,指数范围是-1023~1024。
我们用float存放,因此例子中的指数-3,存放的时候需要加上偏移量,变成124,二进制为(补足8位):0111 1100。整个存放的示意图如下:
接下来,我们再聊一聊IEEE754种对于2种特殊值的规定:非数值NaN和无穷大infinity。
前面我们介绍过,正负浮点数除以0得到正负无穷大,浮点数0除以0,会得到一个NaN。那么无穷大和NaN是怎么表示的呢?IEEE754标准规定如下:
无穷大(infinity):
- 符号位0表示正无穷大,1表示负无穷大
- 偏移指数位全为1
- 有效数位全为0
NaN:
- 符号位0或1均可
- 偏移指数位全为1
- 有效数位只要不全为0(全为0表示无穷大)
前面我们说过有效数部分左边有一个隐藏位,默认为1,那么问题来了,浮点数0怎么存储?因此IEEE754对0做了特殊规定:
零(Zero):
- 符号位0或1均可
- 偏移指数位全为0
- 有效数全为0
前面我们说过有效数部分左边有一个隐藏位,默认为1。但其实并非如此,IEEE754规定了一种特殊情况:
非正规数(Denormalized Number)
- 符号位0或1均可
- 偏移指数位全为0
- 有效数位不全为0(全为0表示0)
对于非正规数,有效数位的隐藏位视为0,并且偏移指数的是单精度是-12610(并非-127),双精度是-1022(并非-1023)
我们可以看到,当偏移指数全为0或全为1的时候,都是特殊情况。
下面我们来研究一下精度问题,我们先看下0.3的存储情况:
0.3*2=0.6,整数部分是0
0.6*2=1.2,整数部分是1
0.2*2=0.4,整数部分是0
0.4*2=0.8,整数部分是0
0.8*2=1.6,整数部分是1
0.6*2=1.2,整数部分是1
因此0.310 = 0.0 1001 1001 1001 1001
1001 1001 1001……2,科学计数法为:
0.310 = 1.001 1001 1001 1001
1001 1001 1001…… * 2-2(红色表示小数点后第24位),单精度有效数只有23位,需要舍入(第24位是1,后面不全为0,需要进1,具体舍入规则这里不解释了)为:1.001 1001 1001 1001 1001
1010 * 2-2,偏移指数为-2+127=125,二进制为01111101,因此单精度存储为:
0 01111101 00110011001100110011010
再还原成十进制为0.30000001192092896,因此浮点数存在精度问题。因此对于精度要求比较高的计算,不推荐使用浮点数进行运算。