СТАТЬЯ 10.08.01

Предыдущая часть

Профессиональная разработка приложений с помощью Delphi5

Часть 1. Основы объектно-ориентированного программирования

Сергей Трепалин,
УКЦ Interface Ltd.
КомпьютерПресс #1 2001

Статья была опубликована в КомпьютерПресс

Создание экземпляров классов. Конструкторы и деструкторы

Переменная типа класса объявляется в приложении так же, как обычная переменная:

var
 Stream:TMemoryStream;

При таком объявлении резервируется 4 байта памяти для любого класса. Очевидно, что этого явно недостаточно для хранения всех переменных в классе. Размер этой переменной говорит о том, что в ней хранится указатель. Экземпляр (или рабочая копия) класса создается посредством вызова его конструктора:

Stream:=TMemoryStream.Create;

При вызове конструктора класса резервируется память, необходимая для хранения данных в объекте (переменных). Обращаться к переменным и методу класса можно только после вызова конструктора, иначе попытка их чтения/записи приводит к исключительной ситуации. Обращение к переменным и методам экземпляра класса осуществляется с указанием на экземпляр:

procedure TForm1.Button1Click(Sender: TObject);
var
 SL1,SL2:TStringList;
begin
 SL1:=TStringList.Create;
 SL2:=TStringList.Create;
 SL1.Add('String added to SL1 object');
 SL2.Add('String added to SL2 object');
….
end;

Довольно распространенной ошибкой (которая, правда, тут же обнаруживается) является вызов конструктора с попыткой обратиться к несуществующему экземпляру класса:

procedure TForm1.Button1Click(Sender: TObject);
var
 SL:TStringList;
begin
 SL:=nil;
 try
 SL.Create; {Error. Should be 
    typed SL:=TStringList.Create;}
…
 finally
 if Assigned(SL) then SL.Free;
 end;
end;

Данный код компилятор пропускает без каких-либо диагностик, но попытка выполнить его немедленно приводит к возникновению исключения.

Любой класс может иметь несколько конструкторов. Примером может служить класс Exception, имеющий восемь конструкторов. По соглашению имя конструктора содержит слово Create (CreateFmt, CreateFromFile…). Конструкторы могут быть как статическими, так и виртуальными или динамическими. Последние могут быть переписаны – в классах-потомках при необходимости определяется новый конструктор со служебным словом override. Переписывать конструкторы необходимо только для компонентов Delphi и для форм – во всех остальных классах их можно просто добавлять к существующим методам. Необходимость переписывания конструкторов компонентов и форм обусловлена тем, что их вызывает среда разработки. Забытая директива override в компоненте приводит к тому, что при создании формы не выполняется новый конструктор. В большинстве других классов (не потомков TComponent) конструктор вызывается в явном виде из приложения, и поэтому будет вызываться последний написанный конструктор.

Конструктор необходимо переписывать (или создавать новый), когда необходимо изначально (при создании экземпляра класса) изменить значения переменных, или запомнить в переменных параметры, передаваемые в конструкторе, или создать экземпляры классов, объявленных внутри другого класса:

TMyBox=class(TListBox)
private
 FData:TList;
 FNAccel:integer;
public
 constructor Create(AOwner:TComponent); override;
end;
implementation
constructor TMyBox.Create;
begin
 inherited Create(AOwner);
 FData:=TList.Create; {work copy of class creation}
 FNAccel:=5; {zero – by default, changing to five}
 Items.Add('1');
end;

Следует обратить внимание на то, что в конструкторе первым вызывается inherited-метод – конструктор класса-предка и только потом пишется код для инициализации переменных. Это обязательное условие в объектно-ориентированном программировании, которое может нарушаться только в отдельных случаях (примером такого случая является класс TThread). При таком способе записи каждый конструктор предка будет вызывать конструктор своего предка – и так до уровня конструктора класса TObject, который фактически будет первым оператором при вызове конструктора любого класса. Далее происходит выполнение кода в конструкторе класса-потомка и т.д. Для класса TMyBox при обращении к конструктору сначала происходит резервирование памяти для хранения переменных, определенных в данном классе и его предках. Затем вызывается конструктор TObject. Далее происходит обращение к конструктору TComponent, который устанавливает связь экземпляра TMyBox с его владельцем, передаваемым в параметре AOwner. Выполняется код конструктора TCustomListBox, который создает экземпляр класса TStrings и инициализирует ряд переменных. И наконец выполняются операторы, определенные в конструкторе TMyBox. Если оператор inherited поставить последним в конструкторе TMyBox, произойдет исключение при выполнении оператора Items.Add('1') – объект для хранения строк создается в конструкторе класса TCustomListBox, который еще не был вызван.

Понятно, что при создании экземпляра класса резервируются ресурсы операционной системы, и когда экземпляр становится ненужным, эти ресурсы необходимо вернуть обратно посредством вызова деструктора. Деструктор объявлен виртуальным на уровне класса TObject. Деструктор объявляется следующим образом:

TMyBox=class(TListBox)
private
 FData:TList;
 FNAccel:integer;
public
 constructor Create(AOwner:TComponent); override;
 destructor Destroy; override;
end;
implementation
destructor TMyBox.Destroy;
begin
 FData.Free;
 inherited Destroy;
end;

Так будет выглядеть новый деструктор, который мы обязаны объявить в примере, приведенном выше. При вызове деструктора следует ссылаться на конкретный экземпляр класса, который необходимо разрушить:

procedure TForm1.Button1Click(Sender: TObject);
var
 MB:TMyBox;
begin
 MB:=TMyBox.Create(Self); {Class reference 
    TMyBox}
 {....}
 MB.Destroy; 
    {Object instance reference MB}
end;

Для этого примера должен быть создан новый деструктор, так как внутри экземпляра класса TMyBox создается экземпляр класса TList. Соответственно разрушаться они должны совместно.

При переписывании деструктора прежде всего разрушаются экземпляры классов, созданных внутри данного класса, и только после этого вызывается деструктор класса-предка inherited Destroy (отметим, что в конструкторе используется обратный порядок). При таком способе вызова в последнюю очередь будет вызван метод Destroy класса TObject, который вернет системе память, зарезервированную для хранения переменных класса. В примере с классом TMyBox первоначально будет разрушен экземпляр класса TList, ссылка на который содержится в переменной FData. После этого будет вызван деструктор класса TlistBox, в котором разрушается экземпляр класса TStrings. И наконец, будет вызван деструктор класса TObject, где будет освобождена память, зарезервированная для классовых переменных TMyBox.

Вместо прямого вызова деструктора рекомендуется вызывать метод Free, позволяющий проверить, была ли выделена память для разрушаемого экземпляра класса, и если да, то вызывать его деструктор. Использование этого метода важно еще и потому, что деструктор должен быть описан таким образом, чтобы он мог корректно разрушить частично созданный экземпляр класса. Частично созданный экземпляр класса получается в том случае, если в его конструкторе произошло исключение. При этом немедленно вызывается деструктор данного класса, и после его отработки nil-указатель возвращается на создаваемый экземпляр класса. Если, например, в конструкторе резервировалась память под какую-либо переменную (FPBuf):

constructor TMyBox.Create(AOwner:TComponent);
begin
 inherited Create(AOwner);
 FData:=TList.Create;
 GetMem(FPBuf,65500);
end;
destructor TMyBox.Destroy;
begin
 FData.Free;
 FreeMem(FPBuf);
 inherited Destroy;
end;

то исключение может произойти в конструкторе в момент вызова inherited Create или в момент вызова TList.Create — из-за нехватки системных ресурсов. Сразу же будет вызван деструктор, и в момент выполнения оператора FreeMem произойдет генерация еще одного исключения. При этом метод inherited Destroy не будет вызван, а частично созданный экземпляр TMyBox не будет разрушен. Корректная реализация деструктора выглядит так:

if FPBuf<>nil then FreeMem(FPBuf);

При этом в обязательном порядке необходимо проверить, была ли выделена освобождаемая память ранее. Такие проверки необходимо делать со всеми ресурсами, подлежащими освобождению в деструкторе. В противном случае освобождать ресурс лучше в защищенном блоке try…except…end без вызова метода raise в секции except…end. Распространение исключения из деструктора недопустимо (пользователя не должно волновать, что программист не смог корректно высвободить ресурсы!).

Следует отметить, что в случае использования в классе ссылки на какой-либо объект, разрушать его в деструкторе иногда не требуется:

TTest=class(TObject)
private
 FData:TList;
public
 constructor Create(AData:TList);
end;
implementation
constructor TTest.Create(AData:TList);
begin
 inherited Create;
 FData:=AData;
end;

Если сам объект AData будет разрушен в той процедуре, где он создан, то переписывать деструктор класса TTest для разрушения объекта FData не требуется. Повторный вызов деструктора приводит к исключению. При этом применение метода Free не спасает, он лишь проверяет, что ссылка на экземпляр класса не указывает на nil.

В отличие от конструктора, для которого может быть определено несколько методов, деструктор бывает только один. Невозможно представить себе ситуацию, когда в классе может понадобиться дополнительный деструктор. Тем не менее компилятор Delphi позволяет это сделать – а зря… Классы с двумя деструкторами – довольно частое явление на распространяемых компонентах для Delphi третьих фирм. Причиной тому программист, забывший директиву override. Это часто приводит к тому, что ресурсы, освобождением которых занимается деструктор, не освобождаются. Во-первых, метод Free обращается к первому виртуальному методу класса – Destroy. При этом будет честно вызван деструктор класса-предка, но ресурсы, освобождение которых программист старательно описывал в деструкторе с забытой директивой override, освобождены не будут. Во-вторых, при разрушении формы содержащиеся на ней компоненты также разрушаются через вызов первого метода в виртуальной таблице, что ведет к аналогичному результату.

В заключение следует рассмотреть на первый взгляд странный вопрос: а всегда ли следует вызывать деструктор (непосредственно или через метод Free) из кода приложения? Правомерность постановки такого вопроса обусловлена тем, что программист нигде не пишет кодов вызова деструкторов компонентов, помещенных на форму на этапе разработки. Ответ заключается в структуре и реализации деструктора класса TComponent. Любой компонент в конструкторе запоминает ссылку на своего хозяина (AOwner) и заносит себя в список компонентов, которыми владеет хозяин. При вызове деструктора компонента он в первую очередь вызывает деструкторы своих «вассалов», и только после этого вызывается собственный деструктор. Таким образом, нет необходимости вызывать деструктор класса TComponent или его потомка – он будет автоматически разрушен при вызове деструктора его хозяина:

TMyBox=class(TListBox)
 private
 FData:TComponent;
 public
 constructor Create(AOwner:TComponent); 
    override;
 end;
constructor TMyBox.Create(AOwner:TComponent);
begin
 inherited Create(AOwner);
 FData:=TComponent.Create(Self);
end;

В данном случае деструктор для разрушения объекта FData не нуждается в переписывании, поскольку он будет разрушен автоматически при разрушении объекта TMyBox. Деструктор для TComponent (или его потомка) следует вызывать только в случае, если его владелец – nil.

Для всех классов–потомков TComponent не следует в явном виде вызывать деструктор.

Для всех остальных классов необходимо в коде приложения вызывать деструктор.

В Dephi5 появились два новых виртуальных метода – AfterConstruction и BeforeDestruction, — которые вызываются сразу же после конструктора или перед вызовом деструктора соответственно. Можно поспорить насчет необходимости введения метода BeforeDestruction: любой класс имеет виртуальный деструктор, который можно переписать. Появление метода AfterConstruction следует приветствовать, поскольку виртуальный конструктор появляется только на уровне TComponent в иерархии классов VCL. Появление виртуального конструктора существенно облегчило написание приложений для распределенных вычислений. Например, TComObject – базовый класс для реализации интерфейсов в COM-серверах — является потомком TObject и не содержит виртуального конструктора. Экземпляры этого класса создаются в ответ на запрос клиентов, а не командами из кода приложений, что затрудняет выполнение инициализации переменных при создании экземпляра класса. Введение виртуального метода AfterConstruction сделало инициализацию данных в этих классах рутинной процедурой.

Продолжение статьи

Дополнительную информацию Вы можете получить в компании Interface Ltd.

Отправить ссылку на страницу по e-mail
Обсудить на форуме Inprise/Borland


Interface Ltd.
Тel/Fax: +7(095) 105-0049 (многоканальный)
Отправить E-Mail
http://www.interface.ru
Ваши замечания и предложения отправляйте автору
По техническим вопросам обращайтесь к вебмастеру
Документ опубликован: 10.08.01