为什么0.1加0.2不一定等于0.3:浮点数精度原理与正确用法

举报
yd_232225224 发表于 2026/09/22 16:03:20 2026/09/22
【摘要】 在很多编程语言中执行下面的代码:print(0.1 + 0.2)得到的结果可能是:0.30000000000000004继续比较:print(0.1 + 0.2 == 0.3)结果可能是:False这并不是计算机连简单加法都算不对,也不是某种编程语言存在缺陷,而是浮点数表示方式带来的正常现象。计算机通常使用有限位二进制保存小数,而许多十进制小数无法被有限位二进制精确表示。程序只能保存一个非常...

在很多编程语言中执行下面的代码:

print(0.1 + 0.2)

得到的结果可能是:

0.30000000000000004

继续比较:

print(0.1 + 0.2 == 0.3)

结果可能是:

False

这并不是计算机连简单加法都算不对,也不是某种编程语言存在缺陷,而是浮点数表示方式带来的正常现象。

计算机通常使用有限位二进制保存小数,而许多十进制小数无法被有限位二进制精确表示。程序只能保存一个非常接近原值的近似数,计算后误差就可能显示出来。

一、十进制小数为什么能精确表示0.1

在十进制中:

0.1 = 1 ÷ 10

它只需要一位小数就能准确表示。

但不是所有分数都能用有限位十进制表示。例如:

1 ÷ 3 = 0.333333……

小数部分会无限循环。

一个最简分数能否在十进制中有限结束,取决于其分母是否只包含2和5这两类质因数。

例如:

1 ÷ 2 = 0.5
1 ÷ 4 = 0.25
1 ÷ 5 = 0.2
1 ÷ 10 = 0.1

这些分母最终都可以分解成2和5的乘积,因此能用有限位十进制表示。

而:

1 ÷ 3
1 ÷ 7

不能用有限位十进制精确表示。

二、二进制也有无法有限表示的小数

计算机通常使用二进制保存浮点数。

二进制小数每一位表示的是:

1/2、1/4、1/8、1/16……

例如:

0.1₂ = 1/2
0.01₂ = 1/4
0.001₂ = 1/8

十进制的0.5可以精确表示为:

0.1₂

十进制的0.25可以表示为:

0.01₂

十进制的0.125可以表示为:

0.001₂

但十进制0.1对应分数:

1/10

分母包含质因数5,而二进制有限小数只能精确表示分母为2的幂的分数。

因此,十进制0.1转换成二进制后会成为无限循环小数,只能截取或舍入到有限位:

0.00011001100110011……

这和十进制无法有限表示三分之一是同一种问题,只是使用的进位制不同。

三、计算机实际保存的并不是0.1

当程序写下:

value = 0.1

如果使用常见二进制浮点类型,计算机保存的通常不是数学意义上精确的0.1,而是最接近0.1的可表示值。

可以把它理解成:

希望保存:0.1
实际保存:0.100000000000000005……

同样,0.2和0.3也可能分别被保存成略大或略小的近似值。

执行加法时,计算机相加的是这些近似值,而不是纸面上的精确十进制数。

因此:

0.1的近似值
+ 0.2的近似值
= 0.30000000000000004附近

而单独写下的0.3又会被转换成另一个最接近0.3的浮点数。两边的最终位模式不完全相同,直接比较就可能得到不相等。

四、浮点数为什么叫“浮点”

定点数会预先约定小数点位置。

例如,使用整数保存金额,并规定最后两位表示小数:

1999 → 19.99元

小数点位置固定,所以称为定点。

浮点数则类似科学计数法:

1.2345 × 10³

数字由有效部分和指数共同表示,小数点可以随指数移动,因此称为浮点数。

在二进制中,可以表示为:

有效数字 × 2的指数次方

这种方式能够同时表示非常大和非常小的数,但只能保留有限数量的有效数字。

五、IEEE 754浮点数的基本结构

现代计算机通常采用IEEE 754浮点标准。

一个二进制浮点数主要由三部分组成:

符号位
指数
尾数或有效数字部分

对于常见的32位单精度浮点数:

1位符号
8位指数
23位小数部分

对于常见的64位双精度浮点数:

1位符号
11位指数
52位小数部分

由于规格化表示通常隐含一个最高有效位,双精度浮点数实际上拥有约53位二进制有效精度。

它大约能够提供15到16位十进制有效数字,但这并不意味着小数点后固定有15位可靠数字。

精度描述的是整体有效数字数量,而不是固定的小数位数。

六、float和double有什么区别

许多语言提供两种常见浮点类型。

float

通常是32位单精度浮点数。

它大约提供:

6到7位十进制有效数字

适合:

  • 图形渲染;
  • 对精度要求不高的传感器数据;
  • 大规模数组;
  • 需要节省内存的计算。

double

通常是64位双精度浮点数。

它大约提供:

15到16位十进制有效数字

适合:

  • 通用科学计算;
  • 统计分析;
  • 大多数工程计算;
  • 需要更高数值稳定性的场景。

double精度更高,但仍然是有限精度的二进制浮点数,依然不能精确表示0.1这类十进制小数。

七、显示正常不代表内部精确

程序执行:

print(0.1)

通常只显示:

0.1

这是因为输出函数会选择一种更适合人阅读的格式,并隐藏无意义的多余数字。

内部保存值仍然可能是:

0.100000000000000005……

如果使用更高精度格式输出,就可能看到近似值:

print(format(0.1, ".20f"))

可能显示:

0.10000000000000000555

因此:

显示为0.1

并不代表:

内存中保存的是数学上精确的0.1

格式化只能改变显示方式,不能提高已经存储的数据精度。

八、浮点数为什么不满足结合律

在数学中:

(a + b) + c = a + (b + c)

但浮点计算每一步都可能发生舍入,所以不同计算顺序可能得到不同结果。

假设:

a = 一个非常大的数
b = 一个较小的数
c = a的相反数

计算:

(a + b) + c

时,b可能因为相对于a太小而在第一次加法中被舍弃,结果接近0。

而计算:

a + (b + c)

时,中间值和舍入路径不同,结果可能保留b的一部分。

因此,在并行计算中,即使输入相同,只要求和顺序发生变化,最后几位也可能不同。

这也是为什么科学计算中不能总是假设不同硬件、线程数和执行顺序会产生完全一致的最后一位结果。

九、什么是灾难性消减

两个非常接近的浮点数相减时,前面的有效数字会大量抵消,剩余结果可能只保留原误差。

例如:

1.23456789
- 1.23456788

理论结果很小。

如果两个输入本身已经包含舍入误差,相减后,有效信息可能大幅减少。这种现象称为灾难性消减。

它常见于:

  • 数值微分;
  • 统计方差计算;
  • 求解方程;
  • 几何运算;
  • 信号处理;
  • 大数之间的小差值。

解决这类问题通常需要重新组织公式,而不只是换成更多小数位输出。

十、浮点误差会不会不断累积

会,但具体程度取决于算法。

例如,连续一百万次累加一个不能精确表示的小数,舍入误差可能逐步积累:

total = 0.0

for _ in range(1000000):
    total += 0.1

结果可能与理论值存在微小差异。

改善方法包括:

  • 使用更高精度类型;
  • 改变求和顺序;
  • 先累加绝对值较小的数;
  • 使用补偿求和算法;
  • 使用定点或十进制类型;
  • 减少不必要的重复运算。

误差是否可以接受,需要结合业务量级和允许偏差判断。

十一、为什么不能直接用等号比较浮点数

下面的比较可能失败:

if 0.1 + 0.2 == 0.3:
    print("相等")

因为两边的实际浮点表示可能不同。

更常见的方式是判断差值是否足够小:

a = 0.1 + 0.2
b = 0.3

if abs(a - b) < 1e-9:
    print("近似相等")

这里比较的不是绝对相等,而是:

两者误差是否处于业务允许范围内

十二、为什么只用固定绝对误差还不够

假设统一使用:

允许误差 = 0.000001

比较两个接近零的数时,这个误差可能太大;比较两个数十亿量级的数时,它又可能太小。

更加稳妥的方式是同时考虑绝对误差和相对误差:

|a - b| ≤ 最大值(
    绝对误差,
    相对误差 × max(|a|, |b|)
)

其中:

  • 绝对误差适合处理接近零的情况;
  • 相对误差根据数值规模调整容忍范围。

Python可以使用:

import math

math.isclose(
    a,
    b,
    rel_tol=1e-9,
    abs_tol=1e-12
)

容差不应随意复制,而应该根据数据范围、测量精度和业务要求确定。

十三、钱为什么不适合直接使用二进制浮点数

金额通常要求明确的小数位和舍入规则。

例如:

19.90元

如果用二进制浮点数保存,19.90可能只是一个近似值。大量金额相加、税费计算和折扣计算后,可能出现分币误差。

金融和账务系统通常采用以下方式。

方法一:使用最小货币单位的整数

例如,用“分”保存金额:

19.90元 → 1990分

加减运算全部使用整数:

1990 + 500 = 2490分

整数运算在范围允许时是精确的。

不过,需要提前明确:

  • 最小货币单位;
  • 是否存在比分更小的计算单位;
  • 汇率计算精度;
  • 税费中间值保留规则;
  • 最终舍入方式。

方法二:使用十进制定点类型

数据库中的DECIMAL以及编程语言中的高精度十进制类型,可以按照十进制规则保存和计算金额。

它们更符合人类账务习惯,但运算速度和存储成本可能高于硬件原生浮点数。

金额计算首先需要正确,其次才是追求微小性能差异。

十四、创建十进制数时也可能引入浮点误差

即使使用十进制类型,如果先经过二进制浮点数,误差也可能已经进入。

例如:

from decimal import Decimal

value = Decimal(0.1)

这里的0.1可能先被解释为二进制浮点数,再转换成十进制对象,因此可能保留原来的近似值。

更合适的方式是从字符串创建:

value = Decimal("0.1")

字符串准确表达了十进制文本“0.1”,不会先经过二进制浮点转换。

十五、数据库中的FLOAT和DECIMAL有什么区别

FLOAT或REAL类类型

通常使用二进制浮点表示。

适合:

  • 科学测量;
  • 统计数据;
  • 允许近似的数据;
  • 数值范围很大的数据。

DECIMAL或NUMERIC类类型

通常按照指定十进制精度保存。

例如:

DECIMAL(10, 2)

可以用于最多10位有效数字,其中2位位于小数点后。

适合:

  • 金额;
  • 税率;
  • 账务数据;
  • 需要精确十进制舍入的数据。

不要因为字段中“有小数”,就默认使用浮点类型。应该先判断业务需要近似计算还是精确十进制计算。

十六、整数也有精度上限

使用浮点数保存整数时,同样存在限制。

双精度浮点数能够精确表示一定范围内的所有整数,但超过某个范围后,相邻整数之间可能没有对应的浮点值。

对于常见双精度浮点数,整数在绝对值不超过:

2⁵³

时通常可以精确表示。

超过这个范围后,可能出现:

一个整数

和:

下一个整数

被转换成同一个浮点值。

因此,大型整数编号、时间戳、订单号和数据库主键不应随意转换为浮点数。

一些使用双精度浮点数表示普通数字的运行环境,在处理超大整数编号时尤其需要注意。可以使用字符串或专门的大整数类型传递。

十七、NaN是什么

浮点标准还定义了一种特殊值:

NaN

表示“不是一个数”。

它可能来自:

  • 无效数学运算;
  • 未定义结果;
  • 缺失或异常数值;
  • 某些计算函数失败。

NaN具有一个反直觉特性:

NaN通常不等于它自己

因此,不能总是使用:

value == value

之外的普通相等逻辑判断NaN。更清晰的方式是使用语言提供的专门检测函数。

NaN进入统计数据后,可能让整个计算结果也变成NaN,因此需要在数据清洗阶段明确处理。

十八、正无穷和负无穷

浮点数还可以表示:

正无穷
负无穷

它们可能由数值溢出或某些除法产生。

无穷不一定立即导致程序崩溃,反而可能继续参与后续计算,使错误传播得更远。

对于关键计算,应检查结果是否为有限数,而不能只判断程序有没有抛出异常。

十九、负零是什么

浮点数通常区分:

+0
-0

它们在大多数相等比较中被视为相等,但在某些运算中可能产生不同结果。

例如,它们可能携带数值从正方向还是负方向趋近于零的信息。

普通业务程序很少需要主动处理负零,但在数值分析、复数计算和底层数学库中,它可能具有意义。

二十、舍入方式为什么重要

现实业务中经常需要将结果保留到固定小数位,例如金额保留两位。

但“保留两位”仍然需要说明具体舍入规则:

  • 四舍五入;
  • 向零截断;
  • 向上取整;
  • 向下取整;
  • 五舍六入;
  • 遇到正中间值时取偶数;
  • 针对正负数采用不同规则。

不同语言的默认round函数可能采用不同策略,或者受到二进制近似值影响。

例如,看似正好处于中间位置的十进制数,在浮点表示中可能略高或略低于中点,因此结果与人工直觉不同。

财务计算必须明确舍入规则,并在所有系统中保持一致。

二十一、传感器数据是否需要精确相等

测量数据本身通常包含:

  • 传感器误差;
  • ADC量化误差;
  • 温度漂移;
  • 电源噪声;
  • 标定误差;
  • 浮点计算误差。

因此,判断电压是否等于某个值时,不应写成:

if (voltage == 5.0) {
    // ...
}

更合理的是根据硬件精度设置容差范围:

if (voltage >= 4.95 && voltage <= 5.05) {
    // 认为处于5V允许范围
}

这里的容差应该来自:

  • 规格要求;
  • 测量系统精度;
  • 元器件误差;
  • 控制系统稳定性;
  • 安全余量。

浮点误差通常只是整体误差预算中的一部分。

二十二、控制系统中为什么需要滞回

如果控制逻辑以单一阈值判断:

电压高于10V:打开
电压低于10V:关闭

当测量值在10V附近波动时,状态可能频繁切换:

9.999
10.001
9.998
10.002

这不仅与浮点误差有关,也与实际信号噪声有关。

可以使用滞回区间:

高于10.1V时打开
低于9.9V时关闭

这样,状态不会因为阈值附近的微小波动反复变化。

因此,工程控制中不能只关注浮点表示,还要结合测量噪声和系统动态设计阈值。

二十三、如何降低求和误差

1. 使用更高精度类型

从单精度改为双精度,可以显著降低舍入误差,但不能完全消除。

2. 从小到大求和

把绝对值较小的数先相加,可以减少小数被大数“吞掉”的情况。

3. 使用补偿求和

补偿求和算法会额外记录部分舍入损失,并在后续运算中进行修正。

4. 分块求和

将大量数据分成多个小块,分别求和后再合并,通常比从头到尾顺序累加更稳定。

5. 使用整数或十进制类型

如果业务数据本质上具有固定小数位,定点表示可能比二进制浮点更加合适。

6. 重新设计公式

一些数学上等价的公式,在浮点计算中的稳定性可能完全不同。避免相近大数相减,常常比增加输出小数位更有效。

二十四、什么时候可以直接比较浮点数

并不是所有浮点数都不能使用==

以下情况可能是合理的:

  • 与自己刚刚赋值的同一浮点值比较;
  • 比较约定的特殊离散值;
  • 参与计算的数都能被二进制精确表示;
  • 判断某个变量是否被设置为确定的状态值;
  • 底层算法明确要求比较位级结果。

例如:

0.5
0.25
0.125

都能被二进制有限表示,一些简单计算可以得到精确结果。

问题不在于==这个操作本身,而在于开发者是否理解参与比较的数据来源和误差范围。

二十五、浮点数适合什么场景

浮点数非常适合:

  • 科学计算;
  • 机器学习;
  • 图像处理;
  • 三维图形;
  • 信号处理;
  • 物理仿真;
  • 统计分析;
  • 传感器数据;
  • 大范围近似数值。

这些场景通常更关注:

  • 数值范围;
  • 计算速度;
  • 相对精度;
  • 误差是否可控。

浮点数不是“不准确所以不能用”,而是在有限存储和高速计算之间做出的工程选择。

二十六、不适合直接使用二进制浮点的场景

需要谨慎的场景包括:

  • 金额;
  • 税费;
  • 账户余额;
  • 精确计数;
  • 超大整数编号;
  • 密码学整数运算;
  • 法律或合同规定的小数舍入;
  • 需要逐位完全一致的跨系统结果。

这些场景更适合:

  • 整数;
  • 定点数;
  • 十进制类型;
  • 大整数;
  • 专门的高精度数值库。

二十七、常见误区

1. 计算结果出现长小数就是程序错误

这通常只是浮点近似值被完整显示出来。

2. double可以精确表示所有小数

double比float精度高,但仍然不能精确表示大量十进制小数。

3. 多保留几位小数就能解决误差

格式化只改变显示结果,不会修复内部计算误差。

4. 所有浮点比较都不能使用等号

是否能够直接比较取决于数据来源和算法语义。

5. 使用一个固定极小值比较所有浮点数

不同数量级需要结合绝对误差和相对误差。

6. 金额使用double后最后统一四舍五入就一定安全

中间步骤、税费分摊和累计误差仍然可能造成不一致。

7. 运算顺序不会影响结果

浮点加法和乘法在实际计算中不一定满足数学上的结合律。

二十八、浮点计算检查清单

设计数值计算时,可以检查以下问题:

  • 数据需要精确值还是近似值;
  • 数据范围有多大;
  • 允许误差是多少;
  • 应该使用float还是double;
  • 是否更适合使用整数或十进制类型;
  • 是否直接比较了计算结果;
  • 比较容差是否考虑数值规模;
  • 是否存在大量重复累加;
  • 是否有相近大数相减;
  • 运算顺序是否影响结果;
  • 是否明确舍入规则;
  • 数据库字段类型是否合适;
  • 超大整数是否被转换成浮点数;
  • 是否正确处理NaN和无穷;
  • 传感器阈值是否考虑测量误差和滞回;
  • 单元测试是否覆盖边界值和极端值。

结语

0.1 + 0.2不一定等于0.3,根本原因是:

十进制0.1和0.2
→ 无法用有限位二进制精确表示
→ 只能保存邻近值
→ 运算过程中再次舍入
→ 最终结果出现微小误差

浮点数不是错误的数字类型,而是一种能够用有限空间表示巨大数值范围的近似计算工具。

正确使用浮点数的关键,不是追求绝对没有误差,而是:

  • 知道误差从哪里产生;
  • 根据业务定义允许误差;
  • 避免直接比较不稳定结果;
  • 为金额选择整数或十进制类型;
  • 为科学与工程计算选择数值稳定的算法;
  • 明确精度、范围和性能之间的取舍。

计算机并没有算错,它只是按照有限位二进制所能表达的最近数值完成了计算。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。