Листинг 3.16 показывает, как можно найти это число (пример Epsilon на компакт-диске).
Листинг 3.16. Поиск машинного эпсилон
procedure TForm1.Button1Click(Sender: TObject);
var
R: Extended;
I: Integer;
begin
R := 1;
while 1 + R/2 > 1 do R := R / 2;
Label1.Caption := FloatToStr(R);
end;
Запустив этот код, мы получим на экране 1.0842021724855Е-19 в полном соответствии с нашими теоретическими выкладками.
Примечание
В тех системах, где наблюдается описанная проблема с уменьшением точности, программа выдаст 2.22044604925031Е-16. Если вы увидели у себя это число, добавьте
код, который переведет FPU в режим максимальной точности.
А теперь изменим тип переменной
R
с
Extended
на
Double
. Результат не изменится. На
Single
— опять не изменится. Но такое поведение лишь на первый взгляд может показаться странным. Давайте подробнее рассмотрим выражение
1 + R / 2 > 1
. Итак, все вычисления (в том числе и сравнение) сопроцессор выполняет с данными типа
Extended
. Последовательность действий такова: число
R
загружается в регистр сопроцессора, преобразуясь при этом к типу
Extended
. Дальше оно делится на 2, а затем к результату прибавляется 1, и все это в
Extended
, никакого обратного преобразования в Single или
Double
не происходит. Затем это число сравнивается с единицей. Очевидно, что результат сравнения не должен зависеть от исходного типа
R
, т.к. диапазона даже типа
Single
вполне хватает, чтобы разместить машинное эпсилон.
3.2.13. Методы решения проблем
Подведем итоги сказанному. Значения, которые мы получаем, могут отличаться от ожидаемых, даже если речь идет о простом присваивании. Во многих случаях (например, в научных расчетах) это несущественно, т.к. сам метод расчета дает еще большую погрешность. Проблемы начинаются там, где мы хотим вывести число на экран или сравнить его с другим. Универсальных рецептов на все случаи жизни не существует, но во многих ситуациях помогают следующие советы:
□ Если ваша задача — просто получить "красивое" представление числа на экране, то функцию
FloatToStr
заменяйте на ее более мощный аналог
FloatToStrF
или на функцию
Format
— они позволяют указать желаемое количество символов после точки.
□ Сравнение вещественных чисел следует выполнять с учетом погрешности, т.е. вместо
if а = b
… писать
if Abs(а - b) < Ерs
…, где
Eps
— некоторая величина, задающая допустимую погрешность (в модуле Math, начиная с Delphi 6, существует функция
SameValue
, с помощью которой это же условие можно записать как
if SameValue(a, b, Eps)
…).
□ Для денежных расчетов следует выбирать тип
Currency
, реализующий число с фиксированной, а не плавающей, десятичной точкой. Отметим также, что не следует пытаться решить проблему неточного представления числа (0,100000001490116 вместо 0,1) с помощью функции
RoundTo
, поскольку эта функция не может обеспечить точность бо́льшую, чем точность аппаратного представления вещественных чисел.
3.3. Тонкости работы со строками
В этом разделе мы рассмотрим некоторые тонкости работы со строками, которые позволяют лучше понять, какой код генерирует компилятор при некоторых, казалось бы, элементарных действиях. Не все приведенные здесь примеры работают не так, как можно было бы ожидать, так что этот материал немного выходит за рамки главы. Но "подводные камни" здесь мы тоже встретим.
3.3.1. Виды строк в Delphi
Для работы с кодировкой ANSI в Delphi существует три вида строк:
AnsiString
,
ShortString
и
PChar
. Различие между ними заключается в способе хранения строки, а также выделения и освобождения памяти для нее. Зарезервированное слово
string
по умолчанию означает тип
AnsiString
, но если после нее следует число в квадратных скобках, то это означает тип
ShortString
, а число — ограничение по длине. Кроме того, существует опция компилятора Huge strings (управляется также директивами компилятора
{$H+/-}
и
{$LONGSTRINGS ON/OFF}
, которая по умолчанию включена, но если ее выключить, то слово
string
станет эквивалентно
ShortString
; или, что то же самое,
string[255]
. Эта опция введена для обратной совместимости с Turbo Pascal, в новых программах отключать ее нет нужды. Внутреннее устройство этих типов данных иллюстрирует рис. 3.2.
Рис. 3.2.
Устройство различных строковых типов Delphi
Наиболее просто устроен тип
ShortString
. Это массив символов с индексами от 0 до N, где N — число символов, указанное при объявлении переменной (в случае использования идентификатора
ShortString
N явно не указывается и равно 255). Нулевой элемент массива хранит текущую длину строки, которая может быть меньше или равна объявленной (эту длину мы будем далее обозначать M), элементы с индексами от 1 до M — это символы, составляющие строку. Значения элементов с индексами M+1..N не определены. Все стандартные функции для работы со строками игнорируют эти символы. В памяти такая переменная всегда занимает N+1 байтов.
Ограничения типа
ShortString
очевидны: на хранение длины отводится только один байт, поэтому такая строка не может содержать больше 255 символов. Кроме того, такой способ записи длины не совпадает с принятым в Windows, поэтому
ShortString
несовместим с системными строками.
В системе приняты так называемые нуль-терминированные строки: строка передается указателем на ее первый символ, длина строки отдельно нигде не хранится, признаком конца строки считается встретившийся в цепочке символов
#0
. Длина таких строк ограничена только доступной памятью и способом адресации (т.е. в Windows теоретически это 4 294 967 295 символов). Для работы с такими строками предусмотрен тип
PChar
. Переменная такого типа является указателем на начало строки. В литературе нередко можно встретить утверждение, что
PChar = ^Сhar
, однако это неверно: тип
PChar
встроен в компилятор и не выводится из других типов. Это позволяет выполнять с ним операции, недопустимые для других указателей. Во-первых, если
P
— переменная типа
PChar
, то допустимо обращение к отдельным символам строки с помощью конструкции
P[N]
, где
N
— целочисленное выражение, определяющее номер символа (в отличие от типа
ShortString
, здесь символы нумеруются с 0, а не с 1). Во-вторых, к указателям типа
PChar
разрешено добавлять и вычитать целые числа, смещая указатель на соответствующее число байтов вверх или вниз (здесь речь идет только об операторах "+" и "-"; адресная арифметика с помощью процедур
Inc
и
Dec доступна для любых типизированных указателей, а не только для
PChar
).
При работе с
PChar
программист целиком и полностью отвечает за выделение памяти для строки и за ее освобождение. Именно это и служит основным источником ошибок у новичков: они пытаются работать с такими строками так же, как и с
AnsiString
, надеясь, что операции с памятью будут выполнены автоматически. Это очень грубая ошибка, способная привести к самым непредсказуемым последствиям.
Хотя программист имеет полную свободу выбора в том, как именно выделять и освобождать память для нуль-терминированных строк, в большинстве случаев самыми удобными оказываются специально предназначенные для этого функции
StrNew
,
StrDispose
и т.п. Их преимущество заключается в том, что менеджер памяти выделяет чуть больше места, чем требуется для хранения строки, и в эту дополнительную память записывается, сколько байтов было выделено. Благодаря этому функция
StrDispose
удаляет ровно столько памяти, сколько было выделено, даже если в середину выделенного блока был записан символ
#0
, уменьшающий длину строки.
Компилятор также позволяет рассматривать статические массивы типа
Char
, начинающиеся с нулевого индекса, как нуль-терминированные строки. Такие массивы совместимы с типом
PChar
, что позволяет обойтись без использования динамической памяти при работе со строками.
Тип
AnsiString
объединяет достоинства типов
ShortString
и
PChar
: строки имеют фактически неограниченную длину, заботиться о выделении памяти для них не нужно, в их конец автоматически добавляется символ
#0
, что делает их совместимыми с системными строками (впрочем, эта совместимость не абсолютная; как и когда можно использовать
AnsiString
в функциях API, мы рассматривали в разд. 1.1.13.).