$f = 19.58;
var_dump(intval($f * 100)); //为啥输出1957
上面是一段PHP代码,肉眼可见的1958为什么变成了1957,这就要从计算机是怎么保存浮点数开始说起了。计算机处理小数时要把我们的十进制转化为二进制,而在转化成二进制的过程中,就会不可避免的丧失精度。现代的编程语言基本都用IEEE754标准来定义浮点数的二进制格式,我们来看一下如何把19.58转化成符合 IEEE754 标准的二进制数。
首先IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。常用的就是单精度和双精度,C语言中float通常指32位单精度,double指64位双精度。而PHP中只有float一种表示浮点数的数据类型,float在32位平台中使用的就是单精度标准,在64位平台中使用的双精度标准。下面为了便于计算,我们用32位单精度来计算十进制19.58的值。
先看一下IEEE754中单精度的组成
符号位 | 指数位 | 小数位 |
1位 | 8位 | 23位 |
符号位和小数位很好理解,那么什么是指数位?计算机用类似科学计数法的方式表示小数,举例十进制中
, 那么1就是这里面的指数位
二进制也类似,比如二进制小数0.0000101就是 , 这里-5就是指数位, 在二进制小数的科学计数法中,我们这么表示小数:
(这里说一下为什么非要以1.xxx开头,这是为了节约小数位,如果不就浪费了4位小数位存0了么。并且如果所有的浮点数都是
这种格式,我们在存储中也可以忽略小数点前的1.这个整数位了,直接存小数位就可以了)
需要注意的是,浮点数指数位在计算机中不是以原码表示的,而是以移码表示的(有对原码,反码,补码不清楚的同学可以自行百度)。这里简单的说一下移码,比如8位二进制(最大正数为255)我们就设中间数127为0
10000001 (十进制129) 2
10000000 (十进制128) 1
01111111 (十进制127) 0
01111110 (十进制126) -1
01111101 (十进制125) -2
移码的移就是偏移量的意思(距离我们定义的0的距离)
讲完基本概念,我们来转化一下19.58这个十进制数
19 / 2 = 9 余 1
9 / 2 = 4 余 1
4 / 2 = 2 余 0
2 / 2 = 1 余 0
1 / 2 = 0 余 1
现在整数位确定了为10011
我们再转化小数位(小数转化规则:小数部分乘以2,取整数部分,直至乘积小数部分为0或超过小数位最大能表示的数):
0.58 * 2 = 1.16
0.16 * 2 = 0.32
0.32 * 2 = 0.64
0.64 * 2 = 1.28
0.28 * 2 = 0.56
0.56 * 2 = 1.12
0.12 * 2 = 0.24
0.24 * 2 = 0.48
0.48 * 2 = 0.96
0.96 * 2 = 1.92
......(就乘到这里吧,目测小数位不会等于0了,这也是损失精度的原因)
小数位从上到下就是1001010001
19.58二进制表示约等于10011.1001010001, 科学计数法就是
指数位127+4 = 131 = 10000011
符号位 | 指数位 | 小数位 |
0 | 10000011 | 00111001010001... |
为了证明以上不是我编的,我们验证一下
当然,他的精度要比我们高几位(我没有算完),但是前面几位是完全一样的,到了这里,我们就完整的用IEEE754表示了19.58这个浮点数了
那么假如我们要把它转回十进制呢?
10011.1001010001 整数位 = 19 , 小数位
误差由此而来,因为我们在转换小数位的时候不能通过*2彻底地把小数位变成0,所以精度会有偏差,原理类似