Тонкости работы со строками в Delphi - Хранение констант

ОГЛАВЛЕНИЕ


Хранение констант

Для работы с этим примером нам понадобится на форму положить пять кнопок и написать следующие обработчики для них (пример Constants):
procedure TForm1.Button1Click(Sender: TObject);
var
  P: PChar;
begin
  P := 'Xest';
  P[0] := 'T'; { * }
  Label1.Caption := P;
end;

procedure TForm1.Button2Click(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);
var
  S: ShortString;
begin
  S := 'Xest';
  S[1] := 'T';
  Label1.Caption := S;
end;

procedure TForm1.Button5Click(Sender: TObject);
var
  S: ShortString;
  P: PChar;
begin
  S := 'Xest';
  P := @S[1];
  P[0] := 'T';
  Label1.Caption := P;
end;
В этом примере только нажатие на третью и четвёртую кнопку приводит к появлению надписи Test., Первые два обработчика вызывают исключение Access violation в строках, отмеченных звёздочками, а при нажатии пятой кнопки программа обычно работает без исключений (хотя в некоторых случаях оно всё же может возникнуть), но к слову "Test" добавляется какой-то мусор. Разберёмся, почему так происходит.

Все строковые константы, встречающиеся в программе, компилятор размещает в сегменте кода, в области, управление которой никогда не передаётся. Встретив в первом обработчике константу 'Test' и определив, что она относится к типу PChar, компилятор выделяет в этой области пять байт (четыре значащих символа и один завершающий ноль), а в указатель P заносится адрес этой константы. Сегмент кода доступен только для чтения, прав на его изменение система программе в целях безопасности не даёт, поэтому попытка изменить то, что находится в этом сегменте, приводит к закономерному результату - Access violation.

В обработчике второй кнопки происходит почти то же самое, с той лишь разницей, что для константы выделяется на восемь байт больше: т.к. в данном случае константа имеет тип AnsiString, ей нужны ещё 4 байта для хранения длины и 4 - для счётчика ссылок. В переменную S записывается указатель на эту константу. Приводя эту переменную к типу PChar, мы, по сути, просто копируем этот указатель в переменную P, а дальше происходит то же самое - попытка изменить страницу памяти, доступную программе только для чтения с тем же самым результатом.

В третьем случае константа, как и раньше, размещается в сегменте кода. Счётчик ссылок у таких констант всегда равен -1 - это значение указывает менеджеру памяти, что это константа, которая не может быть изменена и память для которой не нужно освобождать. Поэтому при любой попытке изменить переменную, которой присвоена такая константа, срабатывает механизм копирования по необходимости: для строки выделяется место в динамической памяти, затем значение константы копируется в эту область, обновляется значение указателя S, а затем выполняется изменение копии, находящейся в динамической памяти. Так как эта память доступна и для чтения, и для записи, исключение не возникает и всё работает так, как и было задумано.

В четвёртом случае константа также хранится в сегменте кода, но работы с указателем уже нет. Эта константа занимает там пять байт - один байт на длину и четыре - на символы. Переменная S размещается в стеке, занимая там 256 байт, а присваивание ей константы - это копирование значения константы в эту область памяти. Таким образом, в дальнейшем мы работаем не с константой в сегменте кода, а с её копией в стеке, которую можно без проблем модифицировать.

В пятом случае мы получаем указатель на этот участок стека. Обратите внимание, что приведение типов в данном случае не работает: для записи в P адреса первого символа строки приходится использовать оператор получения адреса @. Модификация строки проходит, как и в предыдущем случае, успешно, но при присваивании выражения типа PChar свойству типа AnsiString длина строки определяется по правилам, принятым для PChar, т.е. строка сканируется до обнаружения нулевого символа. Но так как ShortString "не отвечает" за то, что будет содержаться в неиспользуемых символах, там может остаться всякий мусор от предыдущего использования стека. Никакой гарантии, что сразу после последнего символа будет #0, нет. Отсюда и появление непонятных символов на экране.

Общий вывод таков: пока мы не вмешиваемся в работу компилятора с типами ShortString и AnsiString, получаем ожидаемый результат. Работа с этими же строками через PChar в обход стандартных механизмов приводит к появлению проблем. Кроме того, при работе со строками PChar необходимо чётко представлять, где и как выделяется для них память, иначе можно получить неожиданную ошибку.