在多数编程语言中,当你计算 0.1 + 0.2
的结果时,会发现并不是想象中的 0.3
,而是 0.30000000000000004
。这是为什么呢?
有人会回答是精度问题,那精度是怎样导致的呢?为什么编程语言不去处理这些问题呢?
正整数的二进制很好理解,1 是 1
,2 是 10
, 3 是 11
,如此类推。而小数的二进制是 0.1
代表十进制的 0.5,0.11
代表 (1/2 + 1/4) = 0.75,0.01
代表 (0 + 1/4) = 0.25,0.111
代表 (1/2 + 1/4 + 1/8) = 0.875,0.101
代表 (1/2 + 0 + 1/8) = 0.625,如此类推。
由此我们可以发现,在整数的世界里,二进制位数的增加意味着我们能表示的整数范围越来越大,且严丝合缝。而小数的二进制位数增加,带来的改变是跳跃性的。除非你要的小数是 2 的 -n 次方,不然你只能不断趋近这个值,而不能准确表达它。
那么,我们假设用 16 位来表示十进制的 0.1 呢,它是 0.0001100110011001
,而这个二进制小数的真实值其实是 0.0999908447265625
。0.2 类似,它的16 位二进制是 0.0011001100110011
,它的真实值是 0.1999969482421875
。而它俩相加值自然其实是 0.29998779296875
了。
因此在计算机中 0.1 + 0.2
为什么不等于 0.3
就很好理解了。至于为什么上面的结果和开头的 0.30000000000000004
不一致,只是精度造成的,我举例里只用了 16 位,而比如 Java 的 double 是 53 位。
那么有人会想了,小数部分如果能设计成十进制,是不是所有问题都解决了呢?并不是,比如试试用十进制表示 1/3,再用它乘 3,一样会有类似的问题。
当然,如果要模拟十进制的效果实现精准计算,各种编程语言也提供了类似的方法,比如 Java 的 BigDecimal
类。那为啥编程语言不让它成为默认的方式呢,自然是因为二进制更契合计算机硬件,运算更快。
本文为即兴科普文,没有详细说明每个知识点。比如读者可能会发现不同的编程语言运算结果出奇地一致,这是因为它们遵循了 IEEE 754 标准。
再比如 Java 的 double 存储长度其实是 64 位,为什么精度只有 53 位呢?也是因为该标准定义的是 1 位表示符号、52 位表示尾数、11 位表示指数。不过这样算是 52 位啊,为什么精度是 53 位呢,是因为该浮点数格式首位恒为 1,为了空间利用率没有将其存储,就多了一位可以用于表示精度,这个概念叫次正规数。具体就不展开谈了。
参考资料:《Java编程的逻辑》。