浮点数的表示
从0.2+0.4不等于0.6说浮点数,浮点数我一直心存疑惑。
下面文章小数特指十进制数字,浮点数特指计算机存储的格式。
现代计算机的一般的浮点数都是遵循IEEE754标准。
首先我们将任何一个小数首先转换成下面的格式:
± 1.M * 2^e (这个1是二进制的1)
(其中,M叫做尾数,e叫做阶数的真值,IEEE754标准中,为了计算机比较阶的大小(都是正数),将E = e+127,存储的是E,而并不是e)
转换过程很简单,我们先将任何一个小数,转换成二进制,通过移动小数点,总能达到上面的格式。
下面举两个例子说明这种转换:
1. 分数
//分子分母拆成2的幂之和。
49/256 = (2^5 + 2^4 + 2^0)/2^8 = 2^-3 + 2^-4 + 2^-8
即0.00110001 = 1.10001 * 2^-3;
2. 小数
20.59375
整数部分和小数部分分别转换成二进制即可:
10100.10011 = 1.010010011 * 2^4
但是不是任何小数都可以转换成精确的二进制:
- 无穷小数(或者无法化成有限位数小数的分数)如π、1/3
- 某些有限的小数,如0.2、0.3等等很多
也就是说,大部分的小数其实不能精确表示的。
但当时我有两个困惑:
- 说浮点数表示范围是(2^104 - 2^128) ~ (2^128 - 2^104),范围这么大,怎么好多题目说一些整数(十进制的整数一定可以换成二进制的)都没办法精确表示呢
- java等一些语言中,输入0.3也没看见有问题呀,不是说0.3不能转换成精确二进制的吗,那么输入0.3,输出这个变量应该不是0.3才对呀
第一个问题
IEEE745 关于 float类型(32位),是这样定义的:
数符s(1位) + 阶码E(8位,使用移码,就是将文章开头的e+127 得到) + 尾数M(23位)
即:s 1.M * 2^E(式子中的1是二进制1,而且是隐含的,就是指我们实际上计算机中并不存这个1,但是我们将二进制浮点数还原成小数的时候,我们会加上这个1)
先说明两个概念的不同数的表示范围和数的精度。
- 数的表示范围。只是一个范围,并不保证范围内的每个数字都可以表示。浮点数可以表示的最大整数,或者最小整数,超过这个这个极限,都是无法表示的。
- 数的精度。是指数字的有效位数。精度听起来很陌生,有效位数大家一定会算,从左边第一个不为0的数字开始,到数字结束的长度就是有效位数。很多小数都没办法精确表示,所以我们研究浮点数的精度,一般是在讨论整数。如123456789 这个浮点数没办法精确表示。
这里的无法表示和上文的无法精确表示不是一个概念。
- 无法表示代表错误,超限。输出结果而输入差了数量级。
- 无法精确表示是表示不是很精确,但是差不多。
先说明浮点数精度的一个结论:
float尾数23位,所以能够表示的十进制数字(包括小数和整数)有效位数为7~8位。
当时看课本的时候我又懵逼了,这是怎么的出来的?
我们回过头来看这个浮点数表示格式:1.M * 2^e
而且我们明确一点,任何小数转换成浮点数都需要先转换成二进制。尾数23位,一般9位的十进制大约是10^9,化成二进制大约是2^28,需要27位表示尾数,所以超过浮点数的精度了。
所以我们才有下面两个结论:
- 数的精度是由尾数长度决定的
- 数的范围是由阶码长度决定的
同理,double 类型尾数52位,化成十进制有效位数大概是17位。
考研有的题目都是给出超过有效位数的十进制,问你是否可以精确表示,所以记住结论即可。
通过IEEE754标准,我们还可以获得浮点数的一些结论:
- E一共8位,即可以表0~255,但获取真正的指数值,需要减去127(不要问为什么不是128,计算机协会就是这么规定,我个人认为设置成128也没有什么问题),即表示范围为-127 ~ 128,但是额外规定了下面:
- E = 255(即e = 128) 配合 尾数全0,数符为1,为负无穷(即- 1.0000 2^128);数符为0,表示正无穷(即 1.0000 2^128)。
- E = 0(e = -127)配合 尾数全0,表示0(即 ± 1.000 * 2^-127)(因为隐藏位的原因,所以必须要定义实数0)
- 所以浮点数的指数真实表示范围为-126 ~ 127。即表示的最大正数为2^128 - 2^104(1.1111(23个1)…… * 2^127)。最小负数为2^104 - 2^128
第二个问题
为什么java下面这段代码是正确的呢?
System.out.println(0.2+0.4); //0.6000000000000001
System.out.println(0.4);//0.4
第一行输出我们应该可以理解了,因为0.2 和 0.4 都不能精确表示,这样计算仍然会丢失精度。
第二行却能正确输出,原因是只是这个浮点数不参加运算,高级语言内部会有一个修正,确保能够精确显示,但不能精确运算。(java 可以使用bigdecimal 精确运算小数。)
最后指出浮点数表示个数和浮点数表示范围也不是一个概念。使用32位,但是实数0有两种表示方法,即浮点数表示格式为2^32 -1,但是表示范围却很大。因为小数是无限的,浮点数用二进制存储携带的信息量个数一定是有限的。
字节序
即分为两种,大端存储和小端存储。
我们假设内存一个地址的存储单元是8位,即存储两位16进制数字。
如:0x12345678
首先我们确定哪边是高位,哪边是低位。很明显个位是低位,十位、百位依次位置更高。
所以存储方式根据命名就可以知道。
小端存储就是把低位存到内存地址小的单元中。
大端存储就是把低位存到内存地址大的单元中。
字节对齐
首先要理解字节对齐是什么意思。就是我们值存到计算机中,并不是一个挨着一个的。不一定是存完了a 变量,我们把b变量放在相邻的位置。
而字节对齐讨论对象是结构体,即结构体内部元素在内存中排列方式问题。
掌握下面三条规则,即可掌握结构体元素的内存排列。
(int 4字节,float 4字节,double 8字节,char 1字节,long 8字节,short 2字节)
- 结构体成员是普通变量,存储的起始地址必须是该变量大小的整数倍
- 结构体成员是另一个结构体或者数组等之类的集合。该结构体/集合存储的起始地址是内部最大的变量大小的整数倍
- 最后,整个结构的内存单元大小必须是内部最大成员(包括普通变量或者是结构体成员)大小的整数倍,不足需要补齐。