Глава 8: Обектно-ориентирано програмиране
Съдържание на осма глава :

8.1. Презаредими функции с аргумент клас
8.2. Виртуални функции
8.3 Виртуални базови класове




--------------------------------------------------------------------------------



Обектно-ориентираното програмиране се характеризира с онаследяване и динамично свързване. С++ поддържа онаследяването чрез извличане на класове - това беше темата на предишната глава. Динамичното свързване се осъществява чрез виртуалните функциина клас.

Йерархията на онаследяване дефинира отношения тип-подтип между типовете класове. Например, Panda е тип Bear, на свой ред Bear е тип ZooAnimal. По същия начин и сортираният масив,и масив с проверка на ранга са типове IntArray. Виртуалните функции дефинират типово зависими операции в йерархията на онаследяването - например, функцията draw() на ZooAnimal или subscript оператора на класа масив. Виртуалните функции осигуряват метод за капсулиране на детайли по реализацията на йерархията на онаследяването от програмите, които ги използват. В тази глава ще разгледаме подробно виртуалните функции. Ще разгледаме също един специален случай на онаследяване на класове - този на виртуалния (или още споделен) базов клас.В началото обаче ще преразгледаме презаредимостта на име на функция с аргумент от тип клас.



8.1. Презаредими функции с аргумент клас

Често една функция се прави презаредима за да се осигури образец на съществуваща функция за работа с обекти класове. Например, ако трябва да се дефинира клас комплексно число, проектантът трябва след това да дефинира образец на библиотечната функцияза извличане на квадратен корен, която оперира с обекти откласа Complex:

extern Complex& sqrt( Complex& );

Първоначалното разглеждане на презаредими функции. Това е предмет на обсъждане на тази глава.

Сега ще разгледаме точното съпоставяне на аргумент от тип клас, съпоставянето чрез стандартни преобразования и съпоставянето чрез извикване на дефиниран от потребителя оператор за преобразуване.

Точно съпоставяне

Обект от клас се съпоставя точно само с формален аргумент от неговия тип клас. Например,

ff( Bear& );

ff( Panda& );

Panda yingYang;// exact match : ff( Panda& )

ff( yinYang );

По подобен начин указател към обект от клас се съпоставя точно само с формален аргумент указател към същия тип клас. Алгоритъмът за съпоставяне на аргументи не може да правиразлика между обект и псевдоним от тип клас. Макар че следващите два образеца декларират две различни функции, реалното обръщение е двузначно и предизвиква грешка по време на компилация.

// warning : cannot be distinguished// by the argument matching algorithm ff( Panda );

ff( Panda& );// ok : ff( Panda& )

int (*pf)( Panda& ) = ff;

ff( yinYang ); // error : ambiguous

pf( yinYang ); // ok



Стандартни преобразования

Ако класовият аргумент не се съпоставя точно, прави се съпоставяне чрез прилагане на предварително дефинирани стандартни преобразувания.

Извлечен обект от клас, псевдоним или указател явно се преобразуват в съответния публичен базов тип клас. Например,

ff( ZooAnimal& );

ff( Screen& );// ff( ZooAnimal& )ff( yinYang );*

Указател към произволен тип клас явно се преобразува в указател от тип void

*.ff( Screen& );

ff( void* );// ff( void* ) ff( yinYang );

Преобразуване на обект от базов клас, псевдоним или указател в съответния тип извлечен клас не се прилага. Например, при следващото обръщение не може да се осъществи съпоставяне:

ff( Bear& );ff( Panda& );

ZooAnimal za;ff( za ); // error : no match

Присъствието на два или повече непосредствени базови класове предизвиква маркирането на обръщението като двузначно. Например, Panda се извлича едновременно от Bear и Endangered. И двете преобразования на Panda изискват едно и също действие. Тъй като и двете преобразувания са възможни, обръщението е грешно.

ff( Bear& );ff( Endangered& );

ff( yinYang ); // error : ambiguous

За да бъде осъществено обръщението, програмистът трябва явно да укаже в него:

ff( Bear(yinYang));

Извлеченият клас се разглежда като по-близък до неговия непосредствен базов клас отколкото до един по-отдалечен базов клас. Следващото обръщение не е двузначно, макар че и в двата образеца се изисква стандартно преобразуване. Panda се третира по-скоро като вид Bear, отколкото като вид на ZooAnimal от алгоритъма за съпоставяне на аргументи.

ff( ZooAnimal& ); ff( Bear& );// ff( Bear& ); ff( yinYang );

Това правило се разширява да включва и void*. Например, дадена е следната двойка презаредеми функции:

ff( void* );ff( ZooAnimal* );

Аргументът от тип Panda* се съпоставя с ZooAnimal*.

Дефинирани от потребителя преобразувания

Дефинираното от потребителя преобразуване може да бъде конструкторс един аргумент или оператор за явно преобразуване. Дефинираните от потребителя преобразувания се прилагат само ако не може да се направи точно съпоставяне или не се открие стандартно преобразуване. За тази част от параграфа нека осигурим ZooAnimal с две потребителски дефинирани преобразувания:

class ZooAnimal

{public:

ZooAnimal( long );// conversion : long ==> ZooAnimal

operator char*();// conversion : ZooAnimal ==> char*

// ...

};

Дадена е следната двойка презаредими функции:

ff( ZooAnimal& );

ff( Screen& );

Обръщение с фактически параметър от тип long ще бъде осъществено чрез образеца на ZooAnimal чрез извличане на потребителски дефинирано преобразувание:

long lval;// ff( ZooAnimal& )

ff( lval );

Какво ще стане, ако обръщението е с аргумент int? Например,

ff( 1024 ); // ???

Не може да се направи нито точно съпоставяне, нито съпоставяне чрез стандартни преобразувания. Има винаги едно приложимо потребителски дефинирано стандартно преобразуване. Проблемът е в това, че конструкторът за преобразуване на ZooAnimal очаква стойност от тип long,а не int. Алгоритъмът за съпоставяне на аргументи ще приложи стандартно преобразуване за намирането на приложимо потребителски дефинирано преобразуване. В случая, 1024 се преобразува да бъде от тип long, за да се възприеме от конструктора на ZooAnimal. Обръщението се осъществява чрез образеца на ZooAnimal.

Дефинирано от потребителя преобразуване се прилага само когато друго преобразуване не е възможно. Ако образецът на ff() беше деклариран да възприема предварително дефиниран тип, операторът за преобразуване на ZooAnimal нямаше да бъде викан. Например,

ff( ZooAnimal& );

ff( char );// ff( char );

long lval;

ff( lval );

В този случай е необходимо явно указване в обръщението за да се осъществи обръщение към образец на ZooAnimal:

ff( ZooAnimal(lval));// ff( ZooAnimal& )

В следващия пример се прилага операторът за преобразуване в char* на ZooAnimal тъй като няма стандартно преобразуване от обект на базов клас в обект на извлечения клас.

ff( char* );

ff( Bear& );

ZooAnimal za;// za ==> char* // ff( char* ) ff( za );

Алгоритъмът за съпоставяне на аргументи ще приложи стандартно преобразуване за постигането на потребителски дефинирано преобразуване, ако това прави възможно самото съпоставяне. Например,

ff( Panda* );

ff( void* );

// za ==> char* ==> void

// ff( void* )

ff( za );

Операторите за преобразуване (но не и конструкторите) се онаследяват по същия начин като другите членове на класа. И Bear, и Panda наследяват char* операторът за преобразуване на ZooAnimal. Например,

ff( char* );

ff( Bear* );

Bear yogi;

Bear pBear = &yogi;// ff( char )ff( yogi );// ff( pBear ) ff( pBear );

Ако съпоставянето е възможно чрез прилагането на две или повече потребителски дефинирани преобразувания, обръщението е двузначно и предизвиква грешка по време на компилация. Преобразуващите конструктори и операторите за преобразуване имат едно и също предимство. Ако може да бъде приложен по един образец от всеки тип, обръщението е двузначно. Например, нека Endangered дефинира оператор за преобразуване от тип int:

class Endangered {

public:

operator int();// conversion : Endangered ==> int

// ...};

Тогава ако Extinct дефинира преобразуващ конструктор, който възприема псевдоним на обект от класа Endangered, както следва

class Extinct {

public:



Extinct( Endangered& );

// ...

};

Следващото обръщение е двузначно. И операторът за преобразуване на Endangered и преобразуващият конструктор на Extinct могат да осъществят съпоставяне.

ff( Extinct& );

ff( int );

Endangered e;

ff( e ); // error : ambiguous

Тук има втори пример на двузначност при извикване на потребителски дефинирано преобразуване. В този случай преобразуващите конструктори на SmallInt и BitVector са еднакво приложими - обръщението се маркира като грешно.

class SmallInt {

public:

SmallInt( int );// conversion : int ==> SmallInt

// ...

};

class BitVector {

BitVector( unsigned long );// conversion : unsigned long ==> BitVector

// ...};

ff( SmallInt& );

ff( BitVector& );

ff( 1 ); // error : ambiguous



8.2. Виртуални функции

Виртуалната функция е специална член функция викана чрез указател към публичен базов клас или псевдоним на публичен базов клас; тя се построява динамично по време на изпълнение. Извикваният образец се определя чрез типа на класа на актуалния обект, адресиран от указателя или псевдонима. За потребителя начинът на реализиране на една виртуална функция е явен.

draw(), например, е виртуална функция с образци в ZooAnimal, Bear, Panda, Cat и Leopard. Функция, викаща draw(), може да бъде дефинирана по следния начин:

inline void draw( ZooAnimal& z )

{ z.draw(); }

Ако един аргумент към нечлен образец на draw() адресира обект от класа Panda, операторът z.draw() ще извика Panda::draw(). Един следващ аргумент, адресиращ обект от класа Cat ще предизвика обръщение към Cat::draw(). Компилаторът решава кои членове функции на класа да извика според на типа на класа на действителния обект. Преди да разгледаме как се декларира и използва виртуална функция, нека накратко видим защо искаме да използваме виртуални функции.

Динамичното построяване е форма на капсулиране. Крайният екран на реализацията ZooAnimal представя множество от животни, за които посетителят е искал информация. Този екран,за удоволствие на децата, е направил от дисплейния терминал една атракция.

За реализирането на множеството се поддържа свързан списък от указатели към животни, за които посетителят ще бъде уведомяван. Когато се натисне клавиша QUIT, главата на свързания списък от ZooAnimal се предава на finalCollage(), която показва животните в подходящ размер и вид на екрана.

Поддържането на свързания лист е просто, тъй като указател към ZooAnimal може да адресира всеки публично извлечен клас.С динамичното построяване не е сложно също да се определи извлечен клас, адресиран от указател към ZooAnimal. finalCollage() може да бъде реализирана по следния начин:

void finalCollage( ZooAnimal *pz )

{for ( ZooAnimal *p = pz; p ; p = p->next )

p->draw();}

В език, в който този проблем не е разрешен по време на изпълнение, остава грижа на програмиста да определи извлечен клас, адресиран от указател към ZooAnimal. Обикновено, това довежда до идентифициране на члена на клас isA() и операторите if-else или switch, които проверяват стойностите на isA(). Без динамично построение finalCollage може да бъде реализирана по следния начин:

// nonobject-oriented implementation

void finalCollage( ZooAnimal *pz )

{ for ( ZooAnimal *p = pz; p ; p = p->next )

switch ( p->isA() )

{ case BEAR:

((Bear *) p)->draw();break;

case PANDA:((Panda *) p)->draw();break;// ... every other derived class} //

switch of isA}

Програмите, написани в този стил, са основани върху детайли в реализацията на йерархията на извличането. Ако тези детайли се променят, работата на програмата може да бъъде нарушена и да се наложи да се разшири кода на програмата.

След като пандите напуснат зоопарка и си отидат в Китай, типът клас Panda може да се изтрие. Когато пристигнат коали от Австралия, трябва да бъде прибавен тип клас Koala. За всяка промяна в йерархията всеки оператор if-else и switch, който проверява типа на класа, трябва да бъде променен. Програмният код се променя със всяка промяна на йерархията.

Всеки оператор switch увеличава забележимо обема на програмния код. Концептуално прости по своята същност действия се усложняват поради условните тестове, необходими за проверка на типа клас на даден обект. Програмите стават трудни за разчитане.

Потребителите на йерархията и нейната реализация, желаещи да разширят йерархията или да използват приложенията й, трябва да имат достъп до програмния код. Това прави поддържането на системата доста по-трудно и за разпространителя, и за потребителя на системата.

Ако проблемът е разрешен по време на изпълнение, това скрива от потребителя детайлите по реализацията на йерархията на извличането. Условните проверки на типа клас вече не са необходими. Това опростява потребителския код и го прави нуждата от промени по-малка. Потребителският код, който вече не се променя със всяка промяна на йерархията, е по-лесен за програмиране и поддържане.

На свой ред това опростява разширяването на йерархията. Прибавянето на ново извличане от ZooAnimal не изисква промяна в съществуващия код. Главната функция draw() не се интересува от бъдещите извличания от ZooAnimal. Нейният код остава функционален независимо от това в каква степен йерархията е била променена, което означава, че реализацията с изключение на header-файловете може да бъде разпространена в двоичен вид.

Опростява се също използването на системата. Тъй като и реализацията на типовете класове, и реализацията на йерархията на класовете са капсулирани, те могат да бъдат променяни с минимални добавки в кода на клиента като остава непроменен публичният интерфейс.

Дефиниция на виртуални функции

Една виртуална функция се определя като се прибави в началото на декларацията на функцията ключовата дума virtual. Само функции,членове на клас, могат да бъдат декларирани като виртуални. Ключовата дума virtual може да се среща само в тялото на клас. Например,

class Foo {public:virtual bar(); // virtual declaration

};

int Foo::bar() { ... }

Следващата опростена декларация на ZooAnimal декларира четири виртуални функции: debug(), locate(), draw() и isOnDisplay().

#include <stream.h>

class ZooAnimal

{

public:

ZooAnimal( char *whatIs = "ZooAnimal"): isA( whatIs ) {}

void isA()

{ cout << "\n\t" << isa << "\n"; }

void setOpen( int status )

{ isOpen = status; }

virtual isOnDisplay()

{ return isOpen; }

virtual void debug();

virtual void draw() = 0;

protected:virtual void locate() = 0;

char *isa;

char isOpen;

};

void ZooAnimal::debug()

{isA();cout << "\tisOpen:"<< ((isOnDisplay()) ? "yes" :

"no") << "\n";

}

debug(), locate(), draw() и isOnDisplay() са декларирани като член функции на ZooAnimal защото представляват множество функции, общи за цялата класова йерархия. Те са декларирани като виртуални, защото има детайли на реализацията, които зависят от типа на класа и са първоначално неуточнени. Виртуалната функция на базовия клас служи като място, където се съхраняват все още неопределените типове класове.

Виртуална функция, дефинирана в базовия клас на йерархията, често въобще не се вика, например locate() и draw(). Нито има някакъв смисъл в абстрактен клас като ZooAnimal. Проектантът на класовете може да определи, че една виртуална функция не е дефинирана в абстрактен клас като инициализира с 0 нейната декларация.

virtual void draw() = 0;

virtual void locate() = 0;

draw() и locate() се наричат чисто виртуални функции. Един клас с една или повече чисто виртуални функции може да бъде използван само като базов клас за следващи извличания. Не е правилно създаването на обекти от клас, съдържащ чисто виртуални функции. Например, следващите две дефиниции на ZooAnimal предизвикват грешка по време на компилация:

ZooAnimal *pz = new ZooAnimal; // error

ZooAnimal za; // error

Само абстрактен клас, за който не се предвижда да има собствени образци, може да декларира чисто виртуална функция.

Класът, който първи декларира една функция като виртуална, трябва също да я декларира като чисто виртуална функция или да осигури дефиниция.

- aко е осигурена дефиниция, тя служи като образец по премъл-чаване ,ако извличаният клас не осигури свой образец на виртуалната функция.

- aко е декларирана чисто виртуална функция, извлеченият кластрябва или да дефинира образец на функцията, или да я декларира отново като чисто виртуална функция. Например, Bear трябва или да осигури дефиниции за draw() и locate(), или да ги декларира отново като чисто виртуални функции.

Какво да правим, обаче, ако възнамеряваме да декларираме обекти от типа клас Bear, но все още желаем да отсрочим реализацията на draw() докато не бъдат извлечени отделни видове като Panda или Grizzly ? Не можем да декларираме draw() като чисто виртуална функция и да продължаваме да дефинираме обекти от класа. Ето три алтернативни решения на въпроса:

1. Дефинираме празен образец на виртуална функция: class Bear :

public ZooAnimal

{ public:void draw() {}// ...

};

2. Дефинираме образец, обръщението към който предизвиква вътрешна грешка: void Bear::draw()

{ error( INTERNAL, isa, "draw()" ); }

3. Дефинираме образец, който да следи за неочаквано поведение при начертаването на родовия образ на Bear. Тоест, системата продължава работата си, но същевременно се събира информация за изключенията по време на изпълнение, които трябва да бъдат обработени по-нататък.

Извлечен клас може да осигурява свой собствен образец на виртуална функция или по премълчаване да онаследява образеца на базовия клас. На свой ред, той може да въведе своя собствена виртуална функция. Например, Bear предефинира debug(), locate() и draw(); той онаследява образеца на isOnDisplay от ZooAnimal. В допълнение Bear дефинира две нови виртуални функции hibernates() и feedingHours(). Дефиницията на Bear е опростена с цел да се подчертае значението на виртуалните функции.

class Bear :

{ public ZooAnimal

public:Bear( char *whatIs = "Bear" ): ZooAnimal( whatIs ),

feedTime( "2:30" ) {} // intentionally null

void draw(); // replaces ZooAnimal::draw

void locate(); // replaces ZooAnimal::locate

virtual char *feedingHours()

{ return feedime; }

protectted:void debug(); // replaces ZooAnimal::debug

virtual hibernates() { return 1; }

char *feedTime;

};

Предефинирането на една виртуална функция в извлечен клас трябва точно да съответства на името, сигнатурата и типа на връщане на образеца на базовия клас. Ключовата дума virtual не енужно да бъде специфицирана (макар, че би могла да бъде при желание на потребителя). Дефиницията е като на обикновена член функция.

void Bear::debug()

{isA();cout << "\tfeedTime:"

<< feedingHours() << "\n";}

void Bear::draw() {/*...code goes here*/}

void Bear::locate() {/*...code goes here*/}

Целият виртуален механизъм всъщност се осъществява от компилатора. Проектантът на класовете само трябва да зададе ключовата дума virtualпри пъървоначалната дефиниция на всеки образец.

Ако повторната декларация в извлечения клас не се съпоставя точно, функцията не се третира като виртуална за извлечения клас. Например, ако Bear декларира debug() по един от следните начини:

void *Bear::debug() {...}// different return type

void Bear::debug( int ) {...}// different signature

debug() няма да бъде виртуална за класа Bear. Например,

Bear b;

ZooAnimal &za = b;

za.debug(); // invoke ZooAnimal::debug()

Един извлечен след това клас от Bear, обаче, все още може да осигурява виртуален образец на debug(), дори Bear да не може. Например,

class Panda : public Bear {

public:

void debug(); // virtual instance

//

...

};

Образецът на Panda е също виртуален поради точното съпоставяне с виртуалната декларация на debug():

Panda p;

ZooAnimal &za = p;

za.debug(); // Panda::debug()

Забележете, че нивата на защита за две от виртуалните функции са различни за образеца на базовия клас и образеца на извлечения клас. Образецът на locate() в ZooAnimal е protected, докато образе-цът в Bear е public. Аналогично, образецът на debug() в ZooAnimal е public, докато образецът в Bear е protected.

Какви са всъщност нивата на защита на locate() и debug()? Например, целта ни е да напишем такива общи функции като:

void debug( ZooAnimal& z )

{// compiler resolves intended instance

z.debug();}

Тъй като Bear::debug() е protected, верни ли са следните обръщения?

main()

{

// outputs : Bear

// feedTime : 2.30

Bear ursus;

debug( ursus );}

Отговорът е не: debug(ursus) не е вярно. Нивото на достъп на виртуалната функция се определя от типа на класа на указателя или псевдонима, чрез който се извиква член функцията. Всяко виртуално обръщение се третира като public, понеже debug() е public член на ZooAnimal. Забележете, че и този образец, и следващия предполагат, че ZooAnimal не декларира чисто виртуални функции.

main()

{ ZooAnimal *pz = new Bear;

pz->debug(); // invokes Bear::debug()

pz = new ZooAnimal;pz->setOpen(1); // open the zoo

pz->debug(); // invokes ZooAnimal::debug() }

след компилация изходът е:

Bear

feedTime: 2:30

ZooAnimal

isOpen: yes

Извикванията на виртуалните образци на debug() чрез типа клас Bear, обаче, могат да бъдат третирани като имащи достъп protected. Следващите извиквания ще бъдат отбелязани като имащи нелегален достъп на protected член на клас:

main()

{

ZooAnimal *pz = new Bear;

pz->debug(); // invokes Bear::debug()

Bear *pb = (Bear *) new ZooAnimal;

// dangerous illegal : main has no access privilege

// to the protected members of Bear!

pb->debug();}

Аналогично, locate() e protected член на ZooAnimal и същевре-менно е public член на Bear. Виртуалните извиквания чрез указател на Bear или псевдоним ще бъдат третирани като имащи public достъп. Виртуалните извиквания чрез указател или псевдоним на ZooAnimal, обаче, ще бъдат третирани като имащи protected достъп. Следващото извикване се маркира като имащо нелегален достъп на protected член докато нечлен функцията locate() не се направи friend за ZooAnimal.

void locate( ZooAnimal *pz )

{// locate() has no access

privilege;// unless made friend to ZooAnimal

pz->locate(); // error

}

Тези два примера са показани, за да илюстрират, че макар че виртуалните функции се разрешават по време на изпълнение, за тях остават в сила правилата за достъп при скриването на информацията. Единствената аномалия е в това, че нивото на достъп на виртуална функция е нивото, определено от типа клас, чрез който се осъществява обръщението.

Bear осигурява свои собствени образци на три от четирите виртуални функции, които дефинира ZooAnimal; допълнително той също дефинира две виртуални функции. Извличане на Panda наследява шестте виртуални функции, достъпни в рамките на базовия клас Bear.

Коя от трите виртуални функции на ZooAnimal е предефинирана в рамките на Bear? Те остават членове, наследени от ZooAnimal, и отново могат явно да бъдат викани. Например, debug() може да бъде отново реализирана по следния начин:

void Bear::debug()

{ZooAnimal::debug();

cout << "Fed time: "<< feedTime << "n";}

Panda се извлича от три базови класа: Bear, Endangered и Herbivore. И Endangered, и Herbivore дефинират две виртуални функции:

class Endangered

{

public:

virtual adjustPopulation( int );

virtual highlight( short ); //...

};

class Herbivore

{

public:

virtual inform( ZooAnimal& );

virtual highlight( short ); //...

};

Panda онаследява десетте виртуални функции, дефинирани в трите базови класал Тя може да осигурява свои собствени образци за всяка от виртуалните си функции. В допълнение, тя може да въведе собствени виртуални функции.

Panda онаследява две виртуални функции, наречени highlight() една при извличането от Endangered и една при извличането от Herbivore. Това е проблематично само ако highlight() се извлича от тип клас Panda или ако типът клас Panda е обект на извличане. И в двата случая използването на псевдоним на highlight() е двузначно. Например,

RedPanda : public Panda { ... };

RedPanda pandy;

Panda &rp = pandy;//...

rp.highlight(); // error : ambiguous

За да се избегне евентуална двузначност, Panda дефинира соб-ствен образец на highlight() (виж Параграф 7.4). Следващата дефиниция на класа Panda е опростено с цел да се наблегне на декларирането на виртуална функция при многократно онаследяване.

class Panda : public Bear, public Endangered,public Herbivore

{

public:

Panda( char *whatIs = "Panda: ) : Bear( whatIs ) {}

virtual onLoan();

inform( ZooAnimal& );

void draw();

int debug();

void locate();

hibernates() { return 0; }

protected:

highlight( short );

short cell;

short onLoan;

};

Базовият клас знае за предефинирането на неговите виртуални функции при следващите извличания на класа. Виканият виртуален образец се определя чрез действителния тип на класа на указател към базовия клас или псевдоним. Базовият клас, обаче не знае за въведените в следващите извличания виртуални функции. Например, не е възможно да се извика hibernate(), въведена от Bear, чрез псевдоним или указател към ZooAnimal. Не е вярно, например, следното:

int hibernate( ZooAnimal &za )

{

// error : hibernate not a member of ZooAnimal

}

Bear е “базов клас” за виртуалната функция hibernate(). Само класовете от неговото ниво в йерархията на онаследяването имат достъп до него. Ако hibernate() е общо действие за широко множество от класове в йерархията на онаследяването, нейната дефиниция не принадлежи на Bear. hibernate() трябва да бъде преместена по-нагоре в йерархията (в този случай се превръща в член на ZooAnimal) или нивата на йерархията трябва да се препроектират. И в двата случая hibernate() трябва да бъде достъпна до всички класове в йерархията, чието общо действие описва.

Таблица 8.1 показва активните виртуални функции в Panda. Втората колона представлява списък на класовете, в които са дефинирани активните функции; третата колона - класът, в който виртуалната функция е първоначално дефинирана.

Виртуална функция Активна функция Първа дефиниция

isOnDisplay() ZooAnimal locate()
Panda ZooAnimal draw()
Panda ZooAnimal debug()
Panda ZooAnimal feedingHours()
Bear Bear hibernates()
Bear
Bear adjustPopulation(int)
Endangered
Endangered highlight(short)
Panda Endangered/ Herbivore
Panda
inform(ZooAnimal&)
Herbivore onLoan()
Panda Panda

Таблица 8.1 Виртуални функции на Panda

Трудно е да се направи добро проектиране на йерархия на онас-ледяването. Проектантът трябва да бъде готов да премине през много повторения. Проектирането на йерархии е област на активни изследвания и все още няма съгласие относно това какви правила трябва да се следват при проектирането.

Упражнение 8-1. Сред виртуалните функции, въведени в дефиницията на Bear, има една, която не е на своето място. Коя е тя? Къде трябва да бъде поместена? Защо?

Упражнение 8-2. Параграф 7.4 представя първоначалната дефиниция на ZooAnimal, Bear и Panda. Тези дефиниции са използвани за илюстрация на механизма на извличането и не е необходимо да бъдат пример за най-добрия проект на йерархията ZooAnimal. Предефинирайте дефинициите на класовете, включително множеството от виртуалните функции и образците на X( const X& ) и operator = ( const X& ).

Упражнение 8-3. Проектирайте отново член функцията debug() на йерархията Shapes, описана в Параграф 7.4 да бъде виртуална член функция.

Упражнение 8-4. Реализирайте отново нечлен функцията debug(), описана в Параграф 7.7 да осъществява виртуална реализация на член функцията debug() на йерарйията Shapes.

Упражнение 8-5. Реализирайте виртуалната функция draw() за йерархията Shapes.

Упражнение 8-6. Реализирайте виртуалната функция reSize() за йерархията Shapes.

Виртуални деструктори

Свързаният списък от елементи на ZooAnimal, подаден на finalCollage(), не е нужен повече, след като завърши изпълнението на функцията. Обикновено се добавя for-loop от следния вид в края на finalCollage() за освобождаването на памет:

for ( ZooAnimal *p = pz->next; p;pz = p, p = p->next )

delete pz;

За нещастие тази стратегия не върши работа. Явното изтриване на pz предизвиква прилагането на деструктора на ZooAnimal към обект, сочен от pz. Обаче обектът може да не е ZooAnimal, а някакъв след това извлечен тип клас като Bear, трябва да бъде извикан дестукторът от действителния тип на класа, сочен от указателя. Но всяко явно извикване отново връща към неприятностите, които възникват при използването на детайли по приложението на йерархията на извличането.

for ( ZooAnimal *p = pz->next; p;pz = p, p = p->next )

switch( pz->isA() )

{ case BEAR:// direct invocation of destructor

((Bear*)pz)->Bear::~Bear();break;

case PANDA:// indirect invocation through delete

delete (Panda *) pz;break;

//... more cаses go here

}

Определянето на деструкторите в йерархията на извличането като виртуални осигурява извикването на нужния деструктор при прилагането на delete към указател към един клас. Следователно, едно ръководно правило е: деструкторът на един абстрактен клас винаги трябва да се определя като виртуален.

Упражнение 8-7. Дефинирайте деструкторите на йерархията Shapes като виртуални.

Извикване на виртуални функции

Виртуална функция се вика чрез указател към даден тип клас или негов псевдоним. Множеството от потенциални виртуални функции, които могат да бъдат да извиквани при всяко обръщение, се състои от:

- oбразецът, дефиниран от викащия тип клас

- тези образци, които са предефинирани от следващи извличания.

Образецът на виртуалната функция, който се изпълнява при всяко обръщение, се определя от действителния тип на класа, сочен от указателя или псевдонима.

Най-всобхватния тип на клас, чрез който се извиква виртуална функция е абстрактният базов клас на йерархията на извличането в нашия пример, това е указател или псевдоним от тип ZooAnimal. ZooAnimal има достъп до цялата верига на онаследяването. Това е целта на дефинирането на абстрактен суперклас.

За илюстрация на извикването на виртуални функции нека да направим едно опростено множество от дефиниции на класове. ZooAnimal ще дефинира две виртуални функции - print() и isA() - и образецна виртуален деструктор.

#include <sstrea.h>

enum ZooLocs { ZOOANIMAL, BEAR, PANDA };

class ZooAnimal

{

public:ZooAnimal( char *s = "ZooAnimal" );

virtual ~ZooAnimal() { delete name; }

void link( ZooAnimal* );

virtual void print( ostream& );

virtual void isA( ostream& );

protected:

char *name;

ZooAnimal *next;

};

#include <string.h>

ZooAnimal::ZooAnimal( char *s ) : next( 0 )

{

name = new char[ strlen(s) + 1 ];

strcpy( name, s );

}

Идеята да се построи смесен лист от извличания от ZooAnimal, свързани с член next. Действителният тип клас на листа не е нужно да бъде известен на програмиста; механизмът на виртуалните функции ще определи типа на клас за всеки елемент.

Функцията link() на ZooAnimal приема аргумент от тип ZooAnimal* и го присвоява на next. Това е реализирано по следния начин:

void ZooAnimal::link( ZooAnimal *za )

{

za->next = next;

next = za;}

isA() и print() са реализирани като виртуални функции. Всяко следващо извличане ще дефинира свой образец на тези две функции. isA() съобщава своя тип клас; print() усъвършенства представянето на типа клас. Всяка една от двете функции приема като аргумент псевдоним на ostream. Ето реализацията:

void ZooAnimal::isA( ostream& os )

{

os << "ZooAnimal name : "<< name << "\n";}

void ZooAnimal::print( osream& os )

{

isA( os ); // virtual invocation

}

Една от целите на нашия проект е поддържането вход-изход за всеки тип клас, който е член на йерархията на онаследяване ZooAnimal. За да се постигне това, трябва да презаредим оператора за изход да възприема аргумент псевдоним на ZooAnimal. Тази функция е реализирана по следния начин:

#include <stream.h>

ostream&

operator << ( ostream& os, ZooAnimal& za )

{za.print( os );return os;}

Сега програмистът може да пренасочи всеки член на йерархията на онаследяване ZooAnimal към оператор за изход и има извикана коректната виртуална функция print(). Ще видим един пример за това след като дефинираме типовете класове Bear и Panda. Забележете, че операторът функция не е направен friend за ZooAnimal. Няма необходимост той да бъде обявен като friend тъй като неговият достъп е ограничен с публичния интерфейс на ZooAnimal.Дефиницията на класа Bear изглежда по следния начин:

class Bear : public ZooAnimal

{

public:

Bear( char *s = "Bear", ZooLocs loc = BEAR,char *sci = "Ursidae" );~

Bear() { delete sciName; }

void print( ostream& );

void isA( ostream& );

protected:

char *sciName; // scientific nameZooLocs zooArea;

};

#include <string.h>

Bear::Bear( char *s, ZooLocs loc, char *sci ) : ZooAnimal( s ), zooArea( loc )

{

sciName = new char[ strlen(sci) + 1 ]; strcpy( sciName, sci );

}

Bear въвежда два допълнителни члена данни:* sciName, научното наименование на животното.* zooArea, мястото в зоопарка, обитавано от животното.

Виртуалните образци на isA() и print() на Bear дават отражение върху представянето му. Те са реализирани по следния начин:

void Bear::isA( ostream& os )

{

ZooAnimal::isA( os ); // static invocation

os << "\tscientific name:\t";

os << sciName << "\n";

}

static char *locTable[] =

{

"The entire animal display area", // ZOOANIMAL

"NorthWest : B1 : area Brown", // BEAR

"NorthWest : B1.P : area BrownSpots" // PANDA

// ... and so on

};

void Bear::print( ostream& os )

{ ZooAnimal::print( os ); // static invocation

os << "\tZoo Area Location:\n\t";

os << locTable[ zooArea ] << "\n";

}

Има три случая, в които се прави извикване на виртуална функция статично по време на компилиране:

1. Когато виртуалната функция се вика чрез обект от типа клас. В следващия откъс от програма, например, функцията isA() се вика чрез обекта za от тип ZooAnimal и това става статично. А викането на isA() чрез указател pz към обект от типа ZooAnimal се разглежда като виртуално обръщение.

#include "ZooAnimal.h"

main()

{

ZooAnimal za;

ZooANimal *pz;

// ...

za.isA( cout ); // nonvirtual invocation

(*pz).isA( cout ); // virtual invocation}

2. Когато виртуалната функция се вика явно чрез указател или псевдоним с използването на оператора за класов обхват. Например,

#include <stream.h>

#include "Bear.h"

#include "ZooAnimal.h"

main()

{

Bear yogi ( "cartoon Bear", BEAR,"ursus cartoons" );

ZooAnimal circus( "circusZooAnimal" );

ZooAnimal *pz;

pz = &circus;cout << "virtual : ZooAnimal::print()\n";

pz->print( cout );

pz = &yogi;cout << "\nvirtual : Bear::print()\n";

pz->print( cout );

cout << "\nnonvirtual : ZooAnimal::print()\n";

cout << "note : isA() is invoked virtually\n";

pz->ZooAnimal::print( cout );}

При компилация и изпълнение се получава следното:

virtual : ZooAnimal::print()

ZooAnimal name : circus

ZooAnimal virtual : Bear::print()

ZooAnimal name : cartoon Bear

scientific name : ursus cartoonus

Zoo Area Location:NorthWest : B1 : area Brown

nonvirtual : ZooAnimal::print()

note: isA() is invoked virtually

ZooAnimal name: cartoon Bear

scientific name: ursus cartoonus

3. Когато виртуалната функция се вика в рамките на конструктора или деструктора на базовия клас. И в двата случая си вика образецът на виртуалната функция в базовия клас, тъй като обектът от извлечения клас или не е създаден още, или вече е изтрит.

Panda въвежда два допълнителни члена данни: indName, името на отделното животно и cell, клетката, в която живее то. Ето дефиницията на Panda:

#include <stream.h>

class Panda :

public Bear

{ public:

Panda( char *nm, int room, char *s = "Panda",char *sci =

"Ailuropoda Melaoleuca", ZooLocs loc = PANDA );

~Panda() { delete indName; }

void print( ostream& );

void isA( ostream& );

protected:

char *indName; // name of individual animal

int cell;

};

#include <string.h>

PandaMadanda( char *nm, int room, char *s,char *sci, ZooLocs

loc ): Bear( s, loc, sci ), cell( room )

{ indName = new char [strlen(nm) + 1];

strcpy( indName, nm );}

Виртуалните образци на isA() и print(), които Panda осигурява, влияят върху представянето му. Те са реализирани по следния начин:

void Panda::isA( ostream& os )

{

Bear::isA( os );

os << "\twe call our friend:\t";

os << indName << "\n";

}

void Panda::print( os )

{

Bear::print( os );

os << "\tRoom Location:\t";

os << cell << "\n";

}

Сега да приложим всичко това в няколко примера. Първият ни пример показва виртуално извикване на print() чрез псевдоним на ZooAnimal. Всеки обект от класа се подава на презаредения образец на оператора за изход (“<<”). Всяко обръщение към

za.print( os );

в рамките на образец на оператора за изход извиква виртуалния образец, дефиниран чрез действителния тип клас на za.

#include <iostream.h>

#include "ZooANimal.h"

#include "Bear.h"

#include "Panda.h"

ZooANimal circus( "circusZooAnimal");

Bear yogi("cartoon Bear",BEAR,"ursus cartoonsus");

Panda yinYang("Yin Yang",1001,"Giant Panda");

main()

{

cout << "Invokation by a ZooAnimal object:\n"

<< circus << "\n";

cout << "\nInvokation by a Bear object:\n"

<< yogi << "\n";

cout << "\nInvokation by a Panda object:\n"

<< yinYang << "\n";

};

При компилация и изпълнение се получава следното:

Invokation by a ZooAnimal object:

ZooAnimal name: circusZooAnimal

Invokation by a Bear object: ZooAnimal

name: cartoon Bear

scientific name: ursus cartoonus Zoo

Area Location:NorthWest:

B1: area BroWn

Invokation by a Panda object: ZooAnimal

name: Giant Panda

scientific name: Ailuropoda Melaoleucawe

call our friend: Yin YangZoo

Area Location:NorthWest: B1.P:

area BrownSpotsRoom Location: 1001

Следващият пример показва работа директно с указатели към обекти. Той вика виртуалната функция isA().

#include <stream.h>

#include "ZooANimal.h"

#include "Bear.h"

#include "Panda.h"

ZooAnimal circus( "circusZooAnimal" );

Bear yogi("cartoon Bear", BEAR, "ursus cartoonus");

Panda yinYang("Yin Yang", 1001, "Giant Panda");

main()

{

ZooAnimal *pz;

pz = &circus;

cout << "virtual: ZooAnimal::isA():\n";

pz->isA( cout );

pz = &yogi;cout << "\nvirtual: Bear::isA():\n";

pz->isA( cout );pz = &yinYang;

cout << "\nvirtual: Panda::isA():\n";

pz->isA( cout );

}

При компилация и изпълнение се получава следното:

virtual: ZooAnimal::isA():

ZooAnimal name: circusZooAnimal

virtual: Bear::isA():

ZooAnimal name: cartoon Bear

scientific name: ursus cartoonus

virtual: Panda::isA():

ZooAnimal name: Giant Panda

scientific name: Ailuropoda Melaoleuca

we call our friend: Yin Yang

В следващия пример действителният тип на класа, адресиран чрез pz се разпознава по време на компилация. Нека оставим виртуалния ме-ханизъм и да извикаме всяка функция статично:

#include <stream.h>

#include "ZooAnimal.h"

#include "Bear.h"

#include "Panda.h"

ZooAnimal circus( "circusZooAnimal");

Bear yogi("cartoon Bear", BEAR, "ursus cartoonus");

Panda yinYang("Yin Yang", 1001, "Giant Panda");

main()

{

ZooAnimal *pz = &yinYang;

cout << "Nonvirtual invokation of Panda::isA():\n";

((Panda*)pz) ->Panda::isA( cout );

pz = &yogi;

cout << "\nNonvirtual invokation of Bear::isA():\n";

((Bear*)pz) ->Bear::isA( cout );

}

Невиртуалното извикване на Panda::isA() чрез указател към ZooAnimal изисква явно обръщение. ZooAnimal няма представа за следващите извличания от класа Panda; това е част от виртуалния механизъм, а не част от самия базов клас. При компилация и изпълне-ние се получава следното:

Nonvirtual invocation of Panda::isA():

ZooAnimal name: Giant Panda

scientific name: Ailoropoda Melaoleucawe

call our friend: Yin Yang

Nonvirtual invocation of Bear::isA():

ZooAnimal name: cartoon Bear

scientific name: ursuss cartoonus

Този последен пример отпечатва смесен списък от указатели към ZooAnimal. Той е реализиран с използването на следната нечлен функция print() - print() трябва да има достъп до членовете данни на ZooAnimal, които не са public, затова трябва да бъде обявена като friend за ZooAnimal.

#include <iostream.h>

#include "ZooAnimal.h"

void print( ZooAnimal *pz, ostream &os = cout )

{

while( pz )

{

pz->print( os );

os << "\n";

pz = pz->next;

}

За нашия програмен пример имаме нужда от указател към ZooAnimal, който да служи като глава на свързания списък:

ZooAnimal *headPptr = 0;

main() дефинираме по следния начин:

#include <stream.h>

#include "ZooAnimal.h"

extern ZooAnimal makeList( ZooAnimal );

ZooAnimal *headPtr = 0;

main()

{ cout << "A Program to Illustrate Virtual Functions\n";

headPtr = makeList( headPtr );

print( headPtr );

}

makeList(), на свой ред, е дефинирана по следния начин:

#include <stream.h>

#include "ZooAnimal.h"

#include "Bear.h"

#include "Panda.h"

ZooAnimal circus( "circusZooAnimal" );

Bear yogi("cartoon Bear",BEAR,"ursus cartoonus");

Panda yinYang("Yin Yang",1001,"Giant Panda");

Panda rocky("Rocky",943,"Red Panda","Ailurus fulgens");

ZooAnimal *makeList( ZooAnimal *ptr )

{// for simplicity, hand code list

ptr = &yinYang;ptr->link( &circus );

ptr->link( &yogi );

ptr->link( &rocky );

return ptr;

}

При компилация и изпълнение се получава следното:

A Program to Illustrate Virtual Functions

ZooAnimal name: Giant Panda

scientific name: Ailuropoda Melaoleuca

we call our friend: Yin YangZoo

Area Location:NorthWest: B1.P:

area BrownSpotsRoom Location: 1001

ZooAnimal name: Red Panda

scientific name: Ailurus fulgens

we call our friend: RockyZoo

Area Location:NorthWest: B1.P:

area BrownSpotsRoom Location: 943

ZooAnimal name: cartoon Bear

scientific name: ursus cartoons

Zoo Area Location:NorthWest: B1:

area Brown

ZooAnimal name: circusZooAnimal

С изключение на makeList(), програмата не взема под внимание детайлите по реализацията на обектите, членовете функции или йерархията на онаследяването. Макар и пределно прост, примерът показва нещо съществено за обектно-ориентираното програмиране.

Упражнение 8-8. Реализирайте нечлен функцията draw(), която има аргумент от тип Shape*. Тя трябва да чертае окръжност, правоъгълен триъгълник и правоъгълник.

Упражнение 8-9. Реализирайте нечлен функцията reSize(), която има аргумент от тип Shape& (ще бъде нужен също аргумент за размер). Приложете последователно draw(), reSize() и след това draw() за окръжност, равностранен триъгълник и квадрат.

Упражнение 8-10. Прибавете виртуален образец на draw(), която пише вобект от клас Screen.

Упражнение 8-11. Реализирайте виртуалната функция save(), която записва обект от йерархията Shape на ostream и restore(), която чете изхода от save().



8.3 Виртуални базови класове

Макар че един базов клас може легално да се появи само веднъж в един списък с извличания, той може да се появи много пъти в рамките на йерархията на извличането. Това може да стане причина за двузначностна членовоте.

Например, има дебати, и то разгорещени, в средите на зоолозите, продължаващи повече от сто години, относно това дали Panda принадлежи на фамилията Raccoon или на фамилията Bear. От компютърна гледна точка, най-доброто решение е Panda да се извлича и от двете фамилии.

class Panda: public Bear, public Raccoon {... }

Panda онаследява базовия клас ZooAnimal едновременно от Bear и от Raccoon; в Panda има две части за базовия клас. Декларирането на обект от класа Panda води до извличането на два конструктора на ZooAnimal; в следния ред:

ZooAnimal(); // base class of Bear

Bear(); // first Panda base

class ZooAnimal(); // base class of Raccoon

Raccoon(); // second Panda base

class Panda(); // derived class constructor is always last

За да можем да обсъдим този въпрос, нека да направим опростена дефиниция на ZooAnimal:

class ZooAnimal

{ // simplified definition

public:

void locate();

protected:

short zooArea;

};

class Bear : public ZooAnimal { /* ... */ }

class Raccoon : public ZooAnimal { /* ... */ }

Panda съдържа две множества от членове данни на ZooAnimal: eдин член данни zooArea, наследен чрез Raccoon, и един, наследен чрез Bear. Фигура 8.1 показва това:

Многократно онаследяване на базови класове

Има ненужна двузначност. Потребителят трябва да познава детайлите от многократното извличане, за да може да направи еднозначно обръщение. Освен това, се разхищава памет заради пространството, в което се съхранява образецът на допълнителния базов клас.

Panda се нуждае само от един образец на базовия клас ZooAnimal. Концептуално, Panda може да се разглежда като един насочен граф без цикли със споделен образец на базовия клас ZooAnimal. По подразбиране, обаче, механизмът за онаследяване дефинира дървовидна йерархия на извличането, в която всяка поява на базов клас поражда образец набазов клас. (виж Фигура 8.2 )

Необходимо е да се намери начин подразбиращият се механизъм на онаследяването да се пренебрегва. Трябва да бъде възможно да се определи споделен базов клас в йерархията на извличането (тоест, да може да се дефинира структурата насочен граф без цикли). В противен случай част от потребителите (възможно е твърде малка) ще бъде затруднена при използването на многократното онаследяване. Методът за пренебрегване на подразбиращия се механизъм на онаследяването, наречен “Виртуални базови класове”, дава възможност на проектантът на класовете да дефинира споделен базов клас. Независимо от това колко често даден виртуален базов клас се среща в йерархията на извличането, генерира се само един негов образец.

Например, Panda ще съдържа само един образец на споделения базов клас ZooAniaml. Достъпът до членовте на класа вече не е двузначен.

Дефиниция на виртуален базов клас

Един базов клас се определя като виртуален чрез добавянето на клю-човата дума virtual към неговата декларация. Например, следващата декларация прави ZooAnimal виртуален базов клас на Bear и Raccoon.

// placement order of keywords public

// and virtual is not significant

class Bear : public virtual ZooAnimal { /* ... */ }

class Raccoon : virtual public ZooAnimal { /* ... */ }

Ако един виртуален базов клас дефинира конструктор, конструкторът не трябва да изисква списък от аргументи - това трябва да бъде конструктор без аргументи или конструктор, за който е определена стойност по премълчаване за всеки един от аргументите му. Освен това, няманужда да бъде променяна дефиницията на ZooAnimal, за да се определи виртуалния базов клас. Ето дефиницията на ZooAnimal, с която ще работим, дискутирайки виртуалните базови класове: Това изискване вече е било премахвано веднъж от езика, макар че AT&T 2.0 версията на С++ го въвежда. Според сегашните правила, ако се изисква конструктор по подразбиране, но такъв няма, се предизиква грешка по време на компилация. По-ранното ограничение е било въведено за да предпазва от грешка по време на компилация.

class ZooAnimal { // simplified definition

public:

ZooAnimal() { zooArea = 0; name = 0; }

ZooAnimal( char*, short );

void locate();

protected:

short zooArea;

char *name; };

По същия начин се използват обектите от класовете Bear и Raccoon. Един виртуален базов клас се инициализира както невиртуален базов клас:

Bear::Bear( char *nm ): ZooAnimal( nm, BEAR ) { ... }

Raccoon::Raccoon( char *nm )

: ZooAnimal( nm, RACCOON ) { ... }

Декларацията на Panda изглежда по същия начин както нейнияневиртуален образец:

class Panda : public Bear, public Raccoon { ... };

Тъй като и Bear, и Raccoon декларират ZooAnimal като виртуален базов клас, всеки обект на Panda вече ще съдържа само един образец на ZooAnimal. Обикновено един извлечен клас може явно да инициализирасамо преките си базови класове. Например, в невиртуално извличанеот ZooAnimal Panda може да не вика по име ZooAnimal в списъказа инициализация на конструктора си. Виртуалните базови класовеса едно изключение. Ще обясним защо.Panda съдържа единствен образец на ZooAnimal, споделян отRaccoon и Bear. Raccoon и Bear, обаче, едновременно явно инициализират ZooAnimal. Образец на Panda не може да бъде инициализиран двукратно. Един виртуален базов клас се инициализира от класа, който е в действителност извличан. Например, не Raccoon или Bear, а Pandaе в действителност извлечен от ZooAnimal. Panda може явно да инициализира ZooAnimal в списъка за инициалиация на своя конструктор. Ако конструкторът на Panda не инициализира явно ZooAnimal, извиква се конструкторът по подразбиране на ZooAnimal. Никога не се прави инициализиране на ZooAnimal част в обект от типа Panda чрез Raccoon и Bear.

Конструкторът на Panda може да бъде дефиниран по следния начин:

PandaMadanda( char *nm ): ZooAnimal( nm, PANDA ),Bear( nm ), Raccoon( nm ) { ... }

PandaMadanda( char *nm ): Bear( nm ), Raccoon( nm ) { ... }

извиква конструктора по подразбиране на ZooAnimal.

Достъп до членовете на виртуален базов клас

Всеки обект от класовете Raccoon и Bear поддържа свое собствено множество от членове на ZooAnimal, което може да бъде достигнато точно по същия начин както онаследените членове от невиртуални извличания. При едно отделно преглеждане на програмния код не може да се направи разлика между употребата на обект на извличане от виртуален и на невиртуален базов клас; единствената разлика е в начина на отделянето на частта от обеста, съответна на базовия клас.

При невиртуално извличане всеки извлечен обект от класа съдър-жа долепващи се базова и извлечена част на класа (виж Фигура 7.4 ). Обект от класа Bear, например, има базова част ZooAnimal и Bear част. При виртуално извличане всеки обект от извлечения клас съдържа извлечена част и указател към частта, съответна на виртуалния базов клас. Виртуалният базов клас не се съдържа в обекта от извлечения клас. Това е илюстрирано за класовете Bear, Raccoon и Pandaна Фигура 8.3.Един виртуален базов клас се подчинява на същите правила заpublic и private, както и при невиртуално извличане:

- онаследените членове при public виртуално извличане запазватнивото си на достъп - public или protected - в извлечения клас.

- онаследените членове при private виртуално извличане стават private членове на извлечения клас.

Какво става, когато еднократно-отдалечено извличане, такова катоPanda, включва едновременно public и private образци на един виртуален базов клас? Например,

class ZooAnimal

{public:

void locate();

protected:

short zooArea;

};

class Bear :

public virtual ZooAnimal { /* ... */ };

class Raccoon :

private virtual ZooAnimal { /* ... */ };

class Panda :

public Bear, public Raccoon { /* ... */ };

class Raccoon : public virtual ZooAnimal

class Bear : public virtual ZooAnimal

Фигура 8.3 Представяне на виртуален базов клас

Panda наследява само едно копие на zooArea и locate(), защото ZooAnimal е виртуален базов клас. Въпросът е дали на Panda е разрешен достъпа до тези членове? При public онаследяване Panda има достъп до тези два члена на ZooAnimal. Обаче, те не са достъпни за Panda при private онаследяване. Кой вид онаследяване се взема впредвид при виртуално извличане? Вярно ли е следното обръщение?

Panda p;p.locate();

Да, обръщението е вярно. Взема се впредвид public онаследяването. Panda има достъп до zooArea и locate(). Като цяло, независимо от брояна образците на виртуалните базови класове в йерархията на онаследяването, ако съществува отделен public образец, един споделен образец при извличане се разглежда като public.

ZooAnimal дефинира член функция locate(), която Raccoon наследява. Обръщението

Bear b;

b.locate(); // Bear instance

вика образеца на Bear, a не този на ZooAnimal. При невиртуално извличане обръщението

Panda p;

p.locate(); // which locate? e двузначно, ако Panda не дефинира свой собствен образец на locate(). Ако обаче ZooAnimal е виртуален базов клас едновременно за Raccoon и Bear,се вика образеца locate() на Bear. Двузначността е премахната. При виртуално извличане се взема същинският извлечен образец на член функцията. Затова се взема образеца locate() на Bear, а не този на ZooAnimal, наследен в Raccoon.

Ред на конструкторите и деструкторите

Виртуалните базови класове се конструират преди невиртуалните базови класове независимо от това къде се разположени в списъка с извличанията. Ако даден клас има два базови класа, и единият от двата е виртуален, винаги се вика пръв конструктора на виртуалния базов клас. Деструкторът на виртуалния базов клас се вика последен. Например,

class TeddyBear : public Bear,public virtual ToyAnimal { ... };

TeddyBear pooh;

Извикването на конструкторите за pooh става в следния ред:

ToyAnimal(); // virtual base

classZooAnimal(); // base class of Bear;

Bear(); // nonvirtual base

class TeddyBear();

Редът на извикване на деструкторите е обратен.

Ако един клас има повече виртуални базови класове, техните конструктори се викат по реда, по който са декларирани виртуалните базови класове. Като имаме в предвид това, можем да кажем, че се следва един нормален начин на извикване на конструкторите за един обектна извлечен клас.

По-сложен е случаят, в който виртуалният базов клас е nestedв йерархията на онаследяването. Например, Panda съдържа споделенияот Bear и Raccoon виртуален базов клас ZooAnimal. Откриването на присъствието на виртуален базов клас изисква претърсване на целия граф на онаследяването на извлечения клас. Например, действителната дефиниция на Panda не предполага, че съществува виртуален базов клас:

class Panda : public Endangered, public Herbivore,

public Raccoon, public Bear { ... };

Panda yin Yang;

Действителното виртуално извличане от ZooAnimal се появява много по-назад в графа на онаследяването, когато се дефинират Raccoon и Bear. За yinYang редът на обръщенията към конструкторите е следния:

ZooAnimal(); // virtual base class

Herbivore(); // base class declaration

orderEndangered();

Raccoon();

Bear();

Panda();

За да се открие присъствието на виртуален базов клас, се претърсва йерархията на онаследяването по реда на декларациите на базовите класове. Например, представеното тук извличане на Panda, се претърсва по следния ред:

Endangered

Herbivore*

Raccoon*

Bear

Ako Endangered също съдържаше виртуален базов клас, редът заизвикване на конструкторите за повече базови класоове щеше да бъде както реда на търсене в йерархията. Например, виртуален базов клас, свързан с Endangered, се вика преди виртуалният базов клас ZooAnimal, свързан едновременно с Bear и Raccoon. Редът на извикване на деструкторите е обратен.

Едновременно използване на виртуални и невиртуални образци

Когато едно извличане съдържа едновременно виртуални и невиртуални образци на базов клас, създава се по един обект от базовия клас за всеки невиртуален образец и друг обект на базовия клас за всички виртуални образци. Например, ако се промени Endangered да бъде невиртуално извлечен от ZooANimal, Panda ще съдържа два образецана ZooAnimal, виртуалния образец от Bear и Raccoon и невиртуалния образец от Endangered.

Отново съществуват два или повече образци на онаследените членове от базов клас. Изискват се псевдоними, които да указват кой точно образец се има впредвид.

Резюме

По подразбиране механизмът на онаследяване дефинира дърво на йерархията на онаследяването, в което всяка поява на базов клас поражда образец на базовия клас. Виртуалните базови класове оси-гуряват начин за пренебреване на този подразбиращ се механизъм. Независимо от това колко често един виртуален базов клас се срещав йерархията на извличането, генерира се само един негов образец. Виртуалният базов клас служи като единствен, споделен образец.

В нашия пример ZooAnimal е виртуален базов клас едновременно наBear и Raccoon. Panda, извлечен от Bear и Raccoon, съдържа единствения споделен образец на ZooAnimal.


Дипломен проект на тема "Проектиране на обучаваща система в Web среда" по дисциплината "Програмни езици и системи"
Разработена е от с-на к-т Иван Маринов Калчев, под ръководството на к-н Дойчинов.
За нейната разработка е използван офис пакета на Microsoft – Office 2000.
В обучаващата система е представен програмния език C++. Тя има за цел бързото и лесно запознаване със синтаксиса на този програмен език, като той е описан в осем глави.




/ Трябва да сте регистриран за да напишете коментар /