我们可以安全地使用浮点数作为循环计数器,并在每次迭代时按小数量递增/递减它们,就像下面看似无风险的程序一样吗?当然我知道使用浮点数作为==运算符的操作数是愚蠢的要做的事情。但是为了“正常”目的使用浮动作为其他比较操作的操作数有什么问题? “正常”我的意思是,即使浮点数可能不是数字的精确数字表示,但不是像0.000000001
这样的变化无关,在大多数情况下可以忽略吗? (例如,在以下程序中甚至不明显)
但是那说,这是我的理解。假设表示不准确,5.0实际上是4.999999。因为我们在每次迭代时继续递减0.5,最后一次与0的比较可能会变为false并且循环可能会因为相差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
不,这是不安全的,原因在于你的问题。考虑一下:
#include<stdio.h>
int main(void) {
float f = 1.0;
for(;f>0;f-=0.1)
printf("%f\n",f);
return 0;
}
当quite ok由f
初始化时,这个例子似乎起作用1.0
。但是将其改为3.0 - 很快就会开始让more interesting获得成功:
2.600000
2.500000
2.400001
...
0.000001
......导致臭名昭着的“一个接一个”的失败。
你认为用>=
而不是>
可能会安全吗?想想again:
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)。
只要起始值,减量和所有减量的结果都可以在浮点类型提供的精度内无误差地表示,那么使用它是安全的。注意,“无错误”在这里表示0绝对错误,非常小的错误仍然被认为是错误。
在您的情况下,起始值5.0
和减量量0.5
可以无误差地表示,并且4.5
,4.0
,3.5
,...,0.0
也可以在float
的23位精度内无误差地表示。在您的情况下是安全的。
如果让我们说起始值是4000000.0
并且减量量是0.00390625
(2-8),那么你就麻烦了,因为在float
类型的23位精度中,减量的结果不能没有错误地表示,尽管起始值并且可以正确地表示减量。
但是,在这种情况下,当积分类型更可靠时,我认为使用浮点没有意义。您不必浪费脑细胞检查上述条件是否适用。
由于浮点表示的问题,只要可能,首选整数值超过浮点。
而不是使用浮点数作为循环控件,重写您的逻辑以使用整数:
需要通过.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);
对于问题中的示例,这是一种简单的方法。当然不是唯一的方法或最正确的方法。更复杂的示例可能需要将逻辑重新设计稍微不同。无论情况如何。
我的观点是,我认为暂缓使用实际浮点值直到最后一刻可以减少由于表示引起的错误。
工程师和科学家经常编写迭代程序,其中浮点值以小增量逐步通过一系列值。
例如,假设“时间”变量需要以tMax
的步长从低的tMin变为高的deltaT
,其中所有这些变量都是双倍的。
显而易见的但是INCORRECT方法如下:
`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,deltaT
为2。
所需的时间将是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.0
到4.0
的范围内插入函数0.5
:
常量 - 取端点输入的平均值,计算f(平均输入),并假设函数在该范围内保持不变。
线性 - 评估端点处的函数,然后使用两者之间的端点函数值的线性插值。
非线性 - 在范围内线性插值函数输入,并在每个评估点评估插值输入的功能。