6

我们是否可以安全地使用浮点数作为循环计数器并在每次迭代时按小数递增/递减它们,就像下面看似无风险的程序一样?当然我知道使用浮点数作为==运算符的操作数是愚蠢的要做的事情。但是将浮点数用作“正常”目的的其他比较操作的操作数有什么问题?所谓“正常”,我的意思是,即使浮点数可能不是数字的确切数字表示,但0.000000001在大多数情况下,这种变化不是无关紧要并且可以忽略吗?(例如在下面的程序中甚至不明显)

但这就是说,这是我的担忧。假设表示不准确,并且5.0实际上是4.999999。因此,当我们在每次迭代中继续递减0.5时,最后与0的比较可能会变为错误,并且循环可能会退出0.000001的差异,不会显示当前输出的最后一行。我希望你明白我的意思。我错了?

#include<stdio.h>

int main(void)
{
float f;

for(f=5.0;f>=0;f-=0.5)
printf("%f\n",f);
}

输出:

5.000000
4.500000
4.000000
3.500000
3.000000
2.500000
2.000000
1.500000
1.000000
0.500000
0.000000
4

5 回答 5

12

不,由于您的问题中给出的原因,这不安全。考虑一下:

#include<stdio.h>

int main(void) {
  float f = 1.0;

  for(;f>0;f-=0.1)
     printf("%f\n",f);
  return 0;
}

当由 初始化时,这个例子似乎工作得很好。但是将其更改为 3.0 - 事情很快就会变得更加有趣:f1.0

2.600000
2.500000
2.400001
...
0.000001

... 导致臭名昭著的“一个接一个”的失败。


你认为你可能是安全的,>=而不是>想想:

float f = 5.0;
for(;f>=1;f-=0.4)
  printf("%f\n",f);

...
3.400000
3.000000
2.599999
2.199999
1.799999
1.399999

...我们一次又一次地去(因为0.99999小于 1)。

于 2013-05-16T19:30:26.460 回答
10

只要起始值、递减量和所有递减的结果都能在浮点类型提供的精度范围内准确无误地表示,就可以放心使用。注意这里的“无错误”是指0绝对错误,非常小的错误仍然认为是错误。

在您的情况下,起始值5.0和递减量0.5可以无错误地表示,并且4.5, 4.0, 3.5, ...,0.0也可以在 23 位精度内无错误地表示float。在你的情况下是安全的。

如果假设起始值为4000000.0,递减量为0.00390625(2 -8 ),那么你就有麻烦了,因为float虽然起始值和递减量都不能以 23 位精度的类型准确无误地表示递减结果金额可以正确表示。

但是,当整数类型在这种情况下更可靠时,我认为使用浮点没有任何意义。您不必浪费脑细胞来检查我上面所说的条件是否适用。

于 2013-05-16T19:43:19.900 回答
6

由于浮点表示的问题,尽可能选择整数值而不是浮点数。

不要使用浮点数作为循环控制,而是重新编写逻辑以使用整数:

需要减少你的计数器.5吗?将起始值加倍并减 1:

float f = 5.0;
int i = f * 2;

for(; i >= 0; i--)
    printf("%f\n", i / 2.0);

需要递减.1?

float f = 5.0;
int i = f * 10;

for(; i >= 0; i--)
    printf("%f\n", i / 10.0);

这是问题中示例的简单方法。当然不是唯一的方法或最正确的方法。一个更复杂的例子可能需要重新设计逻辑有点不同。什么情况都适合。

我想我的观点是将实际浮点值推迟到最后一刻,以减少由于表示而引入的错误。

于 2013-05-16T20:10:51.040 回答
0

工程师和科学家经常编写迭代程序,其中浮点值以小增量逐步通过一系列值。

例如,假设“时间”变量需要以 t 的tMax步长从 tMin 的低值变为 tMin 的高值deltaT,其中所有这些变量都是双倍的。

明显但不正确的方法如下:

`for( time = tMin; time <= tMax; time += deltaT ) {
            // Use the time variable in the loop
}
`

那么为什么这是错误的呢?

如果 deltaT 很小和/或范围很大(或两者兼有),则循环可能会执行数千次迭代。

这意味着到循环结束时,time已经通过数千次加法运算的总和计算出来了。

以十进制形式对我们来说似乎“精确”的数字,例如 0.01,当计算机以二进制形式存储它们时并不精确,这意味着用于的值deltaT实际上是精确值的近似值。

因此,每个加法步骤都会引入非常少量的舍入误差,当您将数千个这些误差相加时,总误差可能会很大。

如果您知道每次迭代的最小值和最大值以及所需的更改,则正确的方法如下:

`int nTimes = ( tMax - tMin ) / deltaT + 1;
 for( int i = 0; i < nTimes; i++ ) {
 time = tMin + i * deltaT;
}
 // NOW use a more accurate time variable
 // Or alternatively if you know the minimum, maximum, and number of desired iterations:
 double deltaT = ( tMax - tMin ) / ( nTimes - 1 );
 for( int i = 0; i < nTimes; i++ ) {
 time = tMin + i * deltaT;
 // NOW use a more accurate time variable
}
`

一般来说,有四个值可用于指定在一个范围内的步进——范围的低端、范围的高端、要采取的步数以及每一步要采取的增量——如果你知道其中任何三个,然后你可以计算第四个。

正确的循环应该使用整数计数器完成给定次数的循环,并使用范围的低端和如图所示的增量来计算循环每次迭代开始时的浮点循环变量。那为什么更好呢?

循环执行的次数现在由一个整数控制,它在递增时没有任何舍入误差,因此不会因为累积舍入而执行一次太多或一次太少的迭代。

时间变量现在是通过单次乘法和单次加法计算的,这仍然会引入一些舍入误差,但远小于数千次加法。+1 从何而来?

需要 +1 才能包括范围的两个端点。假设tMax为 20,tMin 为 10,则为deltaT2。

所需的时间是 10、12、14、16、18、20,总共是 6 个时间值,而不是 5。(如果你想这样看,五个间隔。) ( 20 - 10 ) / 2产生 5,所以你必须添加额外的 1 以获得正确的次数 6。

另一种看待这个问题的方法是,如果 nTimes 是范围内的数据点数,那么nTimes - 1就是数据点之间的间隙数。

示例: interpolate.c 是一个在循环中插入浮点数的简单粗暴的示例,它在课堂上 10 分钟内完成。这不是一个好的代码示例,但它是一个快速的小程序如何用于测试、玩游戏或在这种情况下演示一个新的或不熟悉的概念的示例。

此示例使用三种方法在f( x ) = x^3-1.04.0的步长范围内对函数进行插值:0.5

常数 - 取端点处输入的平均值,计算 f( 平均输入 ),并假设函数在该范围内是常数。

线性 - 评估端点处的函数,然后在其间使用端点函数值的线性插值。

非线性 - 在范围内线性插值函数输入,并在每个评估点评估插值输入的函数。

于 2018-09-04T10:24:28.140 回答
0

不要使用浮点变量作为循环计数器。原因是计算机中的浮点变量只能近似表示实数,而不是精确的整数。

导致失败的情况有两种: 情况1:比较函数需要精度

    for(float val = 0.1f; x<=1.0f; x+=0.1f){
      /*loop may iterate 9 or 10 times, depend on the precision of compare function*/
    }

案例2:大浮点值增加/减少小浮点值,不改变大浮点值的精度

for(float bigVal = 1000000000.f; bigVal>10.f; bigVal -= 1.0f;){
  /*Dead loop, loop forever, bigVal doesn't change after each iteration*/
}

您可以参考这个有用的网页:https ://wiki.sei.cmu.edu/confluence/display/c/FLP30-C.+Do+not+use+floating-point+variables+as+loop+counters

于 2022-03-03T09:12:26.450 回答