Чтение онлайн

ЖАНРЫ

О чём не пишут в книгах по Delphi

Григорьев Антон Борисович

Шрифт:

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" добавляется какой-то мусор. Разберемся, почему так происходит.

Встретив в первом обработчике литерал

'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.

Поделиться с друзьями: