Как работят виртуалните методи
Когато се създава обект в производен клас, като например класа Dog, първо се извиква конструкторът на базовия клас, а след това конструкторът на производния клас. Схематично обект от клас Dog е показан на фиг. 11.2. Имайте предвид, че обектът на производен клас се състои от две части, едната от които е създадена от конструктора на базовия клас, а другата от конструктора на производния клас.
Фиг. 11.2. Създаденият обект от клас Dog
Фиг. 11.3. Таблица на виртуалните функции на класа Mammal
Ако в един от обектите се създаде обикновена невиртуална функция, тогава обектът поема пълна отговорност за тази функция. Повечето компилатори създават таблици с виртуални функции, наричани още v-таблици. Такива таблици се създават за всеки тип данни и всеки обект от всеки клас съдържа указател към виртуална функционална таблица (vptr или v-показател).
Въпреки че подробностите за изпълнението на изпълнението на виртуална функция варират между компилаторите, самите виртуални функции ще работят по абсолютно същия начин, независимо от компилатора.
Фиг. 11.4. Таблица с виртуални функции на клас Dog
И така, във всеки обект има vptr указател, който препраща към таблицата с виртуални функции, която от своя страна съдържа указатели към всички виртуални функции. (За повече относно функционалните указатели вижте Урок 14.) Vptr указателят за обект Dog се инициализира, когато се създаде частта от обекта, която принадлежи към основния клас Mammal, както е показано на Фигура 1. 11.3.
След като се извика конструкторът на клас Dog, указателят vptr се настройва да сочи към заменената виртуална функция (ако има такава), която съществува за класа Dog (Фигура 11-4).
В резултат на това, когато използвате указател към класа Mammalуказателят vptr все още препраща към версията на виртуалната функция, която съответства на действителния тип на обекта. Следователно, когато се извиква методът Speak() в предишния пример, функцията, която беше дефинирана в съответния производен клас, беше изпълнена.
Не можете да го понесете, докато сте тук
Ако метод WagTail() е деклариран на обект Dog, който не принадлежи към класа Mammal, тогава не е възможно да получите достъп до този метод с помощта на указател на клас Mammal (освен ако този указател не е изрично преобразуван в указател на клас Dog). Тъй като функцията WagTail() не е виртуална и не принадлежи към класа Mammal, тя може да бъде достъпна само от обект от класа Dog или с помощта на указател към този клас.
Тъй като всички преобразувания са податливи на грешки, създателите на C++ позволиха само изрични преобразувания на типове. Винаги е възможно да преобразувате всеки указател на клас Mammal в указател на клас Dog, но има по-безопасен и по-надежден начин за извикване на метода WagTail(). За да разберете тънкостите на този метод, трябва да овладеете множественото наследяване, което ще бъде обсъдено в следващия урок, или да научите как да работите с шаблони, което ще бъде темата на урок 20.
Раздробяване на предмети
Трябва да се отбележи, че цялата магия на виртуалните функции се проявява само когато се осъществява достъп до тях чрез указатели и препратки. Ако подадете обект като стойност, тогава виртуалната функция не може да бъде извикана. Този проблем е показан в листинг 11-10.
Списък 11.10. Разрязване на обект при предаването му като стойност
1: //Списък 11.10. Разделяне на обект при предаването му като стойност
10: виртуална празнота Speak() const < cout
27: void ValueFunction(Mammal);
28: void PtrFunction(Mammal*);
29: void RefFunction(Mammal&);
36: bool fQuit = false;
37: cout > избор;
39: превключвател (избор)
41: случай 0: fQuit = true;
43: случай 1: ptr = ново куче;
45: случай 2: ptr = нова котка;
47: по подразбиране: ptr = нов бозайник;
59: void ValueFunction(Mammal MammalValue)
64: void PtrFunction(Mammal * pMammal)
69: void RefFunction(Mammal & rMammal)
Резултат:
(1)куче (2)котка (0) Излезте: 1
(1)куче (2)котка (0)Излезте: 2
(1)куче (2)котка (0) Изход: 0
Анализ: Редове 5-25 дефинират класовете бозайник, куче и котка. След това се декларират три функции - PtrFunction(), RefFunction() и ValueFunction(). Те вземат съответно указател на клас Mammal, препратка към клас Mammal и обект на клас Mammal. След това те извършват същата операция - извикват метода Speak ().
Потребителят е подканен да избере обект от клас Dog или клас Cat, след което се създава указател от съответния тип в редове 43-46.
Съдейки по информацията, изведена от програмата на екрана, потребителят за първи път е избрал обект от класа Dog, който е създаден в свободната област на паметта от 43-ия ред на програмата. След това обектът от клас Dog се предава на трите функции чрез указател, чрез препратка и като стойност.
Дерефериран указател предава обект като стойност. В този случай функцията разпознава, че предаваният обект принадлежи към класа Mammal, компилаторът разделя обекта от клас Dog наполовина и използва само частта, която е създадена от конструктора на класа Mammal. В този случай се извиква версията на метода Speak(), който е деклариран за класа Mammal, който се показва в информацията, показвана от програмата на екрана.
След това бяха извършени същите действия и със същия резултатобект от клас Кат.
Виртуални деструктори
В случай, че се очаква указател към обект от базовия клас, е напълно приемливо и често използвано в практиката да се предаде указател към обект от производен клас. Какво се случва при изтриване на указател, който препраща към обект от производен клас? Ако деструкторът е деклариран като виртуален, тогава всичко ще върви добре - ще бъде извикан деструкторът на съответния производен клас. След това деструкторът на производния клас автоматично ще извика деструктора на базовия клас и посоченият обект ще бъде изтрит изцяло.
Това предполага правилото: ако в класа са декларирани виртуални функции, тогава деструкторът също трябва да бъде виртуален.
Конструктор на виртуално копиране
Конструкторите не могат да бъдат виртуални, което означава, че не може да има и конструктор на виртуално копиране. Но понякога искате програмата да може да предава указател към обект от базов клас и да го копира правилно в обект от производен клас. За да постигнете това, трябва да създадете виртуален метод Clone() в базовия клас. Методът Clone() трябва да създаде и върне копие на текущия обект на класа.
Тъй като методът Clone() се отменя в производни класове, когато се извика, се създават копия на обектите, съответстващи на избрания клас. Програмата, която използва този метод, е показана в списък 11-11.
Списък 11.11. Конструктор на виртуално копиране
1: //Списък 11.11. Виртуален дизайнер-копирамейкър
8: Mammal():itsAge(1) < cout >избор;
66: превключвател (избор)
68: case DOG: ptr = ново куче;
70: case CAT: ptr = new Cat;
72: по подразбиране: ptr = нов бозайник;
75: theArray[i] = ptr;
77: Бозайник *OtherArray[NumAnimalTypes];
78: for(i=0;i Speak();
81: OtherArray[i] = theArray[i]->Clone();
83: for(i=0;i Speak();
Резултат:
1: (1)куче (2)котка (3)Бозайник: 1
2: Конструктор бозайник.
3: Конструктор куче.
4: (1)куче (2)котка (3)Бозайник: 2
5: Конструктор бозайник.
6: Конструктор Cat.
7: (1)куче (2)котка (3)Бозайник: 3
8: Конструктор бозайник.
10: Конструктор за копиране на бозайници.
13: Конструктор за копиране на бозайници.
15: Бозайник говори!
16: Конструктор за копиране на бозайници.
19: Бозайник говори!
Анализ: Списък 11-11 е подобен на предишните два списъка, но тази програма добавя един нов виртуален метод, Clone(), към класа Mammal. Този метод връща указател към нов обект от клас Mammal, използвайки конструктор за копиране, чийто параметър е представен от указателя