СТАТЬЯ 27.07.01

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

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

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

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

Данная публикация является первой в цикле статей, посвященном профессиональной разработке приложений с помощью Delphi 5. Статьи цикла будут содержать базовые сведения, необходимые для создания и отладки приложений, отвечающих современным требованиям. Они представляют интерес для любого программиста — независимо от области применения создаваемых приложений.

Объектно-ориентированное программирование (ООП) – это основа для создания приложений в Windows. В 80-х годах материалов, посвященных объектно-ориентированному программированию, публиковалось довольно много. Несмотря на то что за прошедшее время в синтаксисе объектно-ориентированных языков программирования произошли существенные изменения и появились новые возможности, авторы современных учебных пособий почему-то не уделяют этому вопросу должного внимания. Мы решили восполнить этот пробел. В данной публикации в первую очередь рассматривается то новое, что появилось в Delphi по мере совершенствования этого продукта.

Материалы, изложенные в настоящем разделе (если это особо не оговаривается) применимы как к Delphi, так и к C++Builder (за исключением синтаксиса).

Иерархия классов

Современное название объекта – класс; термин «объект» иногда используется для обозначения рабочей копии (экземпляра) класса.

Класс можно вкратце определить как запись record, к которой добавлены методы для работы с переменными внутри этой записи. Однако в отличие от записей и методов, объявленных вне класса, класс обладает рядом дополнительных возможностей.

Каждому классу, за исключением самого первого, должен предшествовать класс-родитель. В свою очередь, любой класс можно использовать для создания других классов, и он в этом случае будет являться их родителем. В Delphi у класса бывает только один родитель, в C++ родителей может быть несколько. Поэтому в Delphi классы образуют иерархическое дерево с классом TObject в роли корня. Иерархию классов в Delphi можно проследить при вызове команды View/Browser после успешной компиляции какого-либо проекта.

В C++ такое дерево построить невозможно: отдельные его ветви будут сливаться.

Иерархическое дерево реализуется довольно просто: при определении в среде-разработке нового класса указывается класс-предок:

TMyListBox=class(TListBox)
end;

Разрешается не указывать класс-предок. В этом случае по умолчанию считается, что предком является TObject. Код вида

TMyClass=class
end;

эквивалентен записи

TMyClass=class(TObject)
end;

Такое объявление однозначно определяет место нового класса в иерархии классов. Кроме того, оно означает следующее: все переменные и все методы класса-предка копируются в новый класс. Простым объявлением TMyListBox=class(TListBox) мы получили новый класс, который обладает всеми свойствами списка: в него можно добавлять строки, он будет показан на форме, при необходимости на нем автоматически появится вертикальная полоса прокрутки и т.д. Таким образом, при продвижении по ветвям иерархического дерева происходит накопление переменных и методов. Например, класс TWinControl имеет все переменные и методы, определенные в классах TControl, TComponent, TPersistent и TObject.

Самый простой класс – TObject — не имеет предка. Он также не имеет полезных при обычном написании приложений методов и переменных. Однако он играет важнейшую роль в поведении объектов.

Объявление переменных и методов класса

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

TMyClass=class(TObject)
public
 FName:string;
 FAge:integer;
end;

В класс TMyClass к переменным класса TObject добавлены две новые переменные: FName и FAge. Названия переменных в классе принято (но не обязательно) начинать с буквы F от слова field. Классовые переменные, определенные внутри класса, отличаются от глобальных (служебное слово var в секции interface или implementation модуля) и локальных (служебное слово var в процедуре или функции) переменных. При загрузке приложения память для глобальных переменных выделяется немедленно и освобождается по завершении приложения. Локальным же переменным память выделяется в стеке при вызове метода, и после завершения работы метода эти ресурсы возвращаются системе.Так, если была объявлена, например, одна глобальная (или локальная) переменная типа N:integer, то резервируется 4 байта памяти, куда можно поместить одно значение. При объявлении же классовых переменных во время загрузки приложения не выделяется память для их хранения – она выделяется только при создании экземпляра класса после вызова конструктора (см. ниже). Поскольку экземпляров класса может быть несколько, в работающем приложении может быть несколько копий классовых переменных (в том числе и нулевое количество). Соответственно в каждой из этих переменных могут храниться различающиеся данные. Этим и определяется отличие классовых переменных от глобальных и локальных – для последних имеется только одна копия. Еще одной интересной особенностью классовых переменных является то, что при создании экземпляра класса они инициализируются нулями (то есть все их биты заполняются нулями). Поэтому если такая переменная представляет собой указатель, то он равен nil, если целое число, то оно равно 0, если логическую переменную, то она равна False. Локальные и глобальные переменные не инициализируются.

Формально объявление метода класса похоже на объявление обычного, не относящегося к классу, метода:

TMyClass=class(TObject)
public
 FName:string;
 FAge:integer;
 procedure DoSomething(K:integer);
 function AddOne(N:integer):integer;
end;

В данном примере к методам TObject добавлены два новых метода – DoSomething и AddOne. Синтаксис Object Pascal разрешает объявлять новые методы только после объявления переменных – приведенный ниже пример вызовет ошибку компиляции:

TMyClass=class(TObject)
public
 FName:string;
 procedure DoSomething(K:integer);
 FAge:integer;
 function AddOne(N:integer):integer;
end;

После объявления какого-либо метода в классе необходимо в секции implementation данного модуля описать его реализацию. Перед заголовком метода следует поместить указание на класс, к которому он относится. Это необходимо делать, поскольку различные классы могут иметь методы с одинаковыми названиями:

…
interface
type
 TMyClass=class(TObject)
 procedure DoSomething(N:integer);
 end;
 TSecondClass=class(TObject)
 procedure DoSomething(N:integer);
 end;
…
implementation
procedure TMyClass.DoSomething(N:integer);
begin
…
end;
procedure TSecondClass.DoSomething(N:integer);
begin
…
end;

Методы, объявленные в классе

Методы, описываемые в классе, подразделяются на классовые методы и другие методы. Приведем примеры их объявления и реализации:

TMyClass=class(TObject)
 class procedure DoSomething(K:integer); 
    {Class method}
 function AddOne(N:integer):integer; {Keywords class 
    absent – means method}
end;
…
implementation
…
 class procedure TMyClass.DoSomething(K:integer);
begin
 …
end;
 function TMyClass.AddOne(N:integer):integer;
begin
…
end;

Обратите внимание, что и в секции реализации используется служебное слово class.

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

Главное отличие обычных методов от классовых заключается в том, что они могут работать с классовыми переменными, то есть с переменными, которые определены в классе (см. выше). Эти методы характеризуются уже двумя адресами: адресом метода и адресом данных. Второй адрес необходим потому, что для обслуживания всех созданных экземпляров класса используется единственная копия метода в памяти. Методы загружаются сразу же после старта приложения. При создании каждого нового экземпляра данного класса он пользуется уже загруженными в память методами. При этом для работы с классовыми переменными каждому методу незаметно для программиста передается ссылка на данные, и таким образом достигается экономия ресурсов: при создании новых экземпляров класса память выделяется только для хранения переменных.

Для работы с методами имеется структура TMethod, определенная в модуле SysUtils:

type
 TMethod = record
 Code, Data: Pointer;
end;

Эта запись позволяет «разобрать» и вновь «собрать» метод класса на две переменные типа Pointer, что бывает полезным для передачи ссылки на метод во внутренний (in-process) сервер автоматизации.

Статические, динамические и виртуальные методы

Каждый из описанных выше методов может быть статическим, динамическим и виртуальным. Они описываются как обычные методы, но с использованием соответствующих служебных директив в среде разработки:

TMyClass=class(TObject)
 function AddOne(N:integer):integer; {Keywords absent 
    – means
 static method}
 procedure Rotate(Angle:single); virtual; {Virtual method}
 procedure Move(Distance:single); dynamic; {Dynamic 
    method}
end;

Эти методы различаются по способу загрузки в память при старте приложения и по способу их вызова. Для вызова статических методов используется его адрес в памяти компьютера – такой же способ, как и для обычных методов. Вызов же динамических и виртуальных методов происходит иначе. Первоначально процесс обращается к таблице виртуальных или динамических методов. Эти таблицы формируются для каждого класса при загрузке приложения. В таблице находится адрес соответствующего метода, по которому передается управление. При этом в таблицах можно подменить адрес метода, в результате чего приложение будет вызывать другой метод так же, как и ранее определенные методы. При вызове одних и тех же методов различных классов может выполняться различный код. Такое поведение классов называется полиморфизмом (об этом подробно рассказывается в следующей главе). Полиморфизм возможен только для динамических и виртуальных методов, но не для статических. Это главное отличие статических методов от остальных.

В классическом объектно-ориентированном программировании требуется наличие виртуальных методов. Динамические методы поддерживают не все объектно-ориентированные языки, но их использование является достаточно эффективным. Для понимания различий между ними рассмотрим создание главной формы какого-либо приложения. При этом разберемся, как в памяти компьютера распределяются три метода формы: DoEnter (динамический метод), CreateWnd (виртуальный) и DoKeyDown (статический). Каждый из этих методов определен на уровне TWinControl.

Иерархия классов, которая ведет от TWinControl к TForm1, следующая:

TWinControl -> TScrollingWinControl -> TCustomForm -> TForm -> 
    TForm1

При этом в памяти компьютера будет размещена единственная копия статического метода DoKeyDown. При создании экземпляра любого из вышеперечисленных классов и вызове из него метода DoKeyDown компилятор генерирует код, который ссылается на адрес этого метода. Таким образом, один экземпляр статического метода обслуживает не только несколько экземпляров класса, в котором он определен, но и все экземпляры его потомков. При этом следует обратить внимание на то, что вызов метода происходит очень быстро: в коде указывается его адрес в ОЗУ.

При динамическом методе DoEnter в памяти компьютера создается единственная его копия, а в таблице динамических методов TWinControl указывается его адрес. В таблицах динамических методов классов TScrollingWinControl…TForm1 в качестве адреса этого метода указывается nil. При вызове этого метода из экземпляра класса TForm1 первоначально происходит поиск этого метода в таблице динамических методов TForm1. Естественно, метод найден не будет, и поиск продолжится уже в таблице динамических методов класса TForm. Так будет продолжаться до тех пор, пока не начнется поиск в таблице динамических методов TWinControl, где будет найден его адрес, по которому будет передано управление процессом. Как и статические, динамические методы требуют мало ресурсов: один экземпляр динамического метода обслуживает как экземпляры класса, где он определен, так и экземпляры всех его потомков. Но вызов метода происходит достаточно долго, поскольку для этого приходится просматривать несколько таблиц. Вызов может замедляться еще и при использовании директивы компилятора {$R+} (проверка диапазона допустимых значений).

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

Таким образом, статические методы вызываются очень быстро, и для их хранения используется минимальное количество системных ресурсов. Но переписать их для того, чтобы заставить класс выполнять другой (или добавочный) код, невозможно. Динамические методы можно переписать, но их вызов требует достаточно длительного времени. Однако при этом они также потребляют мало системных ресурсов. И наконец, виртуальные методы можно переписать, вызов их производится с почти той же самой скоростью, что и вызов статических методов. Но ресурсов они используют достаточно много: число копий виртуальных методов пропорционально числу ветвей в дереве классов, умноженных на их длину. Именно по этой причине классы, имеющие многочисленных потомков, например TControl, практически не имеют виртуальных методов, а обходятся только динамическими.

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

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

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


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