在 JavaScript 控制台里执行 0.1+0.2,返回 0.30000000000000004,再和 0.3 比相等,结果是 false。Python、Java、C 全都一样。这不是哪门语言的低级 bug,而是进位制的几何事实:0.1 写成二进制是无限循环小数,正如十进制里 1/3=0.333… 永远写不完——有限个比特存不下它,误差从写入的那一刻就已注定。
手工把 0.1 转成二进制
十进制小数转二进制的方法是一路「乘 2 取整」:0.1→0.2 取 0;0.2→0.4 取 0;0.4→0.8 取 0;0.8→1.6 取 1 剩 0.6;0.6→1.2 取 1 剩 0.2——又回到 0.2,从此开始死循环。所以 0.1 的二进制是 0.0001100110011…,「0011」无限串下去。二进制能精确表示的分数,分母必须是 2 的幂:1/2、1/4、3/8 都干净利落;1/10 的分母含因子 5,几位都除不尽。
52 位尾数:误差进场的那一刀
IEEE 754 双精度浮点数给尾数留的格子只有 52 位。0.1 规格化写成 1.100110011…×2⁻⁴,截取前 52 位并按舍入规则进位,实际存下的值是 0.1000000000000000055511151231257827——摊开 17 位十进制可以看到,它比 0.1 大了约 5.55×10⁻¹⁸。0.2 是 0.1 的二倍,指数加一、同一副尾数,同样偏大。而 0.3 是另一个故事:它的二进制 0.0100110011… 独立舍入,存成 0.2999999999999999888977697537484346,比 0.3 偏小。三个数各自错向不同方向。
为什么加法把误差暴露出来
0.1+0.2 在浮点世界里算出的结果落在 0.3000000000000000444…,而字面量 0.3 存入的是 0.2999999999999999888…,两者是相邻的两条浮点刻度线,间距 1 个 ULP,约 5.55×10⁻¹⁷。判等要求比特级完全一致,自然不相等。另一个更能说明「误差有正有负」的例子:把 0.1 连加 10 次,结果是 0.9999999999999999,比 1 低一格。误差会随计算链路累积、放大,在财务对账、气象积分、梯度下降这类长循环里,微小偏差完全可以长成年报级别的问题。换句话说,误差不是在加法那一刻产生的,而是在把 0.1 写进格子的瞬间就已埋下,后续每一步只是把它放大给人看。
什么时候它是准的
分母是 2 的幂的数字永不捣乱:0.5+0.25=0.75 在任何语言都精确成立,因为它们换到二进制都是有限小数。要根治,思路都是绕开二进制分数:金额用最小单位存整数(1 元记成 100 分,0.1 元的误差就地消失);比较改成容差判断,|a−b| 小于 Number.EPSILON(约 2.22×10⁻¹⁶)即视为相等;Java 的 BigDecimal、SQL 的 DECIMAL 走的是放弃浮点、按十进制字符串重做竖式的路线。
用进制转换眼见为实
症结一句话:同一个数在不同进制下有的能写尽、有的写不尽。打开 进制转换工具,把 0.1 转到 2 进制,能看到循环节被截断的尾迹;换 0.5 再转,得到利落的 0.1。两个并排一放,谁带误差、谁不带,一目了然。0.1+0.2≠0.3 与其说是计算机算错了,不如说是它老实演示了一遍:有限的格子装不下无限的循环。