在循环中使用“double”作为计数器variables
在我正在阅读的一本书中,有这样一段摘录:
您也可以使用浮点值作为循环计数器。 下面是一个用这种计数器
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;
这个代码片段计算
a*x+b
值从0.0
到2.0
的a*x+b
的值,步长为0.25
; 但是,在循环中使用浮点计数器时需要小心。 许多十进制值不能完全用二进制浮点forms表示,所以差异可以累积起来。 这意味着你不应该编写一个for循环,这样循环的结束取决于一个浮点循环计数器达到一个精确值。 例如,以下devise不佳的循环永远不会结束:for(double x = 0.0 ; x != 1.0 ; x += 0.2) cout << x;
这个循环的意图是输出
x
的值,因为它从0.0
变化到1.0
; 然而,0.2
没有精确表示为二进制浮点值,所以x
的值不会是1
。 因此,第二个循环控制expression式总是为false,循环无限期地继续。
有人可以解释第一个代码块如何运行,而第二个代码块不运行?
第一个最终会终止,即使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.01 b ,表示为+1.00000000000000000000000 * 2 -2 。
在内部存储1位的符号,8位的指数(表示-127到+128之间的值,23位的值(前导1.未被存储)。实际上,这些位是:
[0] [01111101] [00000000000000000000000]
而在二进制0.2没有确切的表示,就像1/3没有精确的十进制表示。
这里的问题是,正如1/2可以精确地以十进制格式表示为0.5,但是1/3只能近似为0.3333333333,0.25可以精确地表示为二进制分数,但是0.2不能。 在二进制中,它是0.0010011001100110011001100 …. b最后四位数字重复。
要存储在计算机上,它将被路由到0.0010011001100110011001101 b 。 这是真的,非常接近,所以如果你正在计算坐标或绝对值的其他任何事情,这很好。
不幸的是,如果你把这个值加到自己五次,你会得到1.00000000000000000000001 b 。 (或者,如果将0.2舍入为0.0010011001100110011001100 b ,则会得到0.11111111111111111111100 b )
无论哪种方式,如果您的循环条件是1.00000000000000000000001 B == 1.00000000000000000000000 B它不会终止。 如果使用<=来代替,如果该值刚好在最后一个值之下,则可能会运行一次额外的时间,但是会停止。
这将有可能使一个格式,可以准确地代表小十进制值(如只有两位小数的任何值)。 它们用于财务计算等。但正常的浮点值的确如此工作:它们交换代表一些小的“简单”数字(如0.2)的能力,以便以一致的方式表示大范围的能力。
出于这个原因,避免使用float作为循环计数器是常见的,常见的解决scheme是:
- 如果一个额外的迭代无关紧要,请使用<=
- 如果这很重要,请将条件设置为<= 1.0001,或者将其他值小于您的增量值,那么closures0.0000000000000000000001错误并不重要
- 在循环中使用一个整数并将其分开
- 使用专门制作的分数来精确地表示分数值
编译器可以优化一个浮点“=”循环,将其转化为你所期望的发生,但是我不知道这是标准所允许的还是在实践中发生的。
这个例子有很多问题,两种情况是不同的。
-
涉及浮点数相等的比较需要专业领域的知识,所以使用
<
或>
作为循环控制更为安全。 -
循环增量
0.25
实际上具有确切的表示forms -
循环增量
0.2
不具有确切的表示forms -
因此,可以精确地检查许多0.25 (或1.0 )增量的总和,但即使是单个0.2增量也不可能精确匹配。
经常会引用一条通用规则: 不要进行浮点数的相等比较。 虽然这是一个很好的一般性build议,但在处理整数或整数加上由1/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);