Объектно-ориентированный язык программирования характеризуется тремя основными свойствами:
Инкапсуляция – объединение в одном объекте данных и методов их обработки.
Наследование – создание новых объектов на базе ранее определенных. Новые объекты–потомки сохраняют свойства своих родителей и обладают специфическими свойствами.
Полиморфизм – возможность замещения методов объекта-родителя одноименными методами объекта-потомка.
Основная программа не должна иметь непосредственного доступа к данным объекта, кроме как при помощи методов самого объекта. Методы объекта должны обрабатывать лишь данные своего объекта, не влияя на значение глобальных переменных и не обращаясь к данным другого объекта. Выполнение этих принципов обеспечивает надежность ООП, хорошую защиту от ошибок и возможность использования одних и тех же объектов различными программами.
Преимущества ООП в полной мере проявляются при разработке достаточно сложных программ. Инкапсуляция придает объектам совершенно особое свойство “самостоятельности”, максимальной независимости от остальных частей программы. Правильно сконструированный объект располагает всеми необходимыми данными и процедурами их обработки, чтобы успешно реализовать требуемые от него действия. Попытки использовать ООП для программирования несложных алгоритмических действий выглядят искусственными нагромождениями ненужных языковых конструкций.
Описание объекта
Для описания объекта используется служебное слово Object. Тип объекта описывается следующим образом:
Type
ИмяОбъекта = Object
ПоляДанных;
Заголовки методов;
End;
Описание объекта должно помещаться в разделе описания типов. При описании объекта вначале описываются поля-данные, а затем – методы доступа к этим данным. Сами методы при описании объекта не раскрываются, указывается лишь их заголовок. Описываются процедуры где-то ниже по тексту. Поля данных объекта – это то, что объект “знает”, а методы объекта – это то, что объект “делает”.
Объектом считается либо тип, описывающий сами данные и операции над ними, либо переменная объектного типа, иначе называемая экземпляром объекта.
Var Person: TPerson;
Person – переменная объектного типа или экземпляр объекта.
Существуют две секции объявления методов: Private и Public. Директива Private в описании объекта открывает секцию описания скрытых полей и методов. Перечисленные в этой секции элементы объекта “не видны” программисту, если этот объект он получил в рамках библиотечного TPU‑модуля. Скрываются обычно те поля и методы, к которым программист не должен иметь доступа. Директива Public отменяет действие директивы Private. Все, следующие за Public, элементы объекта доступны в любой программной единице.
Type
NewObject = Object {родитель}
поля; {общедоступные}
методы; {общедоступные}
Private
поля; {частные}
методы; {частные}
Public
поля; {общедоступные}
методы; {общедоступные}
End;
Наследование
Можно определить объект-наследник существующего объекта. В этом случае тип определяется следующим образом:
Type
ИмяОбъектаНаследника = Object (Имя ОбъектаПрародителя)
Новые ПоляОбъектаНаследника;
НовыеМетодыОбъектаНаследника;
End;
Этот процесс, с помощью которого один тип наследует характеристики другого типа, называется наследованием. Наследник называется порожденным (дочерним) типом, а тип, наследующий свои характеристики, называется порождающим (родительским) типом. Тип “наследник” иногда называется производным типом.
Переменным типа объект можно присваивать не только значения этого же типа, но и любого производного от него. Пусть имеются экземпляр Person типа TPerson и экземпляр Student типа TStudent, тогда возможно следующее присваивание:
Person:= Student;
Подобная операция заполнит поля данных Person значениями аналогичных полей, унаследованных Student. Методы таким образом не присваиваются. Поскольку производный тип всегда получается не меньшим, чем прародительский, операция присваивания возможна именно таким путем:
Прародитель Наследник.
При этом гарантируется заполнение всех полей переменной, стоящей слева. В противном случае возникла бы неопределенность с “лишними” полями, присутствующими в переменной справа. Во избежание такой неопределенности запрещено ставить “наследный” тип слева от прародительского.
Инкапсуляция
Объединение в объекте методов и данных называется инкапсуляцией. При работе с объектами необходимо создавать достаточное количество методов, обеспечивающих работу со всеми полями данных, чтобы не возникала необходимость обращаться непосредственно к полям.
TPerson = Object
Name: string[25];
Dolgn: string[25];
Stavka: Real;
Procedure Init (Nm, Dg: String; Sv: Real);
Function GetName: String;
Function GetDolgn: String;
Function GetStavka: Real;
Procedure ShowName;
Procedure ShowDolgn;
Procedure ShowStavka;
end;
TStudent = Object (TPerson)
Ball: Real;
Procedure Init (Nm, Dg: String; Sv, Bl: Real);
Function GetBall: Real;
Function GetSum: Real;
Procedure ShowBall;
Procedure ShowSum;
Procedure ShowAll;
end;
В объекте TStudent описаны четыре поля данных: Name, Dolgn, Stavka – TStudent получил по наследству; поле Ball является уникальным для типа TStudent. Методы ShowName, ShowDolgn, ShowStavka, ShowBall выводят фамилию, дату выплаты, размер ставки и средний балл соответственно. Метод GetSum использует Ball для вычисления cуммы выплат студенту в зависимости от среднего балла и организует вывод данного значения. Процедура ShowAll выводит значение всех полей одновременно и поэтому нет необходимости обращаться непосредственно к полям данных.
Для экземпляра Student типа TStudent возможно такое косвенное обращение к полям:
With Student Do
Begin
Init (‘Петров’, ’Студент’, 100, 4.5);
ShowAll;
End;
Можно обращаться с помощью уточненного имени:
Student. Init (‘Петров’, ’Студент’, 100, 4.5);
Student. ShowAll;
Наличие большого количества методов увеличивает объем программы. Возникает вопрос о необходимости включать в описание объекта метод, который может быть и не использован в программе. Неиспользованные методы не влияют на быстродействие программы и на размер EXE–файла. Компоновщик Турбо Паскаля отбрасывает код метода, который ни разу не вызывается в программе.
Полиморфизм
Под полиморфизмом понимают возможность замещения методами потомка методов родителя с теми же именами и параметрами.
В типе TStudent объявлена процедура Init, которая наследуется от предка, но ее необходимо переопределить, так как инициализируется дополнительное поле Ball:
Procedure TStudent.Init;
Begin
TPerson.Init (Nm, Dg, Sv);
Ball:= Bl;
End;
Вместо того, чтобы непосредственно присваивать значения наследованным полям, таким как Name, Dolgn и Stavka, проще использовать метод инициализации объекта TPerson.
Для описания наследуемых методов может использоваться зарезервированное слово Inherited (унаследованный). В этом случае описание метода TStudent.Init будет выглядеть:
Procedure TStudent.Init;
Begin
Inherited Init (Nm, Dg, Sv);
Ball:= Bl;
End;
Смысл переопределения метода не меняется. Любое изменение в родительском методе автоматически сказывается на методах-потомках.
Для определения другой категории служащих – сотрудников института – введем описание объекта TStaff:
TStaff = Object (TPerson)
Private
Bonus: Real;
Public
Procedure Init (Nm, Dg: String; Sv, Bn: Real);
Function GetSum: Real;
Procedure ShowSum;
Procedure ShowAll;
End;
Предположим, размер выплаты стипендии зависит от среднего балла студента следующим образом:
Function TStudent.GetSum: Real;
Begin
If Ball > 4.6 Then GetSum:= 1.5 * Stavka Else GetSum:= Stavka
End;
Сотрудникам института к размеру ставки прибавляется размер премиальных выплат:
Function TStaff.GetSum: Real;
Begin
GetSum:= Stavka + Bonus
End;
Для преподавателей, чья зарплата зависит от количества выработанных часов, описание типа может быть следующим:
TTeacher = Object (TStaff)
Private
Hours: Word;
HourRate: Real;
Public
Procedure Init (Nm, Dg: String; Sv, Bn, Hrt: Real; Hr: Word);
Function GetSum: Real;
Procedure ShowSum;
Procedure ShowAll;
End;
Метод TTeacher.GetSum вызывает TStaff.GetSum и добавляет к этому значению размер часовой ставки, умноженной на количество часов:
Function TTeacher.GetSum: Real;
Begin
GetSum:= TStaff.GetSum + Hours * HourRate
End;
Методы могут быть переопределены, но поля переопределяться не могут. После того, как поле данных в иерархии объекта определено, никакой дочерний тип не может определить поле данных с таким же именем.
В методах не использовалась явная конструкция With... Do.... Поля данных объекта являются доступными для методов этого объекта. Будучи разделенными в исходной программе, тела методов и поля данных совместно используют одну и ту же область действия.
Статические методы
Описания объектов TPerson, TStudent, TStaff и TTeacher содержат процедуры ShowSum и ShowAll. Для объекта TStudent эта процедура может выглядеть следующим образом:
Procedure TStudent.ShowSum;
Begin
Writeln (GetSum);
End;
Для объекта TТeacher эта процедура имеет тот же самый вид:
Procedure TТeacher.ShowSum;
Begin
Writeln (GetSum);
End;
TТeacher может унаследовать ShowSum от TStaff, а TStaff от TStudent. Поскольку все методы одинаковы, возникает вопрос: нужно ли описывать эти методы для всех объектов? Дело в том, что пока копия метода ShowSum не будет помещена в область действия TТeacher для подавления метода ShowSum объекта TStaff, метод будет работать неправильно. Если TТeacher вызывает метод ShowSum объекта TStaff, то и функция GetSum, используемая в методе, будет принадлежать объекту TStaff. Зарплата будет рассчитана без учета количества часов.
Все описанные методы, относящиеся к типам объектов TPerson, TStudent, TStaff и TTeacher, являются статическими методами. Такое название методов связано с тем, что размещение соответствующих ссылок на них осуществляется еще на этапе компиляции. Действия компилятора при обработке методов объектов, составляющих некую иерархию, таковы:
1. При вызове метода компилятор устанавливает тип объекта, вызывающего метод.
2. Установив тип, компилятор ищет метод в пределах типа объекта. Найдя его, компилятор назначает вызов этого метода.
3. Если указанный метод не найден, то компилятор начинает рассматривать тип непосредственного прародителя и ищет метод, имя которого вызвано, в пределах этого прародительского типа. В случае, если метод с таким именем найден, вызов заменяется на вызов метода прародителя.
4. Если искомый метод отсутствует в типе следующего прародителя, то компилятор переходит к типу следующего прародителя. И так далее.
Если метод прародителя вызывает другие методы, то последние также будут методами прародителя.
Поскольку тип TTeacher является потомком типа TStaff, то сначала в сегмент кода будет скомпилирована функция TStaff.GetSum. Затем будет скомпилирована процедура TStaff.ShowSum, вызывающая TStaff.GetSum. Как и при вызове любой процедуры, компилятор замещает ссылки на TStaff.GetSum и TStaff.ShowSum в исходном коде на их адреса в сегменте кода. Фактически, наследуется следующая процедура:
Procedure TStaff.ShowSum;
Begin
Writeln (TStaff.GetSum);
End;
Метод объекта TStaff ничего не знает о существовании объекта TTeacher.
Виртуальные методы
Сущность виртуальных методов заключается в том, что методу присваивается одно имя, которое используется в иерархии объектов, причем каждый объект в этой иерархии реализует это действие своим собственным, пригодным для него способом. Во время компиляции процедуры должно быть неизвестно, объект какого типа будет ей передан в качестве фактического параметра. Такой параметр называется полиморфным объектом и отсюда название полиморфизм.
Метод становится виртуальным, когда за его определением в типе объекта ставится служебное слово Virtual:
Procedure ИмяМетода (параметры); Virtual;
или
Function ИмяМетода (параметры): ТипЗначения; Virtual;
При виртуализации методов должны выполняться следующие условия:
1. Если прародительский тип объекта описывает метод как виртуальный, то все его производные типы, которые реализуют метод с тем же именем, должны описать этот метод тоже как виртуальный. Другими словами, нельзя заменять виртуальный метод статическим.
2. Порядок расположения, количество и типы формальных параметров в одноименных виртуальных методах должны быть неизменными.
3. В описании объекта должен присутствовать специальный метод, инициализирующий объект (обычно ему дают название Init). В этом методе служебное слово Procedure в объявлении и реализации должно быть заменено на слово Constructor. Это служебное слово обозначает особый вид процедуры – конструктор, который выполняет установочную работу для механизма виртуальных методов. Конструктор всегда вызывается до первого вызова виртуального метода.
Различие между вызовом статического метода и виртуального метода заключается в том, что в первом случае компилятору заранее известна связь объекта с методом, и он устанавливает ее на этапе компиляции. Такое связывание называется ранним. Во втором – компилятор как бы откладывает решение до момента выполнения программы. Это позднее связывание. Каждый тип объекта, содержащий виртуальные методы, имеет таблицу виртуальных методов (ТВМ) – сегмент данных, содержащий размер типа объекта и для каждого виртуального метода указатель кода, исполняющий данный метод. Конструктор устанавливает связь между вызывающим его экземпляром объекта и ТВМ этого объекта.
Имеется только одна ТВМ для каждого типа объекта. Отдельные экземпляры объекта содержат только адрес ТВМ, но не саму ТВМ. Конструктор устанавливает значение этого адреса.
Описание объектов из примера платежной ведомости с использованием виртуальных методов:
TPerson = Object
Name: string[25];
Dolgn: string[25];
Stavka: Real;
Constructor Init (Nm, Dg: String; Sv: Real);
Function GetName: String;
Function GetDolgn: String;
Function GetStavka: Real;
Procedure ShowAll; Virtual;
end;
TStudent = Object (TPerson)
Ball: Real;
Constructor Init (Nm, Dg: String; Sv, Bl: Real);
Function GetBall: Real;
Function GetSum: Real; Virtual;
Procedure ShowAll; Virtual;
end;
TStaff = Object (TPerson)
Private
Bonus: Real;
Public
Constructor Init (Nm, Dg: String; Sv, Bn: Real);
Function GetSum: Real; Virtual;
Procedure ShowSum; Virtual;
Procedure ShowAll; Virtual
End;
TTeacher = Object (TStaff)
Private
Hours: Word;
HourRate: Real;
Public
Constructor Init (Nm, Dg: String; Sv, Bn, Hrt: Real; Hr: Word);
Function GetSum: Real; Virtual;
Procedure ShowAll; Virtual
End;
Метод ShowSum удален из определения TTeacher. ShowSum может наследоваться от TStaff со всеми вложенными в него вызовами, которые будут вызывать методы из TTeacher.
Выбор вида метода
Если в программе может понадобиться переопределение метода, то метод должен быть виртуальным. Это обеспечивает расширяемость программ.
Если объект имеет хотя бы один виртуальный метод, то для него создается таблица виртуальных методов и каждый вызов виртуального метода проходит через обращение к таблице. Статические методы вызываются “напрямую”, поэтому вызов статического метода происходит быстрее, чем виртуального. При определении типа метода необходимо делать выбор между некоторым (малозаметным) увеличением скорости вычислений при эффективном использовании памяти, которое дают статические методы, и гибкостью, представляемой виртуальными методами.
Динамические объекты
Так же, как и любые типы данных в Паскале, объекты можно размещать в динамической памяти и работать с ними, применяя указатели. Размещение объектов в динамической области памяти осуществляется с помощью процедуры New, традиционно применяемой для работы с указателями:
Var p: ^TPerson;
...
New (p);
Процедура New выделяет в динамической памяти область, достаточную для хранения экземпляра типа, определяемого указателем, и возвращает адрес этой области в указателе.
Если динамический объект содержит виртуальные методы, он должен инициироваться с помощью вызова конструктора:
p^.Init (‘Захаров’, ‘Бухгалтер’, 400)
Турбо Паскаль использует расширенный синтаксис процедуры New. Процедура New в одной операции позволяет выделить память под объект и вызвать конструктор. Имя указателя используется в качестве первого параметра, а имя конструктора – в качестве второго параметра:
New (p, Init (‘Захаров’, ‘Бухгалтер’, 400));
Для освобождения кучи от динамических объектов применяется стандартная процедура
Dispose (p);
Такой вызов уничтожает объект в целом. Если поля данных объекта были динамическими и под них выделялась дополнительная память при выполнении конструктора или иной процедуры инициализации, то их надо освободить до уничтожения самого объекта. Для этих целей вводится специальный вид метода – деструктор. Он объявляется служебным словом Destructor. По сути, деструктор – это метод, противоположный конструктору. Принято деструктору давать имя Done – завершено. Деструкторы могут наследоваться. Они могут быть статическими или виртуальными. Чаще используются виртуальные деструкторы, так как это гарантирует, что будет выполнен именно тот деструктор, который соответствует данному типу объекта. Смысл введения деструктора заключается в том, что его можно использовать в расширенной процедуре Dispose так же, как используется конструктор в New.
Dispose (p, Done);
Вызов работает следующим образом. Сначала выполняется вызов деструктора как обычного метода. Далее, если объект содержит виртуальные методы, то деструктор осуществляет поиск размера объекта в таблице виртуальных методов и передает размер процедуре Dispose, которая освобождает правильное количество байтов. Поэтому для динамических объектов всегда имеет смысл объявлять виртуальный деструктор, хотя бы и пустой, поскольку основная информация содержится не в теле деструктора, а связана с его заголовком, содержащим слово Destructor.
Destructor TPerson.Done;
Begin
End;
Деструктор дочернего типа, например TStaff, также должен последним действием вызывать соответствующий деструктор своего непосредственного предка, чтобы освободить поля всех наследуемых указателей объекта:
Destructor TStaff.Done;
Begin
Inherited Done;
End;