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