procedure TForm1.Button1Click(Sender: TObject);
var
CI: TWndClass;
S: string;
procedure DoGetClassInfo;
begin
GetClassInfo(hInstance, PChar('TForm' + IntToStr(1)), CI);
end;
begin
DoGetClassInfo;
S:= 'abcde' + IntToStr(2);
Label1.Caption:= CI.lpszClassName;
end;
Что будет выведено на экран в результате выполнения этого кода? Так как класс называется "TForm1", логично предположить, что именно это и будет выведено. На самом деле мы увидим abcde2 — ту строку, которая присвоена переменной S
.
Разберемся, как значение переменной S
оказывается в поле CI.lpszClassName
. Согласно MSDN поле lpszClassName
имеет тип LPCTSTR(PChar)
, и в него функция GetClassInfo
заносит указатель на строку, содержащую имя оконного класса. Но нигде не сказано, в какой области памяти должна располагаться эта строка.
Функция GetClassInfo
поступает очень просто, но не совсем корректно: один из ее аргументов — указатель на строку с именем класса. Именно его функция и помещает в lpszClassName
.
В приведенном примере в качестве аргумента GetClassInfo
передаётся выражение типа string
, приведенное к PChar
, которое не может быть вычислено на этапе компиляции, поэтому компилятор генерирует код, вычисляющий данное выражение. Этот код размещает вычисленное выражение в динамической памяти, и в GetClassInfo
передаётся указатель на эту строку.
Все строковые выражения, вычисленные подобным образом, должны удаляться из памяти, чтобы не было утечек. Компилятор помещает код, освобождающий эту память, в эпилог той функции, в которой встретилось выражение. В данном случае — в эпилог локальной процедуры DoGetClassInfo
.
Освободившуюся память менеджер памяти не сразу возвращает системе придерживает, чтобы иметь возможность быстрее выделить память при следующем запросе. Таким образом, после завершения работы DoGetClassInfo
память, в которой хранится вычисленное имя оконного класса (и на которую указывает CI.lpszClassName
), по-прежнему принадлежит процессу, но менеджер памяти полагает ее свободной и считает себя вправе использовать ее по своему усмотрению.
Когда присваивается значение переменной S
, для размещения новой строки менеджер памяти выделяет ту самую область, в которой ранее хранилось имя класса. Так как CI.lpszClassName
по-прежнему содержит этот адрес, обращение к этому полю возвращает новую строку, которая присвоена переменной S
.
В Delphi до 7-й версии включительно описанный эффект наблюдается при любой длине строки, присваиваемой переменной S
, в более новых версиях Delphi — только в том случае, если длина этой строки находится в пределах от 5 до 11 символов. Это связано с тем, что новый менеджер памяти, появившийся в этих версиях Delphi, с целью уменьшения фрагментации разбивает кучу на несколько областей, в каждой из которых выделяет блоки памяти, укладывающиеся в соответствующий данной области диапазон размеров блоков. Если строка, присваиваемая переменной S
, слишком сильно отличается по размеру от 'TForm1'
для этой строки выделяется память в другой области, и подмены не происходит.
Если в данном примере не выносить вызов функции GetClassInfo
в отдельную процедуру DoGetClassInfo
, а вызывать ее напрямую из Button1Click
, описанного эффекта не будет, потому что в этом случае освобождение памяти, занятой для вычисленного имени класса, будет производиться в эпилоге Button1Click
, и на момент присваивания значения переменной S
эта память будет считаться занятой, поэтому для S
менеджер памяти выделит другую область.
Принципиально и то, что в обоих случаях (в функции GetClassInfo
и при присваивании значения переменной S
) используются не строковые литералы, а выражения, вычисляемые только на этапе выполнения программы. Строковые литералы размещаются компилятором в сегменте кода, и указатели, переданные в GetClassInfo
и присвоенные переменной S
, будут указывать не на динамическую память, а на эти литералы, и подмены не произойдет.