剖析 0.1 + 0.2 不等于 0.3 的原因

这是一个在程序界很著名的问题,几年前看过一遍但是记忆有点模糊了,今天翻出来再复习一下

提示: 本文所有的计算都以 Javascript 语言为例,其他语言也可参考

问题的抛出

> 0.1 + 0.2
> 0.30000000000000004

造成这个问题原因跟计算机存储浮点数的方式有关

十进制转二进制

计算机在存储十进制数之前需要转为二进制,举例:

十进制值 进制 按位格式 描述
13 10 13 1x10^1 + 3x10^0 = 10 + 3
13 2 1101 1x2^3 + 1x2^2 + 0x2^1 + 1x2^0 = 8 + 4 + 0 + 1

上面是整数的表示,小数表示很类似

十进制值 进制 按位格式 描述
0.625 10 0.625 6x10^-1 + 2x10^-2 + 5x10^-3 = 0.6 + 0.02 + 0.005
0.625 2 0.101 1x2^-1 + 0x2^-2 + 1x2^-3 = 1/2 + 0 + 1/8

十进制整数转二进制方法:除 2 取余
十进制小数转二进制方法:乘 2 取整

例如 10 的二进制表示为:1010

10 / 2 => 商 50
5 / 2 => 商 21
2 / 2 => 商 10
1 / 2 => 商 01

再例如 1.8125 转为二进制小数为 1.1101

整数部分为1 转为二进制为 1
0.8125 x 2 1.625 取 1
0.625 x 2 1.25 取 1
0.25 x 2 0.5 取 0
0.5 x 2 1.0 取 1

二进制表示 0.1 和 0.2

十进制 0.1 转换成二进制,乘 2 取整过程:

0.1 * 2 = 0.2 # 0
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.2 * 2 = 0.4 # 0
......

从上面可以看出,0.1 的二进制格式是:0.000110011...,这是一个二进制无限循环小数,但计算机内存有限,不能储存所有的小数位数,因此会在某个位置截掉,进行 0 舍 1 入

同样的,0.2 用二进制表示为 0.00110011...,也是一个二进制无限循环小数

Javascript 中浮点数的存储

Javascript 中 Number 类型严格按照 IEEE754 标准来定义,以 64 位来存储浮点数

对于 64 位的浮点数,最高的 1 位是符号位 S,接着的 11 位是指数 E,剩下的 52 位为有效数字 M

以 0.1 和 0.2 为例,计算机再存储这两个数之前,先将其转换为二进制

// 0.1
0.0001100110011001100110011001100110011001100110011001...
// 0.2
0.0011001100110011001100110011001100110011001100110011...

然后进行规格化,即用科学计数法表示

// 0.1
1.1001100110011001100110011001100110011001100110011001...*2^-4
// 0.2:
1.1001100110011001100110011001100110011001100110011001...*2^-3

但是,JS 中最多存储小数点后 52 位,因此存储的数字实际上是(小数点后 52 位)

// 0.1
1.1001100110011001100110011001100110011001100110011010*2^-4
// 0.2:
1.1001100110011001100110011001100110011001100110011010*2^-3

提示:
1.小数点前的 1 是不用存的,因为除了 0 以外,其它数值规格化以后第一位都是 1
2.原本小数点最后两位是 01,但根据 0 舍 1 入的规则,就变成了 10

因此,0.1 在计算机中的存储就是把指数 -4 存在上图对应的 exponent 中,而 1001100110011001100110011001100110011001100110011010 则存在上图对应的 fraction 中,0.2 同理,由于 0.1 和 0.2 都是正数,所以符号位(sign)都是 0

0.1 + 0.2

根据上文,0.1 + 0.2 的实际计算过程如下(后面多出的 0 舍掉)

> 0.0001100110011001100110011001100110011001100110011001101

+ 0.0011001100110011001100110011001100110011001100110011010

= 0.0100110011001100110011001100110011001100110011001100111

= 1.00110011001100110011001100110011001100110011001100111*2^-2

将规格化后的结果进行 0 舍 1 入,小数点后保留 52 位最终得到

1.0011001100110011001100110011001100110011001100110100*2^-2
// 舍掉后面的 0 就是
1.00110011001100110011001100110011001100110011001101*2^-2
// 即
0.0100110011001100110011001100110011001100110011001101

其实上面得到的最终结果转换成十进制就是 0.30000000000000004

(0.30000000000000004).toString(2) // 转换为二进制
// 0.0100110011001100110011001100110011001100110011001101

这就是为什么在编程中 0.1 + 0.2 不等于 0.3 的原因了,它跟计算机存储浮点数的方式有密切关系

平时写程序中,我们一般会把浮点数转换成整数再进行计算,不然会产生很大误差,这也是为什么银行一般都会用分为单位进行计算而不是用角或者元的原因了

除特殊说明外本人博客均属原创,转载请注明出处:http://blog.johnhan.cn/blog_1025.html
鄂ICP备17018604号-1  鄂公网安备42060702000030号