十进制与二进制:为什么浮点数不准?

July 24, 2017

写代码的时候经常会发现计算机应该返回0.3,返回的却是0.30000011241,这是因为二进制转换为十进制时可能会有偏差。

计算机是用开关的开和闭来表示1和0,因此计算机只能「计算」二进制。关于这部分更详细的介绍,例如图灵机、逻辑电路等内容,可以看《编码:隐匿在计算机软硬件背后的语言》一书,也可以看李戈老师精彩的公开课《计算概论 A》的前三节,CS50 有更简短的介绍,本文的截图就来自 CS50。

与计算机不同,人类是用十进制思考的,十进制,顾名思义,每到十就进一位,我们从0数到9,然后十位进一位得1,各位变成0,也就是10。

这里我有个不太理解的地方,从直觉的角度,人有十根手指头,应当从「一根」数到「十根」,每一根手指都应当有标准的符号,为什么第十根就变成10了呢?汉字用「一二三四五六七八九十」反而更自然吧。这里我没有更深入的查证,可能是因为古埃及和古巴比伦很早就有了高级的数字表示法,有兴趣的可以一起讨论。

十进制下,145这样的数字,代表 1 × 100 + 4 × 10 + 5 × 1。二进制下,1011 代表 1 × 8 + 0 × 4 + 1 × 2 + 1 × 1。

十进制如何转换成二进制呢?一种思路是想成搭积木,如何用1、2、4、8、16……这些2的幂指数组合成十进制的数。比如42这个数,这是个偶数,所以我不用1,我肯定要用上32,然后我再找到2和8就够了,4和16都不要,所以二进制是101010。另一种思路是用算法,42 ÷ 2 取余数 0 得 21,21 ÷ 2 取余数 1 得 10,10 ÷ 2 取余数 0 得 5,5 ÷ 2 取余数 1 得 2,2 ÷ 2 取余数 0 得 1,1 ÷ 2 取余数 1 得 0。把这些余数从后到前,101010 即为二进制转换的结果。

本质上,第一种方法和第二种方法是相同的,通过余数判断这个数有没有1,再判断有没有2、4、8…… 如果有余数,就代表取1,没有就代表取0。

如果这个数有小数部分,就有点棘手了,以 1.25 为例,我先要让这个数变成整数,先乘上 2^3,1.25 × 2^3 = 10。十进制的10转换成二进制是1010,因为我之前已经让这个数乘上 2^3 次方了,所以我要除掉 2^3,在十进制里,我们除1000就是退三位,比如2274 ÷ 1000 = 2.274,在二进制里,除 2^3 就是退三位,1010 退三位是 1.010 也就是 1.01。

在转换的时候,我们会要求计算机把这个小数不断地 × 2,直到变成整数,但有些数字,比如 0.225,不论你乘上多少2的倍数,都没法变成整数,于是计算机只能近似地用二进制表示这些数,计算完毕后再转换成十进制出现在你的屏幕上,这个转换必然是有误差的,这就是出现 0.30000011241 这样的浮点数的原因。

所以在写代码的时候,a 和 b 分别是两个浮点数,不要出现 if a == b这样的判断,因为 a 和 b 经过计算后会有偏差,应该写成 if a - b <= 0.00001