建议3-2:避免使用浮点数进行精确计算

前面已经阐述过,由于计算机的字长有限,浮点数能够精确表示的数是有限的。因此在进行数值计算时,有可能要对计算得到的中间结果数据使用相关的舍入规则来取近似值,而这就会导致计算过程产生误差。示例如代码清单1-18所示。

代码清单1-18 浮点数计算示例


#include <stdio.h>
#include <limits.h>
float average(float *arr,size_t size);
enum { array_size = 10 };
int main(void)
{
    float arr[array_size];
    size_t i=0;
    for (i = 0; i < array_size; i++)
    {
            arr[i] = 5.1;
    }
    printf("total / size = %f\n", average(arr,array_size));
    return 0;
}
float average(float *arr,size_t size)
{
    float total = 0.0;
    size_t i=0;
    if (size > 0&&size<=SIZE_MAX)
    {
            for (i = 0; i < size; i++)
            {
                total += arr[i];
                printf("arr[%d] = %f , total = %f\n",
                        i, arr[i], total);
            }
            return total / size;
    }
    else
    {
            return 0.0;
    }
}

在代码清单1-18中,程序取了10个相同的浮点数(5.1)来计算其平均值。理论上,由于这10个浮点数是相同的,都是5.1,因此所计算得出的平均值也应该是5.1。但实际计算结果并非如此,如图1-28所示。

图1-28 代码清单1-18在GCC中的运行结果

是什么原因导致产生图1-28所示的结果呢?

如果你详细看完建议3-1中的内容,相信找出答案并不难。其实,之所以得到这样的运行结果,归根结底就是因为5.1无法精确地表达为相应的浮点数,而只能保存为经过舍入计算的近似值。这个近似值再进行累加运算之后,自然无法产生精确的结果,最后的平均值结果也就自然而然地产生了误差。

为了让大家能够更加清楚地看见其结果的舍入情况,笔者把代码清单1-18里的浮点数保留到小数点22位,如下所示:


printf("total / size = %.22f\n", average(arr,array_size));…
printf("arr[%d] = %.22f , total = %.22f\n", i, arr[i], total);

运行调整后的代码清单1-18,你就可以清楚地看见其舍入结果,如图1-29所示。

图1-29 调整后的代码清单1-18在GCC中的运行结果(浮点数保留22位小数)

当然,这种结果并不是唯一的,如果编译器不同,舍入的值也有可能会不同。例如,在VC++2010中运行调整后的代码清单1-18的结果如图1-30所示。

图1-30 调整后的代码清单1-18在VC++2010中的运行结果(浮点数保留22位小数)

由此可以看出,浮点数据会因为其舍入误差而导致运算结果产生误差,从而失去精确性。因此,我们应该尽量避免使用浮点数进行精确计算。

当然,对于代码清单1-18,我们还可以通过整数代替浮点数的方法来执行内部加法,以保证其结果的精确性。而浮点数只用在打印结果及执行除法计算算术平均值的时候,如代码清单1-19所示。

代码清单1-19 整数代替浮点数示例


#include <stdio.h>
#include <limits.h>
float average(int *arr,size_t size);
enum { array_size = 10 };
int main(void)
{
    int arr[array_size];
    size_t i=0;
    for (i = 0; i < array_size; i++)
    {
            arr[i] = 510;
    }
    printf("total / size = %f\n", average(arr,array_size));
    return 0;
}
float average(int *arr,size_t size)
{
    int total = 0;
    size_t i=0;
    if (size > 0&&size<=SIZE_MAX)
    {
            for (i = 0; i < size; i++)
            {
                total += arr[i];
                printf("arr[%d] = %f , total = %f\n", i,
                       (float)arr[i]/100, (float)total/100);
            }
            return (float)(total/ size)/100;
    }
    else
    {
            return 0.0;
    }
}

上面代码的运行结果如图1-31所示。虽然采用这种方法就可以避免浮点数的舍入误差带来的不精确性,但在一般情况下不建议这样做。

图1-31 代码清单1-19在GCC中的运行结果