Во второй форме напишем обработчик события OnClose
таким образом, чтобы он устанавливал по закрытию действие caFree
. Добавим поле строкового типа, перекроем конструктор и метод WndProc
так, чтобы окончательный код выглядел следующим образом (листинг 3.52, пример CloseAV на компакт- диске).
Листинг 3.52. Код класса TForm2
type
TForm2 = class(TForm)
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
S: string;
protected
procedure WndProc(var Message: TMessage); override;
public
constructor Create(AOwner: TComponent); override;
end;
....
constructor TForm2.Create(AOwner: TComponent);
begin
S := 'abc';
inherited;
end;
procedure TForm2.WndProc(var Message: TMessage);
begin
inherited;
S[2] := 'x'; { * }
end;
procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := caFree;
end;
Обратите внимание, что в конструкторе сначала присваивается значение полю S
, и лишь потом вызывается унаследованный конструктор. Это сделано потому, что по умолчанию S
содержит пустую строку, т.е. nil
, а уже при вызове унаследованного конструктора окно получит сообщения, для обработки которых будет вызван метод WndProc
. Если в этот момент S
будет по-прежнему nil
, попытка обратиться ко второму символу строки вызовет Access violation. Поэтому еще до начала работы унаследованного конструктора поле S
должно получить подходящее значение.
Запустим программу и попытаемся закрыть второе окно. Возникнет исключение Access Violation: Write of address 00000001. Проблема будет в строке, отмеченной {*}
. При этом любые другие манипуляции с окном никаких исключений вызывать не будут.
При Action = caFree
после завершения работы метода FormClose VCL вызывает метод TCustomForm.Release
. Проблема именно в нем: если попытаться закрыть Form2
с помощью Release
, возникнет то же самое исключение. В справке Release
позиционируется как безопасный способ удаления формы из ее собственного метода. К сожалению, в действительности это не так: реализация этого удаления оставляет желать лучшего и может приводить к попыткам работать с объектом тогда, когда его уже не существует.
При вызове Release
в очередь помещается сообщение CM_RELEASE
, адресатом которого является сама удаляемая форма. В очередном цикле петли сообщений CM_RELEASE
извлекается из очереди и передается на обработку. Так как сообщение адресовано форме, она же его и обрабатывает. Рассмотрим более подробно, как это происходит. (Детально механизм обработки сообщений в VCL описан в разд. 1.1.8 ; мы здесь рассмотрим только ту часть, которая относится к обработке CM_RELEASE
.)
Система передает управление оконной процедуре. Для каждого экземпляра визуального компонента VCL создает свою оконную процедуру с помощью MakeObjectInstance
. Эта процедура вызывает метод объекта MainWndProc
, передающий управление тому методу, на который указывает свойство WindowProc
. По умолчанию это WndProc
. WndProc
не обрабатывает CM_RELEASE
самостоятельно, а передает его методу Dispatch
. Dispatch
пытается найти для этого сообщения специальный обработчик (метод с директивой message
) и, т.к. в TCustomForm
такой обработчик описан (он называется CMRelease
), передаёт управление ему.
И здесь начинается самое интересное. CMRealease
просто вызывает Free
, удаляя тем самым объект, т.е. объект удаляется из метода самого объекта, что делать запрещено. Таким образом, после выполнения Free
управление вновь получает CMRealease
. Из него управление возвращается в Dispatch
, оттуда — в WndProc
, затем — в MainWndProc
, далее — в оконную процедуру, и только после этого управление получает код, который никак не связан с конкретным экземпляром компонента. Мы видим, что после обработки CM_RELEASE
и удаления объекта его методы продолжают работать. Методы уже не существующего объекта!
В принципе, методы несуществующего объекта могут вполне нормально завершить свою работу, если не будут обращаться к его полям или иным образом использовать указатель Self
, который к этому моменту будет уже недействительным. Но стоило нам только вставить в один из этих методов код, задействующий поле объекта, как возникла ошибка.
В данном примере получается следующее: сначала CM_RELEASE
передаётся стандартному обработчику, который вызывает деструктор. При работе деструктора финализируются все поля объекта, для которых это требуется. В нашем случае это означает, что в поле S
заносится nil
(освобождения памяти при этом не происходит, потому что S
до этого ссылалась на литерал, хранящийся в кодовом сегменте, а не в динамической памяти). После этого начинает работать наш код, который пытается изменить второй символ в строке. Программа пытается обратиться к ячейке с адресом nil
+ 1, т.е. 00000001, что и приводит к ошибке Access violation.
Читать дальше
Конец ознакомительного отрывка
Купить книгу