А. Григорьев - О чём не пишут в книгах по Delphi
□ Для денежных расчетов следует выбирать тип 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.).
Переменная типа AnsiString — это указатель на первый символ строки, как и в случае PChar. Разница в том, что перед этой строкой в память записывается дополнительная информация: длина строки и счетчик ссылок. Это позволяет компилятору генерировать код, автоматически выделяющий, перераспределявший и освобождающий память, выделяемую для строки. Работа с памятью происходит совершенно прозрачно для программиста, в большинстве случаев со строками AnsiString можно работать, вообще не задумываясь об их внутреннем устройстве. Символы в таких строках нумеруются с единицы, чтобы облегчить перенос старых программ, использовавших строки типа ShortString.
Счетчик ссылок позволяет реализовать то, что называется copy-on-demand, копирование по необходимости. Если у нас есть две переменные S1, S2 типа AnsiString, присваивание вида S1 := S2 не приводит к копированию всей строки. Вместо этого в указатель S1 копируется значение указателя S2, а счетчик ссылок строки увеличивается на единицу. В дальнейшем, если одну из этих строк потребуется модифицировать, она сначала будет скопирована (а счетчик ссылок оригинала, естественно, уменьшен) и только потом изменена, чтобы это не затрагивало остальные переменные.
Далее мы рассмотрим, какие проблемы могут возникнуть при использовании строк разного вида.
3.3.2. Хранение строковых литералов
Литералами называются значения, записываемые в программе буквально. В частности, строковые литералы в Delphi — это последовательности символов, заключенных в кавычки или записанных в виде ANSI-кодов с использованием префикса #.
Когда в программе встречается строковый литерал, компилятор должен поместить его в какую-либо область памяти, чтобы это значение стало доступным программе. Компилятор Delphi размещает строковые литералы в сегменте кода, в участках, управление которым никогда не передается. В данном разделе мы рассмотрим, к каким последствиям это может привести.
Положим на форму пять кнопок и напишем следующие обработчики для нажатия на них (листинг 3.17, пример Constants на компакт-диске).
Листинг 3.17. Примеры работы со строковыми литераламиprocedure TForm1.Button1Click(Sender: TObject);
var
P: PChar;
begin
P := 'Xest';
P[0] := 'T'; { * }
Label1.Caption := P;
end;
procedure TForm1.Buttom2Click(Sender: TObject);
var
S: string;
P: PChar;
begin
S:= 'Xest';
P := PChar(S);
P[0] := 'T'; { * }
Label1.Caption := P;
end;
procedure TForm1.Button3Click(Sender: TObject);
var
S: string;
begin
S := 'Xest';
S[1] := 'T';
Label1.Caption := S;
end;
procedure TForm1.Button4Click(Sender: TObject);