Виртуальный (кажущийся, гипотетический) метод имеет спецификатор (стандартную директиву) Virtual. Например:
Procedure Met2; Virtual;
Виртуальный метод предназначен для переопределения виртуального метода предшествующего предка. Недопустимо смешение статических и виртуальных методов при их переопределении. Ограничение: переопределяющие виртуальные методы должны иметь точно такой же набор формальных параметров, как и самый первый виртуальный метод многоуровневой иерархии объектов.
Паскаль обеспечивает вызов (связывание) виртуального метода программы на этапе выполнения программы. Это называют поздним связыванием. При позднем связывании полиморфизм распространяется не только от текущего уровня иерархии вниз, к потомкам (как для статических методов), но и вверх, к предкам.
В соответствии с этим основные отличия переопределения виртуальных методов от переопределения статических методов состоят в следующем:
1) если для экземпляра потомка методы предка вызывают другие виртуальные
методы, имена которых есть в объекте-предке и в объекте-потомке, то вы
зываются методы потомка;
2) виртуальные методы используют позднее связывание данных с методами,
с помощью которых они будут обрабатываться, т. е. связывание на этапе
выполнения программы.
Компилятор не устанавливает связи объекта с виртуальным методом. Вместо этого он создает специальную таблицу виртуальных методов (ТВМ, VMT -Virtual Method Table). Для каждого типа объекта создается своя ТВМ; каждый экземпляр объекта использует эту ТВМ, единственную для данного типа виртуальных объектов. В каждой ТВМ содержится размер данного типа объекта в байтах. ТВМ любого объекта доступна через скрытый параметр Self, содержащий адрес ТВМ, который передается методу при вызове.
Связывание каждого экземпляра объекта и его ТВМ осуществляется с помощью конструктора на этапе выполнения программы. Это специальный метод, подобный обычной процедуре, но в заголовке вместо PROCEDURE стоит слово CONSTRUCTOR. Если объектный тип содержит виртуальный метод, то он должен содержать хотя бы один конструктор. Каждый экземпляр объекта должен инициализироваться отдельным вызовом конструктора. Конструктор инициализирует экземпляр объекта и устанавливает для него значение адреса его ТВМ. Экземпляр объекта содержит только адрес ТВМ, а не саму ТВМ.
Вызов конструктора должен предшествовать вызову любого виртуального метода для обработки данного экземпляра объекта. Обращение к виртуальному методу до вызова конструктора вызовет ошибку на этапе выполнения программы, так как нет связи экземпляра объекта с его ТВМ.
При отладке программы можно использовать директиву компилятора {$R+}. При этом компилятор будет проверять, инициализирован ли экземпляр объекта, вызывающий виртуальный метод. Если в процессе проверки инициализации экземпляра объекта, вызывающего виртуальный метод, будет обнаружен вызов метода до инициализации объекта конструктором, выдается сообщение о фатальной ошибке 210:
Object not initialized - объект не инициализирован.
Так как директива {$R+} замедляет выполнение программы, после отладки директиву надо удалить; по умолчанию работает директива {$R->.
В объекте может быть сколько угодно конструкторов. Конструктор не может быть виртуальным. Конструктор может быть только статическим и может быть переопределен. Конструкторы наследуются так же, как и другие статические методы. Из конструктора можно вызывать и виртуальные методы.
Метод конструктора может быть и пустым, так как основная информация содержится не в теле конструктора, а связана с его заголовком, содержащим слово Constructor. Например:
Constructor TA.TNIT ;
Begin
End;
Конструктору принято давать имя INIT. На практике в качестве конструктора используют метод, который устанавливает некоторые начальные значения экземпляра объекта. В конструкторе может происходить выделение ОП из кучи, если поля данных динамические, и необходимая инициализация полей данных (в том числе и вызовы конструкторов-предков для унаследованных полей).
Пример программы с использованием виртуальных методов дан в листинге 4. Это вариант программы листинга 3, но с виртуальным методом Met2.
Листинг 4.Использование виртуальных методов.
Program virt1; {$F+,R+} Uses Crt;
Type ObjName1 = object { - объявление объекта-предка } Fl1 : integer;
Constructor Met1; { - конструктор }
Procedure Met2; Virtual; { - виртуальный метод } End; ObjName2 = object(ObjName1){- объявление потомка}
Procedure Met2; Virtual; { - виртуальный метод } End;
{ -- Методы объекта ObjName1 ------ }
Constructor ObjName1.Met1; Begin Fl1 := 12;
Met2; { - вызов Met2 из конструктора }
End;
Procedure ObjNamel.Met2;
Begin
Writeln ( 'Работает метод ObjName1.Met2: FL1 = ', Fl1) End;
{ -- Методы объекта ObjName2 ------ }
Procedure ObjName2.Met2; Begin Fl1 := 34;
Writeln ( 'Работает метод ObjName2.Met2: FL1 = ', Fl1) End;
Var VI:ObjName1;{- переменная объектного типа - предка }
V2 : ObjName2; { - " " " потомка }
{ ------ Основная программа ---------- }
Begin ClrScr;
Assign (Output, '2virt.res'); Rewrite (Output);
Writeln ( 'ОБЪЕКТЫ, ВИРТУАЛЬНЫЕ МЕТОДЫ' );
Writeln ('Работаем с VI - экземпляром типа предка'); VI.Met1; { - вызывается конструктор Met1 для экземпляра VI - предка }
{ Met1 вызывает метод ObjName1.Met2 - предка }
VI.Met2; { - непосредственно вызывается метод ObjName1.Met2; }
Writeln ('Работаем с V2 - экземпляром типа потомка'); V2.Met1; { - вызывается конструктор Met1 для экземпляра V2 - потомка VI; Met1 вызывает метод ObjName2.Met2 - потомка -в этом достоинство виртуальных методов }
V2.Met2 { - непосредственно вызывается метод ObjName2.Met2; }
Close. (Output) ;
End.
Каждый экземпляр объекта должен инициализироваться отдельным вызовом конструктора. Нельзя инициализировать один экземпляр объекта и затем присваивать этот экземпляр другим, неинициализированным объектам. Другие экземпляры, даже если они содержат правильные данные, не будут инициализированы оператором присваивания и заблокируют систему при любых вызовах их виртуальных методов. Например:
Var Obj1, Obj2 : TObj; { - объявление переменных } Begin
Obj1.Init; { - инициализация Obj1 }
Obj2 := Obj1; { - недопустимо до инициализации Obj2
с помощью Obj2.Init; }
End.
Хорошим стилем ООП является использование процедур инициализации предков. То есть если потомок имеет новую процедуру инициализации, то в ней обычно сначала вызывается процедура инициализации (например, конструктор) непосредственного предка, а затем выполняется своя. Это естественный способ проинициализировать наследуемые поля предназначенным для этого методом.
Дополнительная возможность вызова метода, непосредственно наследуемого объектом-потомком, - использование ключевого слова Inherited (наследуемый) перед именем вызываемого метода. Например, Objl непосредственный предок объекта Obj2. В состав Objl входит метод Metl. Вызов этого метода объекта Objl из метода Obj2.Met2 может быть задан явно в виде:
Procedure Obj2.Met2;
Begin Inherited Met1 (A1, A2);
{ Это эквивалентно вызову: Obj1.Met1 (A1, A2); }
End;
При этом не надо знать имя объекта-предка (Objl). Основные правила использования виртуальных методов:
1) если тип объекта-предка описывает метод как виртуальный, то все его потомки, которые реализуют свой метод с тем же именем, должны описать
этот метод как виртуальный. Нельзя виртуальный метод заменить статическим. Иначе компилятор выдаст сообщение об ошибке:
Error 149: VIRTUAL expected - ОЖИДАЕТСЯ СЛОВО VIRTUAL.
2) если переопределяется виртуальный метод, то заголовок переопределяемо
го метода не может быть изменен; должны остаться неизменными: количество, последовательность и типы формальных параметров в одноименных
виртуальных методах. Если этот метод - функция, то не должен изменяться и тип результата. При нарушении этого правила компилятор выдаст сообщение:
131: Header does not match previous definition - заголовок не соответствует предыдущему определению;
3) в описании объекта, имеющего виртуальные методы, обязателен -конструктор. Он устанавливает работу механизма виртуальных методов. Вызов виртуального метода без предварительного вызова конструктора может при
вести к тупиковому состоянию. Чтобы избежать этого, на период отладки
надо включить директиву компилятора {$R+};
4) каждый экземпляр объекта должен инициализироваться отдельным вызовом конструктора;
5) количество конструкторов может быть любым;
6) конструктор должен быть статическим и может быть переопределен.