通过0.0时减去浮点数时出错

以下程序:

#include  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获取该信息)。

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

 val == target;  // 不安全

 //而是这样做
 //其中EPS是一些合适的低值,如1e-7
 fabs(val  -  target)

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

0.2不是双精度浮点数,因此它舍入到最接近的双精度数,即:

  0.200000000000000011102230246251565404236316680908203125 

这是相当笨拙的,所以让我们用hex来看看它:

  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) 

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

浮点运算不能完全代表所有数字。 因此,像你观察到的舍入错误是不可避免的。

一种可能的策略是使用固定点格式,例如十进制或货币数据类型。 此类型仍然不能代表所有数字,但会像您期望的那样表现。

详细说明一下:如果浮点数的尾数是以二进制编码的(大多数现代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,你就可以达到零。