Антон Григорьев дата публикации 19-09-2005 08:29 КАТЕГОРИЯ | | СИСТЕМА .WIN32.Перетирание имени оконного класса, возвращаемого GetClassInfo | ПРОДУКТ | | Delphi | ПЛАТФОРМА | | Windows |
Создадим новый проект в Delphi, поместим на форму кнопку Button1 и создадим ей следующий обработчик OnClick:
procedure TForm1.Button1Click(Sender: TObject);
var CI:TWndClass;
S:string;
procedure DoGetClassInfo;
begin
GetClassInfo(hInstance,PChar('TForm'+IntToStr(1)),CI)
end;
begin
DoGetClassInfo;
S:='X'+IntToStr(2);
Button1.Caption:=CI.lpszClassName
end;
Какой заголовок будет у кнопки после выполнения этого кода? Так как класс называется 'TForm1', логично предположить, что именно такой заголовок и получит кнопка. На самом деле заголовок будет 'X2' – а это та строка, которая присвоена переменной S.
Разберёмся, как значение переменной S оказывается в поле CI.lpszClassName.
Согласно MSDN поле lpszClassName имеет тип LPCTSTR (PChar), и в него функция GetClassInfo заносит указатель на строку, содержащую имя оконного класса. Но нигде не сказано, в какой области памяти должна располагаться эта строка.
Функция GetClassInfo поступает очень просто, но не совсем корректно: один из её аргументов – указатель на строку с именем класса. Именно этот указатель функция и помещает в lpszClassName.
В приведённом примере в качестве аргумента GetClassInfo передаётся выражение типа string, приведённое к PChar. Это выражение не может быть вычислено на этапе компиляции, поэтому компилятор генерирует код, который его вычисляет. Этот код размещает вычисленное выражение в динамической памяти, и в GetClassInfo передаётся указатель на эту строку.
Все строковые выражения, вычисленные подобным образом, должны удаляться из памяти, чтобы не было утечек. Компилятор помещает код, освобождающий эту память, в эпилог той функции, в которой встретилось выражение. В данном случае – в эпилог процедуры DoGetClassInfo.
Освободившуюся память менеджер памяти не сразу возвращает системе, а придерживает, чтобы иметь возможность быстрее выделить память при следующем запросе. Таким образом, после завершения работы DoGetClassInfo память, в которой хранится вычисленное имя оконного класса (и на которую указывает CI.lpszClassName), по-прежнему принадлежит процессу, но менеджер памяти полагает её свободной и считает себя вправе использовать её по своему усмотрению.
Когда присваивается значение переменной S, для размещения новой строки менеджер памяти выделяет ту самую область, которая была ранее использована для хранения имени класса. Так как CI.lpszClassName по-прежнему содержит этот адрес, обращение к этому полю возвращает новую строку, которая присвоена переменной S.
Если в данном примере не выносить вызов функции GetClassInfo в отдельную процедуру DoGetClassInfo, а вызывать её напрямую из Button1Click, описанного эффекта не будет, потому что в этом случае освобождение памяти, занятой для вычисленного имени класса, будет производиться в эпилоге Button1Click, и на момент присваивания значения переменной S эта память будет считаться занятой, поэтому для S менеджер памяти выделит другую область.
Принципиальным является и то, что в обоих случаях (в функции GetClassInfo и при присваивании значения переменной S) используются не строковые константы, а выражения, вычисляемые только на этапе выполнения программы. Строковые константы размещаются компилятором в сегменте кода, и указатели, переданные в GetClassInfo и присвоенные переменной S, будут указывать не на динамическую память, а на эти константы, и перетирания не произойдёт.
Программа протестирована в Delphi 3, 4, 5, 6 и 7 в Windows 95 OSR2, Windows 2000 и Windows XP – везде наблюдался описанный эффект.
Избежать проблемы можно двумя способами. Во-первых, не следует передавать значение поля lpszClassName за пределы той функции, в которой была вызвана функция GetClassInfo. Во-вторых, имя оконного класса должно быть известно программе до вызова GetClassInfo. Лучше использовать ту строку, в которой хранится это имя, чем поле lpszClassName.
В процедуре DoGetClassInfo мы передаем в GetClassInfo указатель на строку как временный, а назад получаем его как постоянный — на время жизни структуры TWndClass. Этот тонкий момент и есть вся соль данного камушка.
Проблема не зависит ни от версии Delphi, ни вообще от языка. Она существует для всех функций API, возвращающих указатель на память, предоставленную вызывающей программой. Если программист повторно воспользовался буферной переменной, то указатель будет указывать уже не на то, что ожидалось. В случае с данным камушком все это (выделение и использование буферной памяти) происходит неявно для программиста, и потому легко пропускаемо через фильтр разума.
[TForm] [Окна, оконные сообщения]
Обсуждение материала [ 02-03-2008 10:02 ] 4 сообщения |