【C/C++】浮点数大小的比较问题分析及解决方案

1. 问题

在实际工程当中时常会遇到浮点数float的比较。但常规的比较思路是不可靠的,C/C++JavapythonJavaScript等主流语言都存在这个问题,存在巨大的工程隐患。例如:

float a = 0.9f;
float c = 1.0f;

我们想判断a + 0.1变量c的大小,按常规的做法就是:

printf("%d\n", (a + 0.1) == c); // 0,即不相等

我们会发现时而正确时而不正确,给项目留下隐患。实际上这是计算机浮点数表示法精度导致的问题。

2. 分析

我们先看一看aa + 0.1c三者值是什么。
使用常规精度的printf会进行近似计算再给出结果,所以表面上看上去是正确的。

printf("%lf\n", a);       // 0.9000000
printf("%lf\n", a + 0.1); // 1.0000000
printf("%lf\n", c);       // 1.0000000

实际上,实际值是有些许偏差的,我们不妨让printf多输出几位小数看看它的庐山真面目。

printf("%.20lf\n", a);       // 0.89999997615814208984
printf("%.20lf\n", a + 0.1); // 0.99999997615814206764
printf("%.20lf\n", c);       // 1.00000000000000000000

所以导致二者比较结果不正确,不符合0.9 + 0.1 = 1.0这个预期也就不奇怪了。

printf("%d\n", (a + 0.1) == c); // 0,即不相等

3. 解决方案

当了解到是在计算机处理精度不足时的四舍五入导致的小数点后值细微的偏差,我们可以采用类似极限的思想,即采用 | a - b | < 精度的方式判断ab是否相等。
此处使用的精度是float.h头文件中定义的FLT_EPSILON常量。

// 此处使用的是float.h头文件中定义的FLT_EPSILON常量。
if (fabs(a + 0.1 - c) < FLT_EPSILON)
{
    printf("1"); // 会输出1,即相等
}
else
{
    printf("0");
}

注意:

实际上大于、小于的比较也存在这样的误差,例如

printf("%.20lf\n", a + 0.1); // 0.99999997615814206764
printf("%.20lf\n", c);       // 1.00000000000000000000

实际上会出现a + 0.1 < c,但是我们的心理预期是二者应当相等的而不是a + 0.1小于变量c,所以在大于、小于比较之前,需要排除掉相等的情况

// 判断大小一定要先排除相等的情况,再使用大于小于号
// 下面两种情况都不会输出,即不会误判大于和小于。
if (fabs(a + 0.1 - c) > FLT_EPSILON && (a + 0.1) > c)
{
    printf("a + 0.1 > c\n"); // 不会输出
}

if (fabs(a + 0.1 - c) > FLT_EPSILON && (a + 0.1) < c)
{
    printf("a + 0.1 < c\n"); // 不会输出
}

特别地:

  1. 0.50.250.125……等可以由二进制直接表示的是精确表示的,除此之外都是需要考虑误差的。
  2. 有可能现代编译器会帮助我们对浮点数比较进行了部分优化,在某些情况下这个问题可能是不存在对。不过小心使得万年船吧。
  3. 可考虑两边同乘1,000,000,并转化为int来进行比较。在某些情况下性能较好。甚至考虑不用float
  4. JavaFloat类提供了compareTo()方法。

4. 完整示例代码

#include <stdio.h>
#include <float.h> // FLT_EPSILON
#include <math.h>  // fabs()

int main(int argc, char const *argv[])
{
    float a = 0.9f;
    float c = 1.0f;

    // 使用常规精度的printf会进行近似计算再给出结果,所以表面上看上去是正确的。
    printf("%lf\n", a);       // 0.9000000
    printf("%lf\n", a + 0.1); // 1.0000000
    printf("%lf\n", c);       // 1.0000000

    // 实际值其实是有些许偏差的。
    printf("%.20lf\n", a);       // 0.89999997615814208984
    printf("%.20lf\n", a + 0.1); // 0.99999997615814206764
    printf("%.20lf\n", c);       // 1.00000000000000000000

    // 所以导致二者比较结果不正确,不符合0.9 + 0.1 = 1.0这个预期。
    printf("%d\n", (a + 0.1) == c); // 0,即不相等

    // 解决方案:
    // 当判断是否相等时,采用 | a - b | < 精度 的方式判断。
    // 此处使用的是float.h头文件中定义的FLT_EPSILON常量。
    if (fabs(a + 0.1 - c) < FLT_EPSILON)
    {
        printf("1"); // 会输出1,即相等
    }
    else
    {
        printf("0");
    }

    // 注意:
    // 判断大小一定要先排除相等的情况,再使用大于小于号
    // 下面两种情况都不会输出,即不会误判大于和小于。
    if (fabs(a + 0.1 - c) > FLT_EPSILON && (a + 0.1) > c)
    {
        printf("a + 0.1 > c\n"); // 不会输出
    }

    if (fabs(a + 0.1 - c) > FLT_EPSILON && (a + 0.1) < c)
    {
        printf("a + 0.1 < c\n"); // 不会输出
    }

    return 0;
}