使用“;双“;作为循环中的计数器变量

本文关键字:计数器 变量 使用 循环 | 更新日期: 2023-09-27 18:00:51

在我目前正在读的一本书中,有这样一段摘录:

您也可以使用浮点值作为循环计数器。这是一个具有这种类型的for循环的示例计数器的:

double a(0.3), b(2.5);
for(double x = 0.0; x <= 2.0; x += 0.25)
    cout << "'n'tx = " << x << "'ta*x + b = " << a*x + b;

此代码片段计算对于x的值,a*x+b的值从0.02.0,步骤为0.25;但是,你需要小心在中使用浮点计数器时循环。许多十进制值不能精确地用二进制表示浮点形式,因此存在差异可以累积价值。这意味着您不应该对for循环,以便结束循环取决于浮点循环计数器达到精确值。对于例如,以下设计不佳循环永不结束:

for(double x = 0.0 ; x != 1.0 ; x += 0.2)
    cout << x;

这个循环的目的是当x的值变化时输出从CCD_ 8到CCD_;然而,0.2没有作为二进制浮点值,因此CCD_ 11的值从来都不完全是CCD_。因此,第二环路控制表达式总是错误的,并且循环无限期地继续。

有人能解释一下第一个代码块是如何运行的,而第二个不运行吗?

使用“;双“;作为循环中的计数器变量

即使x没有完全达到2.0,第一个最终也会终止……因为它最终会比2.0大,从而爆发。

第二个必须使x正好命中1.0才能断开。

不幸的是,第一个例子使用了0.25的步长,这正好可以用二进制浮点表示——如果两个例子都使用0.2作为步长,那会更明智。(0.2不能用二进制浮点精确表示。)

第一个块使用小于或等于条件(<=)。

即使存在浮点不准确性,这最终也是错误的。

这是一个更广泛问题的例子-在比较双打时,您通常需要在一些可接受的容差内检查是否相等,而不是完全相等。

在某些情况下,通常检查未更改的默认值,相等是可以的:

double x(0.0); 
// do some work that may or may not set up x
if (x != 0.0) {   
    // do more work 
}

不过,一般来说,与预期值进行检查是不可能的——你需要这样的东西:

double x(0.0); 
double target(10000.0);
double tolerance(0.000001);
// do some work that may or may not set up x to an expected value
if (fabs(target - x) < tolerance) {   
    // do more work 
}

浮点数在内部表示为二进制数,几乎总是采用IEEE格式。您可以在这里看到数字的表示方式:

http://babbage.cs.qc.edu/IEEE-754/

例如,二进制中的0.25是0.01b,表示为+1.00000000000000000000000*2-2

这在内部存储,符号为1位,指数为8位(表示-127和+128之间的值),值为23位(前导1。未存储)。事实上,比特是:

[0][01111101][0000000000000000000000000]

而二进制中的0.2没有精确的表示,就像1/3在十进制中没有精确的表达一样。

这里的问题是,正如1/2可以精确地以十进制格式表示为0.5,但1/3只能近似为0.3333333333一样,0.25可以精确地表示为二进制分数,但0.2不能。在二进制中,它是0.0010011001100110011001100….b,其中最后四位重复。

要存储在计算机上,它被分组为0.0010011001100110011001101b。这是非常非常接近的,所以如果你在计算坐标或任何其他绝对值重要的东西,这很好。

不幸的是,如果您将该值加五次,您将得到1 00000000000000000000001b。(或者,如果你把0.2四舍五入到0.001001100110011001100b,你会得到0.11111111111111111100b

无论哪种方式,如果循环条件为1 00000000000000000000001b==1 0000000000000000000000000000000b,它都不会终止。如果使用<=相反,如果值刚好低于最后一个值,它可能会额外运行一次,但它会停止。

可以制作一种能够准确表示小十进制值的格式(就像任何只有两位小数的值一样)。它们用于财务计算等。但正常的浮点值确实是这样工作的:它们用表示0.2等小"容易"数字的能力来换取以一致的方式表示大范围的能力。

出于这个原因,避免使用浮点作为循环计数器是很常见的,常见的解决方案是:

  • 如果一个额外的迭代无关紧要,则使用<
  • 如果它确实重要,则使条件<1.0001,或者比增量小的其他值,所以减去-0.0000000000000000000001的错误无关紧要
  • 在循环过程中使用整数并将其除以
  • 使用专门制作的类来精确地表示分数值

编译器可以优化float"="循环,将其转化为你想要发生的事情,但我不知道这是否是标准允许的,或者在实践中是否发生过。

该示例存在多个问题,不同情况下有两个不同之处。

  • 涉及浮点等式的比较需要该领域的专家知识,因此使用<>进行循环控制更安全。

  • 循环增量0.25实际上确实具有精确的表示

  • 循环增量0.2是否具有精确的表示

  • 因此,可以精确检查许多0.25(或1.0)增量的总和,但即使是单个0.2增量也不可能精确匹配。

人们经常引用一条一般规则:不要对浮点数进行相等比较虽然这是一个很好的一般建议,但当处理整数或整数加上由½+¼组成的分数时。。。你可以期待精确的表示。

你问为什么?简短的答案是:因为分数表示为½+¼。。。,大多数十进制数字都没有精确的表示,因为它们不能被分解为二次幂。这意味着FP内部表示是将舍入到输出的预期值的长字符串,但实际上并不是该值。

一般做法是不比较两个浮点数,即:

// using System.Diagnostics;
double a = 0.2; a *= 5.0;
double b = 1.0;
Debug.Assert(a == b);

由于浮点数的不精确性,a可能不完全等于b。为了比较相等性,您可以将两个数字的差异与容差值进行比较:

Debug.Assert(Math.Abs(a - b) < 0.0001);