И.Г. Головин, И.А. Волкова - Языки и методы программирования (1160773), страница 26
Текст из файла (страница 26)
В языке C++ во всех классахнаследниках наследуемая виртуальная функция остается виртуальной.Таким образом, все типы-наследники полиморфного типа являютсяполиморфными типами.В C# возможна ситуация, когда наследуемая функция перестаетбыть виртуальной (или начинает новую цепочку виртуальных методов), однако использовать эту ситуацию настоятельно не рекомендуется.Механизм виртуального вызова (т.
е. вызова виртуального метода через ссылку или указатель) основан на понятии замещения(overriding) виртуального метода. Пусть есть (базовый) классBase, имеющий виртуальный метод М (замещаемый метод). Заместитель виртуального метода — это метод, определенный в производном классе и имеющий тот же прототип (т.е. тот же списокформальных параметров и тот же тип возвращаемого значения),что и метод М. Допускается следующее единственное ослаблениетребования совпадения прототипа: заместитель может иметь типвозвращаемого значения, ковариантный возвращаемому типу замещаемого метода М.129В языке C# заместитель должен быть обязательно снабжен описателем override. При его отсутствии метод не считается заместителем.Как уже отмечалось, это не является хорошей практикой, поэтомукомпилятор выдает по поводу «псевдозаместителя» предупреждение.Подавить это предупреждение можно только указанием описателяnew перед объявлением этого метода.При вызове виртуального метода Мчерез ссылку или указатель наобъект класса Base определяется динамический тип Т ссылки илиуказателя (т.
е. тип объекта, адрес которого хранится в ссылке илиуказателе). Напомним, что тип Т должен быть ковариантен Base(т.е. должен совпадать с Base или быть его наследником). Далеевызывается ближайший по цепочке наследования от Т к Base заместитель метода М.Другими словами, если метод Мзамещен в т, то вызывается этотзаместитель, если же м не замещен в Т, то отыскивается заместительв родительском классе и т.д.
до тех пор, пока заместитель не найдется(в крайнем случае вызывается метод из Base).В рассматриваемых нами языках реализация виртуального вызовадостаточно эффективна и не будет перебирать родительские классы,однако в таких языках как SmallTalk или JavaScript вызов виртуального метода может привести к циклу просмотра всей иерархииродительских классов.Приведем пример вызова виртуального метода на языке C++:tinclude <iostream>using namespace std;class A {public:virtual void f (int x){cout << "A::f" << '\n';}};class B: public A{public:void f (int x){cout <<}};int m a i n (){A a;A* pa;В b;130<< '\ n ';В* pb ;pb = & b;pb -> f (1); // B::fpa = pb;pa -> f (1); // B::fpa = &a;pa -> f (1); // A : :freturn 0;}Аналогичный пример на языке С# будет иметь видclass Program{class А{public virtual void f(int x){Console.WriteLine("A::f");}}class В : A{public override void f(int x){Console.WriteLine("B::f");}}static void Main(string[] args){A a;В b = new В ();b.f(l); // В ::fa = b;a.f (1); // B::fa = new A ();a.f (1); // A::f}}To же самое можно написать и на языке Java, надо только убратьмодификаторы override и virtual (и, конечно, изменить вывод).Заметим, что несмотря на то, что мы говорим о виртуальных методах, виртуальность — это свойство вызова, которое проявляется131только при вызове через ссылку или указатель.
Если в C++ вызватьвиртуальный метод через имя объекта, то никакого виртуальноговызова не будет (ведь тип собственно объекта известен во времятрансляции и меняться не может).Если вызывать виртуальную функцию класса X из функции-членакласса X или его производного класса, то это тоже будет виртуальныйвызов (ведь он осуществляется через ссылку (указатель) t h i s ) .Однако виртуальность вызова можно отменить, уточнив имя метода именем конкретного класса.Например, в фрагменте программы вызова виртуального методаможно статически привязать вызов к функции А: : f (i n t ), если поставить уточнение:рсрсрарарара— & cl;- > A: :f (l); // А: :f= рс;-> A: :f (l); // А: :f— &al ;-> A: :f (l); // А: :f«Снятие» виртуальности вызова часто применяется при написании заместителей. Заместители виртуальных методов можноусловно разделить на две категории: первые целиком берут на себяответственность за реализацию сущности, а вторые (каскадная) выполняют только часть работы, вызывая реализацию этого же методаиз базового класса (обычно либо в начале, либо в конце вызова).В этом случае следует явно указать, какая функция вызывается, иначепроизойдет рекурсивный вызов.В C# для каскадного вызова можно воспользоваться ключевымсловом b a se (например, b a se .h a n d le r ( a r g s ) ), а в Java — s u p e r(например, s u p e r .
h a n d le r ( a r g s ) ).Заметим, что в ряде случаев «умный» компилятор может сам отменить виртуальность вызова, но только если динамический типобъекта ссылки можно предугадать. Например, в конце подразд. 8.1отмечалось, что в языках C# и Java можно запретить наследование класса («запечатать» класс) с использованием модификаторовs e a le d и f i n a l . Если вызов виртуального метода происходит черезссылку на запечатанный класс, то очевидно, что динамический типэтой ссылки не может измениться (ведь производных классов уже неможет быть), поэтому вызов можно привязать к реализации методастатически.Также можно запретить и замещение виртуального метода.
Еслив Java перед объявлением метода м в классе С стоит модификаторf i n a l (вспомним, что все методы в Java виртуальные), это означает, что замещение метода в производных классах запрещается. Привызове «запечатанного» метода м через ссылку на С виртуальностьтоже можно снять.132В C# не все методы виртуальные, поэтому модификатор sealedможет стоять только перед методом-заместителем и должен сопровождаться модификатором override. Как и в Java, это означаетзапрещение замещения метода в производных классах и может использоваться для повышения эффективности вызова метода.Реализация динамического полиморфизмаДля иллюстрации того, как может быть реализован динамическийполиморфизм, рассмотрим реализацию механизма виртуальныхфункций в случае одиночного наследования в языке C++. Языки C#и Java используют схожие методы реализации.Для реализации механизма виртуальных функций используетсяспециальный (связанный с полиморфным типом) массив указателейна виртуальные методы класса.
Такой массив называется т а б л и ц е йв и р т у а л ь н ы х м е т о д о в (ТВМ). В каждый полиморфный объект компилятор неявно помещает указатель на соответствующую ТВМ, хранящую адреса виртуальных методов, который условно обозначаетсяvtbl* pvtbl,Строки ТВМ содержат адреса заместителей виртуальных методов:если метод замещен в классе, то записывается его адрес, а если незамещен, то заимствуется адрес заместителя из таблицы для базовогокласса.Рассмотрим пример иерархии из трех классов в следующем фрагменте программы на языке C++:class А {public:virtual void f() {cout « "A::f ";}virtual void u() {cout << "A::f ";}void g() { cout << "A::g};class В : public A {public:void f ( ) {cout << "B::fvoid g(){ cout « "B::g}virtual void h() { cout << "B::h}};class С : public В {public:void f() { cout << "C::f}void u(){ cout << "C::u}void h(){ cout « "C::h}virtual void w() {cout << "C::w ";}133};void P(A*pa,B& b) {pa->f (); pa->u(); pa->g();b.f(),;„b.u(); b.g(); b.h();delete pa;}int m a i n () {В b;P(new A, b ) ;c o u t « " ------------------------------- \n";С с;P (new B, c);return 0;}Результат работы приведенного фрагмента программы следующий:А : :f А : :u А : :g В : :fА: :иВ : :g В : :hВ : :f А : :u А::д С : :f A::u В::д C::hДля рассмотренного примера с виртуальными функциями созданные таким образом структуры приведены на рис.
8.1.В ТВМ типа-наследника имеющиеся адреса одинаковых методовзамещаются, а новые — дописываются в конец. Так, ТВМ класса Асодержит две записи (адрес метода f () и и ()), ТВМ класса в — тризаписи (адреса f ( ) , и () и h ()), а ТВМ класса С — четыре (адресаf ( ) , и ( ) , h(), w ()). Важно то, что каждому методу соответствуетфиксированный индекс в ТВМ. Метод f () всегда имеет индекс 0(и будет его иметь в любой ТВМ, производной от А класса), методи () — индекс 1 и т.д.Объект типа класс АА: :А: :vtbl*pvtblОбъект типа класс Вvtbl*pvtbl•-Объект типа класс Сvtbl*pvtblРис. 8.1.
ТВМ для иерархии классов134ТВМ для класса А&f О&U ()ТВМ для класса ВВ: :А: :В: :&f О&U ()&h()ТВМ для класса СС:А:В:С:::::&f О&U ()&h()&W ()Так как указатель на ТВМ находится в самом начале объекта,то он доступен всегда, каким бы ни был тип указателя на объект.Конечно, при этом из ТВМ могут быть выбраны только те методы,которые имеются в объявлении класса — статического типа указателя.Слндовательно, как показано в рассматриваемом примере, если объект производного типа обрабатывается через указатель базового типа,то из ТВМ данного объекта можно вызывать только виртуальныеметоды, перечисленные в базовом типе.