44、浮点数比较:浮点数直接相等比较的精度问题
浮点数比较,这话题我每次讲课时都会多说几句。为什么?因为踩坑的人实在太多了。我自己刚入行那会儿,也在这上面栽过跟头——调试了一整天,最后发现是 if (a == b) 惹的祸。
说白了,浮点数在计算机里存的不是精确值。你写个 0.1,它存的是个近似值。两个近似值直接比相等,那不就是碰运气吗?
为什么不能直接比?
先看个例子。你猜下面这段代码会输出什么?
#include <stdio.h>
int main() {
float a = 0.1f;
float b = 0.1f;
if (a == b) {
printf("相等\n");
} else {
printf("不相等\n");
}
return 0;
}
大部分情况下它会打印“相等”。但别高兴太早,换种写法试试:
#include <stdio.h>
int main() {
float a = 0.1f;
float b = 0.2f;
float c = a + b;
if (c == 0.3f) {
printf("相等\n");
} else {
printf("不相等\n");
}
return 0;
}
嗯,你猜结果?大概率是“不相等”。
为什么会这样?因为 0.1 和 0.2 在二进制里都是无限循环小数。计算机只能截取有限位,运算后再截取一次。两次截取误差叠加,最后 c 的值跟 0.3 的近似值对不上。
核心结论:浮点数的相等比较,必须用“误差范围”来判断,不能直接用 ==。
正确的比较方式
我个人的习惯是定义一个很小的阈值,叫 EPSILON。两个数差的绝对值小于这个阈值,就认为它们相等。
#include <math.h>
#include <stdio.h>
#define FLOAT_EPSILON 1e-6f
int float_equal(float a, float b) {
return fabsf(a - b) < FLOAT_EPSILON;
}
int main() {
float a = 0.1f;
float b = 0.2f;
float c = a + b;
if (float_equal(c, 0.3f)) {
printf("相等\n");
} else {
printf("不相等\n");
}
return 0;
}
这样写,结果就是“相等”了。阈值选多大?看你的应用场景。一般 1e-6 对 float 够用,double 可以用 1e-12。
小技巧:如果数值范围很大(比如天文数字),用绝对误差可能不合适。这时候可以用相对误差:fabs(a - b) / fmax(fabs(a), fabs(b)) < EPSILON。
避坑指南:我曾经踩过的雷
我曾经在一个电机控制项目里,直接用 == 比较两个浮点数。结果电机在某个位置死活停不准,来回抖动。查了两天,最后发现是浮点数比较的问题。从那以后,我写浮点数比较都会多问自己一句:“这个数真的是精确的吗?”
这里总结几个常见陷阱:
- 不要用
==比较运算结果——哪怕你觉得数学上应该相等。 - 不要用
==比较从文件或网络读入的浮点数——序列化和反序列化可能引入微小差异。 - 不要用
==比较不同精度的浮点数——比如float和double混用。 - 不要用
==判断浮点数是否为0——用fabs(x) < EPSILON代替。
特别注意:有些编译器优化可能会让 == 比较“碰巧”通过,但换个编译器或优化等级就崩了。别依赖这种运气。
知识体系:浮点数比较的核心逻辑
下面这张图,是我自己整理浮点数比较时的思考框架。你一看就明白:
实际项目中的经验
我记得有一次做传感器数据融合,两个浮点数明明在物理上应该相等,但程序就是判断不通过。后来我加了个 printf 打印出来看——一个存的是 1.0000001,另一个是 0.9999999。你说这能相等吗?
还有一次,我在嵌入式设备上做 PID 控制。积分项累加多了,浮点数误差越来越大。最后我改用定点数才解决。不过那是另一个话题了。
你想想看,如果浮点数比较这么容易出问题,那标准库为什么不直接提供一个 float_equal() 函数?因为阈值怎么选,完全取决于你的业务场景。没人能替你决定 EPSILON 该取多大。
| 场景 | 推荐阈值 | 说明 |
|---|---|---|
| 普通计算(float) | 1e-6 | 大部分场景够用 |
| 普通计算(double) | 1e-12 | 精度更高 |
| 图形学/游戏 | 1e-4 或 1e-5 | 视觉上感觉不到差异即可 |
| 科学计算 | 1e-15 | 需要高精度时 |
| 嵌入式/资源受限 | 1e-4 或 1e-5 | 避免过度计算 |
我的建议:写一个通用的浮点数比较函数,放在你的工具库中。每次需要比较时直接调用,别临时写 fabs。这样既统一,又不容易漏掉。
最后说一句:浮点数比较不是玄学,是数学。理解了二进制表示和误差来源,你就知道该怎么处理了。别怕,多踩几次坑就记住了。