Подтвердить что ты не робот

Использование "double" в качестве счетных переменных в циклах

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

Вы также можете использовать плавающую точку значение как счетчик циклов. Здесь пример цикла 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. Таким образом, второе управление контуром выражение всегда ложно, а цикл продолжается бесконечно.

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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

Ответ 3

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

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

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 
}

Ответ 4

Числа с плавающей запятой представлены внутренне как двоичное число, почти всегда в формате 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.1111111111111111111111100 b)

В любом случае, если ваше условие цикла равно 1.00000000000000000000001 b == 1.00000000000000000000000 b, он не будет завершен. Если вы используете <= вместо этого, возможно, он запустит дополнительное время, если значение находится под последним значением, но оно остановится.

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

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

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

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

Ответ 5

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

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

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

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

  • Следовательно, можно точно проверить сумму многих 0.25 (или 1.0), но невозможно точно совместить даже один 0.2.

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

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

Ответ 6

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

// 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);