Теперь попробуем сравнить значение переменной и константы, которую мы ей присвоили (листинг 3.10, пример Compare1 на компакт-диске).
Листинг 3.10. Пример ошибки при сравнении вещественной переменной и константы
procedure TForm1.Button1Click(Sender: TObject);
var
R: Single;
begin
R := 0.1;
if R = 0.1 then Label1.Caption := 'Равно'
else Label1.Caption := 'He равно';
end;
При нажатии кнопки мы увидим надпись Не равно. На первый взгляд это кажется абсурдом. Действительно, мы уже знаем, что переменная R
получает значение 0.100000001490116 вместо 0.1. Но ведь "0.1" в правой части равенства тоже должно преобразоваться по тем же законам, т.к. работает аналогичный алгоритм. Тут самое время вспомнить, что FPU работает только с 10-байтным типом Extended
, поэтому и левая, и правая часть равенства сначала преобразуется в этот тип, и лишь потом производится сравнение. То число, которое оказалось в переменной R
вместо 0.1, хотя и выглядит страшно, но зато представляется в виде конечной двоичной дроби. Информация же о том, что это на самом деле должно означать "0.1", нигде не сохранилась. При преобразовании этого числа в Extended
младшие, избыточные по сравнению с типом Single
разряды мантиссы просто заполняются нулями, и мы снова получим то же самое число, только записанное в формате Extended
. А "0.1" из правой части равенства преобразуется в Extended
без промежуточного превращения в Single
. Поэтому некоторые из младших разрядов мантиссы будут содержать единицы. Другими словами, мы получим хоть и не точное представление числа 0.1, но все же более близкое к истине, чем 0.100000001490116.
Из-за таких хитрых преобразований оказывается, что мы сравниваем два близких, но все же не равных числа. Отсюда — закономерный результат в виде надписи Не равно.
Тут уместна аналогия с десятичными дробями. Допустим, в одном случае мы делим 1 на три с точностью до трех знаков и получаем 0,333. Потом мы делим 1 на три с точностью до четырех знаков и получаем 0,3333. Теперь мы хотим сравнить эти два числа. Для этого приводим их к точности в четыре разряда. Получается, что мы сравниваем 0,3330 и 0,3333. Очевидно, что это разные числа.
Если попробовать заменить число 0,1 на 0,5, то мы увидим надпись Равно. Полагаем, что читатели уже догадались, почему, но все же приведем объяснение. Число 0,5 — это конечная двоичная дробь. При прямом приведении ее к типу Extended
в младших разрядах оказываются нули. Точно такие же нули оказываются в этих разрядах при превращении числа 0,5 типа Single
в тип Extended
. Поэтому в результате мы сравниваем два равных числа. Это похоже на процесс деления 1 на 4 с точностью до трех и до четырех значащих цифр. В первом случае получили бы 0,250, во втором — 0,2500. Приведя оба значения к точности в четыре знака, получим сравнение 0,2500 и 0,2500. Очевидно, что эти числа равны.
3.2.8. Сравнение разных типов
Теперь попытаемся сравнить переменную не с константой, а с другой переменной (листинг 3.11, пример Compare2 на компакт-диске).
Листинг 3.11. Пример ошибки при сравнении переменных разных типов
procedure TForm1.Button1Click(Sender: TObject);
var
R1: Single;
R2: Double;
begin
R1 := 0.1;
R2 := 0.1;
if R1 = R2 then Label1.Caption := 'Равно'
else Label1.Caption := 'He равно';
end;
Почему этот пример также выдаст Не равно, понять проще, чем в предыдущем случае. При R1
бесконечная дробь обрывается на 24-х разрядах, а при R2
— на 53-х. Таким образом, в дополнительных по сравнению с типом Single
разрядах переменной R2
будут единицы. При дополнении значений нулями до 10-байтной точности мы получим разные числа, что и определяет результат сравнения. Это напоминает ситуацию, когда мы сравниваем 0,333 и 0,3333, приводя их к точности в пять знаков: числа 0,33300 и 0,33330 не равны.
Как и в предыдущем случае, замена 0,1 на 0,5 даст результат Равно.
Рассмотрим еще один пример, иллюстрирующий ситуацию, которая часто озадачивает начинающего программиста (листинг 3.12, пример Subtraction на компакт-диске).
Листинг 3.12. Накапливание ошибки при вычитании
procedure TForm1.Button1Click(Sender: TObject);
var
R: Single;
I: Integer;
begin
R := 1;
for I := 1 to 10 do R := R - 0.1;
Label1.Caption := FloatToStr(R);
end;
В результате выполнения этого кода на экране появится -7.3015691270939E-8 вместо ожидаемого нуля. Объяснение этому достаточно очевидно, если вспомнить то, о чем мы говорили ранее. Число 0,1 не может быть передано точно ни в одном из вещественных типов, а при каждом вычислении происходит преобразование Single
в Extended
и обратно, причем последнее — с потерей точности. Эти потери приводят к тому, что мы получаем в результате не ноль, а "почти ноль".
Читать дальше
Конец ознакомительного отрывка
Купить книгу