Использование "double" в качестве счетчика переменных в циклах - PullRequest
45 голосов
/ 30 июня 2011

В книге, которую я сейчас читаю, есть отрывок:

Вы также можете использовать с плавающей точкой значение в качестве счетчика цикла. Вот пример цикла 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 для значений x от 0.0 до 2.0, с шагом 0.25; Тем не менее, вам нужно позаботиться при использовании счетчика с плавающей точкой в цикл. Многие десятичные значения не могут быть представлен в двоичном виде форма с плавающей точкой, поэтому расхождения может накапливаться с совокупными значениями. Это означает, что вы не должны кодировать для цикла, так что окончание цикла зависит от цикла с плавающей точкой счетчик достигает точного значения. За Например, следующий плохо спроектированный цикл никогда не заканчивается:

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

Целью этого цикла является выведите значение x, как оно меняется от 0.0 до 1.0; однако 0.2 не имеет точного представления как двоичное значение с плавающей точкой, поэтому значение x никогда не бывает точно 1. Таким образом, второй цикл управления выражение всегда ложно, а цикл продолжается до бесконечности.

Может кто-нибудь объяснить, как работает первый блок кода, а второй нет?

Ответы [ 6 ]

71 голосов
/ 30 июня 2011

Первый в конечном итоге прекратит работу, даже если x не достигнет точно 2.0 ... потому что в конечном итоге он будет больше , чем 2,0, и, таким образом, сломаетсяout.

Второй должен сделать x hit точно 1.0, чтобы сломаться.

К сожалению, в первом примере используется шаг 0,25,который в точности представлен в двоичной системе с плавающей запятой - было бы разумнее сделать так, чтобы оба примера использовали 0.2 в качестве размера шага.(0.2 не является точно представимым в двоичной плавающей запятой.)

15 голосов
/ 30 июня 2011

В первом блоке используется условие меньше или равно (<=).

Даже с неточностью с плавающей точкой это в конечном итоге будет ложным.

9 голосов
/ 30 июня 2011

Это пример более широкой проблемы - при сравнении значений типа double вам часто нужно проверять равенство в пределах некоторого допустимого допуска , а не точное равенство.

В некоторых случаях, как правило, для проверки неизменного значения по умолчанию, равенство в порядке:

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 
}
6 голосов
/ 01 июля 2011

Числа с плавающей запятой представлены внутри в виде двоичного числа, почти всегда в формате IEEE. Вы можете увидеть, как числа представлены здесь:

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

Например, 0,25 в двоичном виде - это 0,01 b и представляется как +1,00000000000000000000000 * 2 -2 .

Хранится внутри с 1 битом для знака, восемью битами для показателя степени (представляющего значение между -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 , оно не прекратится. Если вместо этого использовать <=, возможно, он запустится еще один раз, если значение будет чуть ниже последнего значения, но остановится. </p>

Можно было бы создать формат, который может точно представлять небольшие десятичные значения (как любое значение только с двумя десятичными знаками). Они используются в финансовых расчетах и ​​т. Д. Но обычные значения с плавающей точкой работают так: они обменивают способность представлять некоторые небольшие «простые» числа, например, 0,2, на способность представлять широкий диапазон согласованным образом.

Обычно избегают использования числа с плавающей запятой в качестве счетчика циклов, по этой точной причине общие решения были бы:

  • Если одна дополнительная итерация не имеет значения, используйте <= </li>
  • Если это имеет значение, задайте вместо этого условие <= 1.0001 или какое-либо другое значение меньше вашего приращения, чтобы ошибки off-by-0.0000000000000000000001 не имели значения </li>
  • Используйте целое число и делите его на что-то во время цикла
  • Используйте класс, специально созданный для точного представления дробных значений

Компилятор мог бы оптимизировать цикл "=" с плавающей точкой, чтобы превратить его в то, что вы хотите, но я не знаю, разрешено ли это стандартом или когда-либо происходит на практике.

2 голосов
/ 01 июля 2011

В этом примере много проблем, и в разных случаях две вещи различны.

  • Сравнение, включающее равенство с плавающей запятой, требует экспертного знания предметной области, поэтому безопаснее использовать< или > для элементов управления цикла.

  • Приращение цикла 0.25 на самом деле имеет точное представление

  • Приращение цикла 0.2 делает не точным представлением

  • Следовательно, возможноточно проверьте сумму многих 0,25 (или 1,0 ) приращений, но невозможно точно сопоставить даже одно приращение 0,2 .

Часто цитируется общее правило: не делайте сравнения на равенство чисел с плавающей запятой. Хотя это хороший общий совет при работе с целыми числами или целыми числами плюсдроби, состоящие из ½ + ¼ ... вы можете ожидать точного представления.

И вы спросили почему ?Краткий ответ: поскольку дроби представлены как ½ + ¼ ..., большинство десятичных чисел не имеют точных представлений, поскольку их нельзя разложить на степени двух.Это означает, что внутренние представления FP представляют собой длинные строки битов, которые будут округлять до ожидаемого значения для вывода, но на самом деле не будет точно этим значением.

1 голос
/ 01 июля 2011

Общая практика заключается в том, что вы не сравниваете два числа с плавающей запятой, т. Е .:

// 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);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...