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.10.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-6float 够用,double 可以用 1e-12

小技巧:如果数值范围很大(比如天文数字),用绝对误差可能不合适。这时候可以用相对误差:fabs(a - b) / fmax(fabs(a), fabs(b)) < EPSILON

避坑指南:我曾经踩过的雷

我曾经在一个电机控制项目里,直接用 == 比较两个浮点数。结果电机在某个位置死活停不准,来回抖动。查了两天,最后发现是浮点数比较的问题。从那以后,我写浮点数比较都会多问自己一句:“这个数真的是精确的吗?”

这里总结几个常见陷阱:

  • 不要用 == 比较运算结果——哪怕你觉得数学上应该相等。
  • 不要用 == 比较从文件或网络读入的浮点数——序列化和反序列化可能引入微小差异。
  • 不要用 == 比较不同精度的浮点数——比如 floatdouble 混用。
  • 不要用 == 判断浮点数是否为0——用 fabs(x) < EPSILON 代替。

特别注意:有些编译器优化可能会让 == 比较“碰巧”通过,但换个编译器或优化等级就崩了。别依赖这种运气。

知识体系:浮点数比较的核心逻辑

下面这张图,是我自己整理浮点数比较时的思考框架。你一看就明白:

浮点数比较决策流程 需要比较两个浮点数 数值范围是否固定且已知? 使用绝对误差比较 fabs(a-b) < EPSILON 使用相对误差比较 fabs(a-b)/max(a,b) < EPS 得到可靠的比较结果

实际项目中的经验

我记得有一次做传感器数据融合,两个浮点数明明在物理上应该相等,但程序就是判断不通过。后来我加了个 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。这样既统一,又不容易漏掉。

最后说一句:浮点数比较不是玄学,是数学。理解了二进制表示和误差来源,你就知道该怎么处理了。别怕,多踩几次坑就记住了。


专注资料整理