2

以下程序:

#include <stdio.h>

int main()
{
    double val = 1.0;
    int i;

    for (i = 0; i < 10; i++)
    {
        val -= 0.2;
        printf("%g %s\n", val, (val == 0.0 ? "zero" : "non-zero"));
    }

    return 0;
}

产生这个输出:

0.8 non-zero
0.6 non-zero
0.4 non-zero
0.2 non-zero
5.55112e-17 non-zero
-0.2 non-zero
-0.4 non-zero
-0.6 non-zero
-0.8 non-zero
-1 non-zero

谁能告诉我从 0.2 减去 0.2 时导致错误的原因是什么?这是舍入错误还是其他?最重要的是,如何避免此错误?

编辑:看起来结论是不用担心,因为 5.55112e-17 非常接近于零(感谢@therefromhere 提供的信息)。

4

4 回答 4

3

这是因为浮点数不能以精确的值存储在内存中。因此,==在浮点值中使用永远是不安全的。使用 double 会提高精度,但同样不准确。比较浮点值的正确方法是执行以下操作:

val == 目标;// 不安全

// 而不是这样做
// 其中 EPS 是一些合适的低值,例如 1e-7
晶圆厂(val - 目标)< EPS;

编辑:正如评论中所指出的,问题的主要原因是 0.2 无法准确存储。因此,当您从某个值中减去它时,每次都会导致一些错误。如果您反复进行这种浮点计算,那么在某些时候错误会很明显。我想说的是,所有浮点值都无法存储,因为它们是无限的。轻微的错误值通常不会引起注意,但使用连续计算会导致更高的累积误差。

于 2011-04-21T09:25:01.197 回答
2

0.2 不是双精度浮点数,所以四舍五入到最接近的双精度数,即:

            0.200000000000000011102230246251565404236316680908203125

这相当笨拙,所以让我们用十六进制来看看它:

          0x0.33333333333334

现在,让我们看看当这个值被反复从 1.0 中减去时会发生什么:

          0x1.00000000000000
        - 0x0.33333333333334
        --------------------
          0x0.cccccccccccccc

确切的结果不能用双精度表示,所以它是四舍五入的,它给出:

          0x0.ccccccccccccd

在十进制中,这正是:

            0.8000000000000000444089209850062616169452667236328125

现在我们重复这个过程:

          0x0.ccccccccccccd
        - 0x0.33333333333334
        --------------------
          0x0.9999999999999c
rounds to 0x0.999999999999a
           (0.600000000000000088817841970012523233890533447265625 in decimal)

          0x0.999999999999a
        - 0x0.33333333333334
        --------------------
          0x0.6666666666666c
rounds to 0x0.6666666666666c
           (0.400000000000000077715611723760957829654216766357421875 in decimal)

          0x0.6666666666666c
        - 0x0.33333333333334
        --------------------
          0x0.33333333333338
rounds to 0x0.33333333333338
           (0.20000000000000006661338147750939242541790008544921875 in decimal)

          0x0.33333333333338
        - 0x0.33333333333334
        --------------------
          0x0.00000000000004
rounds to 0x0.00000000000004
           (0.000000000000000055511151231257827021181583404541015625 in decimal)

因此,我们看到浮点运算所需的累积舍入会产生您正在观察的非常小的非零结果。舍入是微妙的,但它是确定性的,不是魔术,也不是错误。值得花时间去学习。

于 2011-04-22T17:29:23.213 回答
1

详细说明一下:如果浮点数的尾数以二进制编码(就像大多数当代 FPU 的情况一样),那么只有数字 1/2、1/4、1/8 的(倍数)之和, 1/16, ... 可以在尾数中精确表示。值 0.2 近似为 1/8 + 1/16 + .... 一些甚至更小的数字,但用有限尾数无法达到 0.2 的确切值。

您可以尝试以下方法:

 printf("%.20f", 0.2);

你(可能)会看到你认为的 0.2 不是 0.2,而是一个微小差异的数字(实际上,在我的计算机上它打印 0.20000000000000001110)。现在你明白了为什么你永远无法达到 0。

但是如果你让 val = 12.5 并在你的循环中减去 0.125,你可能会达到零。

于 2011-04-21T09:40:56.930 回答
1

浮点运算不能准确地表示所有数字。因此,您观察到的舍入误差是不可避免的。

一种可能的策略是使用定点格式,例如小数或货币数据类型。此类类型仍然不能代表所有数字,但会按照您对本示例的预期表现。

于 2011-04-21T09:41:29.017 回答