этом примере только нажатие на третью и четвертую кнопку приводит к появлению надписи Test. Первые два обработчика вызывают исключение Access violation в строках, отмеченных звездочками, а при нажатии пятой кнопки программа обычно работает без исключении (хотя в некоторых случаях оно все же может возникнуть), но к слову "Test" добавляется какой-то мусор. Разберемся, почему так происходит.
Встретив в первом обработчике литерал
'Xest'
и определив, что он относится к типу
PChar
, компилятор выделяет в подходящей области сегмента кода пять байтов (четыре значащих символа и один завершающий ноль), а в указатель
P
заносится адрес этого литерала. Сегмент кода доступен только для чтения, прав на его изменение система программе в целях безопасности не дает, поэтому попытка изменить то, что находится в этом сегменте, приводит к закономерному результату — выдаче сообщения "Access violation".
В обработчике второй кнопки происходит почти то же самое, с той лишь разницей. что для литерала выделяется на восемь байтов больше: т.к. в данном случае литерал имеет тип
AnsiString
, ему нужны еще 4 байта для хранения длины и 4 — для счетчика ссылок. В переменную
S
записывается указатель на этот литерал. Приводя эту переменную к типу
PChar
, мы, по сути, просто копируем этот указатель в переменную
P
, а дальше происходит то же самое — попытка изменить страницу памяти, доступную программе только для чтения с тем же самым результатом.
В третьем случае литерал, как и раньше, размещается в сегменте кода. Счетчик ссылок у таких литералов всегда равен -1 — это значение указывает менеджеру памяти, что это константа, которая не может быть изменена и память для которой не нужно освобождать. Поэтому при любой попытке изменить переменную, которой присвоен литерал, срабатывает механизм копирования по необходимости: для строки выделяется место в динамической памяти, затем значение литерала копируется в эту область, обновляется значение указателя
S
, а затем выполняется изменение копии, находящейся в динамической памяти. Так как эта память доступна и для чтения, и для записи, исключение не возникает, и все работает так, как и было задумано.
В четвертом случае литерал также хранится в сегменте кода, но работы с указателем уже нет. Этот литерал занимает там пять байтов: один байт на длину и четыре — на символы. Переменная
S
размешается в стеке, занимая там 256 байтов, а присваивание ей литерала — это копирование значения литерала из сегмента кода в область памяти, занятую переменной. Таким образом, в дальнейшем мы работаем не с константой в сегменте кода, а с ее копией в стеке, которую можно без проблем модифицировать.
В пятом случае мы получаем указатель на этот участок стека. Обратите внимание, что приведение типов в данном случае не работает: для записи в
P
адреса первого символа строки приходится использовать оператор получения адреса
@
. Модификация
строки проходит, как и в предыдущем случае, успешно, но при присваивании выражения типа
PChar
свойству типа
AnsiString
длина строки определяется по правилам, принятым для
PChar
, т.е. строка сканируется до обнаружения нулевого символа. Но
поскольку
ShortString "не отвечает" за то, что будет содержаться в неиспользуемых символах, там может остаться всякий мусор от предыдущего использования стека. Никакой гарантии, что сразу после последнего символа будет
#0
, нет. Отсюда и появление непонятных символов на экране.
Общий вывод таков: пока мы не вмешиваемся в работу компилятора с типами
ShortString
и
AnsiString
, получаем ожидаемый результат. Работа с этими же строками через
PChar
в обход стандартных механизмов приводит к появлению проблем. Кроме того, при работе со строками
PChar
необходимо четко представлять, где и как выделяется для них память, иначе можно получить неожиданную ошибку.
3.3.3. Приведение литералов к типу PChar
В разд. 1.1.13 мы уже говорили, что когда у функции есть параметр типа
PChar
, и этот параметр не будет изменяться функцией, при вызове ей можно передавать строковый литерал (см. листинг 1.20). Компилятор размещает литерал в сегменте кода и передает функции указатель на эту память.
В примерах кода, приведенных на различных сайтах, можно нередко встретить такую ситуацию, когда литерал, передаваемый в качестве параметра типа
PChar
, явно приводится к этому типу. Разберемся, что это дает. Для этого положим на форму четыре кнопки и напишем в обработчиках их нажатия следующий код (листинг 3.18. пример
PCharLit
на компакт-диске).
Листинг 3.18. Приведение литералов к типу
PChar
procedure TForm1.Button1Click(Sender: TObject);
begin
Application.MessageBox('Text', nil, 0);
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
Application.MessageBox('A', nil, 0);
end;
procedure TForm1.Button3Click(Sender: TObject);
begin
Application.MessageBox(PChar('Text'), nil, 0);
end;
procedure TForm1.Button4Click(Sender: TObject);
begin
Application.MessageBox(PChar('A'), nil, 0);
end;
Метод
TApplication.MessageBox
по каким-то непонятным причинам имеет параметры типа
PChar
вместо
string
, и мы этим воспользуемся. При его вызове будет показано диалоговое окно с текстом, переданным в качестве первого параметра (в заголовке будет написано Ошибка, т.к. второй параметр у нас
nil
). Нажатие на первую и вторую кнопку не приводит ни к каким неожиданностям — мы видим на экране Text и А соответственно. Теперь перейдем к коду с явным приведением литерала к
PChar
. Нажатие на третью кнопку к сюрпризам не приведет, а вот нажатие на четвертую даст исключение Access violation.