17为什么0.1 + 0.2 不等于 0.3
Posted 天界程序员
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了17为什么0.1 + 0.2 不等于 0.3相关的知识,希望对你有一定的参考价值。
计算机使用二进制存储数据
- 整数转换二进制没有误差,如9转换为二进制是1001
- 而小数可能无法用二进制准确表达,如0.2转换为1.1001100…
- (不光JS,其他编程语言也都一样)
建议使用第三方库npm:mathjs
在浏览器控制台里面输入:0.1 + 0.2
你得到的结果是:0.30000000000000004
, 而不是 0.3
0.1+0.2的计算过程
1.十进制转成二进制
在JS内部所有的计算都是以二进制方式计算的。 所以运算 0.1+ 0.2 时要先把 0.1和 0.2 从十进制转成二进制。
关于十进制转二进制,整数和小数的转化方法不一样:
- 整数:采用【除2取余,逆序排列】
具体做法是:用2整除十进制整数,可以得到一个商和余数;再用2去除商,又会得到一个商和余数,如此进行,直到商为小于1时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。
- 小数:采用【乘2取整,顺序排列】
具体做法是:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,此时0或1为二进制的最后一位。或者达到所要求的精度为止。
0.1转化成二进制的算法:
0.1*2=0.2======取出整数部分0 ------ 余0.2
0.2*2=0.4======取出整数部分0 ------ 余0.4
0.4*2=0.8======取出整数部分0 ------ 余0.8
0.8*2=1.6======取出整数部分1 ------ 余0.6
0.6*2=1.2======取出整数部分1 ------ 余0.2
接下来会无限循环
0.2*2=0.4======取出整数部分0 ------ 余0.4
0.4*2=0.8======取出整数部分0 ------ 余0.8
0.8*2=1.6======取出整数部分1 ------ 余0.6
0.6*2=1.2======取出整数部分1 ------ 余0.2
所以0.1转化成二进制是:0.0001 1001 1001 1001......
0.2转化成二进制的算法:
0.2*2=0.4======取出整数部分0 ------ 余0.4
0.4*2=0.8======取出整数部分0 ------ 余0.8
0.8*2=1.6======取出整数部分1 ------ 余0.6
0.6*2=1.2======取出整数部分1 ------ 余0.2
接下来会无限循环
0.2*2=0.4======取出整数部分0 ------ 余0.4
0.4*2=0.8======取出整数部分0 ------ 余0.8
0.8*2=1.6======取出整数部分1 ------ 余0.6
0.6*2=1.2======取出整数部分1 ------ 余0.2
所以0.2转化成二进制是:0.0011 0011 0011 0011......
这里要注意 0.1 和 0.2 转成的二进制是无穷的。另外在现代浏览器中是用浮点数形式的二进制来存储二进制,所以还要把上面所转化的二进制转成浮点数形式的二进制。
2.转成浮点数
浮点数是与定点数相对的概念,计算机中的定点数约定小数点的位置不变,即人为约定俗成地规定了一个数小数点的位置。例如定点纯整数约定了小数点在数值位的最后。定点纯小数约定了数值位的最高位在小数点后面。
由于计算机字长的限制,当需要表示的数据有很大的数值范围时,他们不能直接用定点小数或者定点整数表示
双精度浮点数用1位表示符号位,11位表示指数位,52位表示小数位,如下图所示:
- 符号位:正数为0,负数为1;
- 指数位:阶数+偏移量,阶数是: 2 e − 1 − 1 2^e-1 -1 2e−1−1
e
为阶码的位数。偏移量是把小数点移动到整数位只有1时移动的位数,正数表示向左移,负数表示向右移;
- 小数位:即二进制小数点后面的数。
接下来把0.1转成的二进制0.0001100110011001 …转成浮点数形式的二进制。
- 先要把小数点移动到整数位只有1,要向右移动4位,故偏移量为−4,通过指位数的计算公式
2 11 − 1 − 1 − 4 = 1019 2^11-1 -1-4 = 1019 211−1−1−4=1019
把1019转成二进制为1111111011
,不够11位要补零
,最终得出指位数为01111111011
;
- 小数位为100110011001… ,因为小数位只能保留52位,第53位为1故进1。
0.1
转换结果如下图所示:
同理,再把 0.2
转成的二进制0.0011 0011 0011 0011......
转成浮点数形式的二进制,转换结果如下图所示:
3.浮点数相加
浮点数相加时,需要先比较指位数是否一致,如果一致则小数位直接相加,如果不一致,要先把指位数调成一致的,指位数小的向大的调整。
为了行文方便,把0.1转成的浮点数称为为0.1,把0.2转成的浮点数称为0.2。
0.1的指数位是1019 ,0.2的指数位是1020 。故要把0.1的指数位加1,即把0.1的小数点向左移动1位,另外浮点数的整数位固定为1,过程如下所示
1.1001100110011001100110011001100110011001100110011010 原先
0.11001100110011001100110011001100110011001100110011010 移动后
0.1100110011001100110011001100110011001100110011001101 将小数的第53位舍去,因为为0故不需进1
现在0.1和0.2的指数位相同了,把小数位直接相加。
1100110011001100110011001100110011001100110011001101 0.1的小数位
+ 1001100110011001100110011001100110011001100110011010 0.2的小数位
= 10110011001100110011001100110011001100110011001100111
会发现现在的小数位多出了一位,超出了52位,故要把小数位最后一位截掉,小数位最后一位是1,故要进1,如下所示:
10110011001100110011001100110011001100110011001100111
1011001100110011001100110011001100110011001100110100
截掉小数位的最后一位相当把小数点向左移了一位,故指数位要加1,此时的指数是0.2的指数1021
,加1后变成1021
,转成二进制为01111111101
,那么相加后的浮点数如下所示:
4.浮点数转成十进制
二进制浮点数计算结束后,把结果(二进制的浮点数)转成十进制,其转换公式为
,s是符号位为0或1,e为浮点数指数位转成十进制的值,i表示小数位从左到右的位数,第一位 i=1 ,
表示每一位的值为0或1。
那么按着公式把二进制的浮点数转成十进制:
结果如下所示:
0.3000000000000000444089209850062616169452667236328125
由于精度问题,只取到0.30000000000000004
。
答案
0.1+0.2 不等于 0.3 ,因为在 0.1+0.2 的计算过程中发生了两次精度丢失。第一次是在 0.1 和 0.2 转成双精度二进制浮点数时,由于二进制浮点数的小数位只能存储52位,导致小数点后第53位的数要进行为1则进1为0则舍去的操作,从而造成一次精度丢失。第二次在 0.1 和 0.2 转成二进制浮点数后,二进制浮点数相加的过程中,小数位相加导致小数位多出了一位,又要让第53位的数进行为1则进1为0则舍去的操作,又造成一次精度丢失。最终导致 0.1+0.2 不等于0.3 。
拓展
0.1+0.2 不等于 0.3 会引起那些BUG ?
会引起统计页面展示错乱的BUG,还有 300.01 优惠300 元后,支付金额不足0.01 元等类似的BUG。
怎么解决 0.1+0.2 不等于 0.3 ?
可以用Math.js数学计算库来解决,或者用toFixed()给计算结果四舍五入,但是toFixed()在chrome或者火狐浏览器下四舍五入也有精度误差。可以用Math.round来解决精度误差,比如要把 2.55 四舍五入保留 1 位小数,先把 2.55∗10 得到 25.5 ,再用Math.round取整25.5 ,会得到25,再把 25÷10 得到 2.5 ,就这样间接实现了四舍五入。可以用Math.pow来做个简单的封装Math.round(Math.pow(10, m) * number) / Math.pow(10, m),其中number是要四舍五入的数,m是保留几位小数。
以上是关于17为什么0.1 + 0.2 不等于 0.3的主要内容,如果未能解决你的问题,请参考以下文章