跳转到内容

经典的面试题:为什么 0.1 + 0.2 ≠ 0.3?

编程界很经典的一个问题:为什么0.1 + 0.2 ≠ 0.3?面试中突然被问起,一时之间却不知道怎么回答。只记得与二进制有关,但具体是怎么来着,却有点说不清楚了。

后来查找了相关资料,才知道0.1 + 0.2 ≠ 0.3的来龙去脉。

简单来说:

  • 0.1的二进制是:0.0001100110011...(无限循环0011
  • 0.2的二进制是:0.001100110011...(无限循环0011

由于计算机的存储空间有限,所以两个无限循环的二进制必须被截断存储,从而使两个数丢失了原本的精度。

  • 0.1实际存储的值是0.1000000000000000055511151231257827021181583404541015625
  • 0.2实际存储的值是0.200000000000000011102230246251565404236316680908203125

两数相加:

0.10000000000000000555...
+
0.20000000000000001110...
=
0.3000000000000000444...

其结果被浮点精度截断后就是:0.30000000000000004

到这里,基本上就可以讲清楚0.1 + 0.2 ≠ 0.3的问题了。

如果还要进一步深挖,你可能还有很多疑问:

  • 小数的二进制是如何转换的?
  • 计算机是如何存储小数的?
  • 计算机是如何处理小数相加的?

小数的二进制转换

我们知道,整数的二进制表示:b * 2^(n - 1)b表示二进制位的系数,n表示第几位。比如十进制6可以表示:

6 = 1 * 2^2 + 1 * 2^1 + 0 * 2^0
  = 110

同理,小数部分的二进制表示方法类似:b * 2^(-n)。比如:0.25可以这样表示:

0.25 = 0 * 2^-1 + 1 * 2^-2
     = 0.01

对应的二进制就是:0.01

计算小数部分的二进制方法可以使用乘2取整法

举例:0.625的二进制:

  1. 0.625 * 2 = 1.25,整数1,小数0.25
  2. 0.25 * 2 = 0.5,整数0,小数0.5
  3. 0.5 * 2 = 1,整数1,小数0,结束。

顺序排列,就可以得到:0.101

相同的方法就可以计算出0.10.2的二进制,有兴趣的话可以自行验证。

计算机如何存储小数

计算机通常都是使用IEEE 754 标准的双精度浮点数进行小数的存储,也是就64bit

存储结构:

1bit    符号位
11bit   指数
52bit   尾数

实际能存储的小数的二进制尾数只有52位,超出后就会被截断。

所以,一个小数在计算机中其实是通过指数表示法来存储的,比如:0.625,对应的二进制是0.101。指数表示就是:

-1^s * 1.01 * 2^-1
  • s是符号位,01,上例中s = 0
  • 01存储在尾数部分,1.不需要存储,默认隐含;
  • -1需要加上1023后,以二进制形式存储在指数部分。

小数的加法计算

基于小数的存储结构,做加法运算时,必须先统一指数(也就是对阶),然后再进行二进制加法运算。

我们再来梳理一下0.1 + 0.2的计算过程:

1. 0.1的二进制

0.1 = 0.0001100110011001100110011001100...
    # 实际有 52 位尾数,便于书写已省略,下同
    ≈ 1.1001100110011 × 2^-4

2. 0.2的二进制

0.2 = 0.0011001100110011001100110011...
    ≈ 1.1001100110011 × 2^-3

3. 对阶

0.1 ≈ 0.11001100110011 × 2^-3
0.2 ≈ 1.10011001100110 × 2^-3

4. 相加

 1.10011001100110
+0.11001100110011
-----------------
10.01100110011001

结果就是:

10.01100110011001 × 2^-3

5. 规格化

小数点左移 1 位

1.001100110011001 × 2^-2

最后转换成十进制后就是0.30000000000000004(注意:上面的数据不是完整的52位尾数,若要验证需要使用完整的52位尾数进行计算)。

最后

总结几个关键字:转二进制→无限循环→截断存储→丢失精度

好了,下次如果再被问到类似的题目,就可以扯一趴啦了。