asyan.org
добавить свой файл
1
Віртуальні методи

Робота з об'єктами частіше за все проводиться через покажчики. Покажчику на ба­зовий клас можна присвоїти значення адреси об'єкту будь-якого похідного класу, наприклад:

// Описується покажчик на базовий клас:

monstr *p;

// Покажчик посилається на об'єкт похідного класу:

р = new daemon;

Виклик методів об'єкту відбувається відповідно до типу покажчика, а не фак­тичним типом об'єкту, на який він посилається, тому при виконанні оператора, наприклад,

p->draw(l, 1.1, 1);

буде викликаний метод класу monstr, а не класу daemon, оскільки посилання на методи дозволяються під час компоновки програми. Цей процес називається раннім скріпленням. Щоб викликати метод класу daemon, можна використовувати явне перетворення­ типу покажчика:

(daemon * p)->draw(l, 1, 1, 1);

Це не завжди можливо, оскільки в різний час покажчик може посилатися на об'єкти різних класів ієрархії, і під час компіляції програми конкрет­ний клас може бути невідомий. Можна навести як приклад функ­цію, параметром якої є покажчик на об'єкт базового класу. На його місце під час виконання програми може бути переданий покажчик на будь-який похідний клас. Інший приклад — зв'язний список покажчиків на різні об'єкти ієрархії, з яким вимагається працювати одноманітно.

В C++ реалізований механізм пізнього скріплення, коли дозвіл посилань на метод відбувається на етапі виконання програми залежно від конкретно­го типу об'єкту, який викликав метод. Цей механізм реалізований за допомогою вірту­альних методів. Для визначення віртуального методу використовується специфікатор virtual, на­приклад:

virtual void draw(int x, int у, int scale, int роsition);

Правила опису і використовування віртуальних методів.

1. Якщо в базовому класі метод визначений як віртуальний, метод визначений в похідному класі з тим же ім'ям і набором параметрів, автоматично стає віртуальним, а з відмінним набором параметрів — звичайним. Віртуальні методи успадковуються, тобто перевизначати їх в похідному класі потрібно тільки при необхідності задати відмінні дії. Права доступу при перевизначенні змінити не можна.

2. Якщо віртуальний метод перевизначений в похідному класі, об'єкти цього класу можуть отримати доступ до методу базового класу за допомогою операції доступу до області видимості. Віртуальний метод не може оголошуватися з модифікатором static, але може бути оголошений як дружній.

3. Якщо в класі вводиться опис віртуального методу, він повинен бути визначений хоча б як чисто віртуальний. Чисто віртуальний метод містить ознаку - 0 замість тіла, наприклад:

virtual void f(int)= 0;

Чисто віртуальний метод повинен перевизначатися в похідному класі (­можливо, знову як чисто віртуальний).

4. Якщо визначити метод draw в класі monstr як віртуальний, рішення про те, ме­тод якого класу викликати, ухвалюватиметься залежно від типу об'єкту, на який посилається покажчик:

monstr *r *р;

r - new monstr; // Створюється об'єкт класу monstr

р - new daemon; // Створюється об'єкт класу daemon

r->draw(l, 1, 1, 1); // Викликається метод monstr::draw

p->draw(l, 1, 1, 1); // Викликається метод daemon::draw

p-> monstr::draw(l. 1, 1, 1); // Обхід механізму віртуальних методів

Якщо об'єкт класу daemon викликатиме метод draw не безпосередньо, а кос­вено (тобто з іншого методу, визначеного в класі monstr), буде викликаний метод draw класу daemon. Отже, віртуальним називається метод, посилання на який дозволяється на етапі виконання програми.
^ Механізм пізнього скріплення

Для кожного класу (не об'єкту!), який містить хоча б один віртуальний ме­тод, компілятор створює таблицю віртуальних методів (vtbl), в якій для ко­жного віртуального методу записана його адреса в пам'яті. Адреси методів методів в таблиці в порядку їх опису в класах. Адреса будь-якого віртуального методу має в vtbl один і той же зсув для кожного класу в межах ієрар­хії.

Кожний об'єкт містить приховане додаткове поле посилання на vtbl, яке має назву vptr. Воно заповнюється конструктором при створенні об'єкту (для цього ком­пілятор додає в початок тіла конструктора відповідні інструкції). На етапі компіляції посилання на віртуальні методи замінюються на звернення до vtbl через vptr об'єкту, а на етапі виконання в момент звернення до методу його адреса вибирається з таблиці. Таким чином, виклик віртуального методу, на відміну від звичайних методів і функцій, виконується через додатковий етап отримання адреси методу з таблиці. Це дещо уповільнює виконання програми.

Рекомендується робити віртуальними деструктори для того, щоб гаранту­вати правильне звільнення пам'яті з-під динамічного об'єкту, оскільки в цьому випадку у будь-який момент часу буде вибраний деструктор, який відповідає фактичному типу об'єкту. Деструктор передає операції delete розмір об'єкту, який має тип size_t. Якщо об'єкт який видаляється, є похідним і в ньому не визначений віртуальний деструктор, розмір об'єкту який передається мо­же виявитися невірним.

Чіткого правила, по якому метод слідує робити віртуальним, не існує. Можна тільки дати рекомендацію оголошувати віртуальними методи, для яких є вірогідність, що вони будуть перевизначені в похідних класах. Методи, які у всій ієрархії залишаться незмінними або ті, якими похідні класи користуватися не будуть, робити віртуальними немає змісту. З другого боку, при проектуванні ієрархії не завжди можна передбачити, яким чином розширятимуться базові класи (особливо при проектува­нні бібліотек класів), а оголошення методу віртуальним забезпечує гнучкість і можливість розширення.

Для пояснення останньої тези уявимо собі, що виклик методу draw робиться з методу переміщення об'єкту. Якщо текст методу переміщення не залежить від типу переміщуваного об'єкту (оскільки принцип переміщення всіх

об'єктів однаковий), перевизначати цей метод в похідних класах немає необхідності, і він може бути опи­сан як невіртуальний. Якщо метод draw віртуальний, метод переміщення зможе без перекомпіляції працювати з об'єктами будь-яких похідних класів — навіть тих, про які при його написанні нічого відомо не було. Віртуальний механізм працює тільки при використанні покажчиків або посилань на об'єкти. Об'єкт, визначений через покажчик або посилання і який містить віртуальні методи, називається поліморфним. В даному випадку полімор­фізм полягає в тому, що за допомогою одного і того ж звернення до методу виконуються різні дії залежно від типа, на який посилається покажчик в кожний момент часу.
^ Абстрактні класи

Клас, який містить хоча б один чисто віртуальний метод, називається абст­рактним. Абстрактні класи призначені для представлення загальних понять, які передбачається конкретизувати в похідних класах. Абстрактний клас може використовуватися тільки як базовий для інших класів — об'єкти абстрактного класу створювати не можна, оскільки прямий або непрямий виклик чисто віртуального методу приводить до помилки при виконанні. При визначенні абстрактного класу необхідно мати на увазі наступне:

1. Абстрактний клас не можна використовувати при явному приведенні типів, для опису типу параметра і типу значення яке повертається функцією;

2. Допускається оголошувати покажчики і посилання на абстрактний клас, якщо при ініціалізації не вимагається створювати тимчасовий об'єкт;

3. Якщо клас, похідний від абстрактного, не визначає всі чисто виртуаль­ні функції, він також є абстрактним.

Таким чином, можна створити функцію, параметром якої є покажчик на абстрактний клас. На місці цього параметра при виконанні програми може передаватися покажчик на об'єкт будь-якого похідного класу. Це дозволяє створювати поліморфні функції, які працюють з об'єктом будь-якого типу в межах однієї ієрархії.

^ Множинне спадкоємство

Множинне спадкоємство означає, що клас має декілька базових класів. Якщо в базових класах є однойменні елементи, при цьому може виникнути конфлікт ідентифікаторів, який усувається за допомогою операції доступу до області бачення:

class monstr

{

public: int get_health():

};

class hero

{

public: int get_health();

......

};

class ostrich: public monstr, public hero

{

......

};

int main()

{

ostrich А;

cout << A.monstr::get_health();

cout << A.hero::get_health();

}

Як видно з прикладу, для виклику методу get_hea1th вимагається явно вказувати клас, в якому він описаний. Використання звичайної для виклику методу класу конструкції A.get_health() приведе до помилки, оскільки компілятор не в змозі розібратися, до методу якого з базових класів потрібно звернутися.

Якщо в базових класів є загальний предок, це приведе до того, що похідний від цих базових клас успадковуватиме два екземпляри полів предка, що частіше за все є небажаним. Щоб уникнути такої ситуації, потрібно при спадкоємстві загального предка визначити його як віртуальний клас:

class monstr

{

......

};

class daemon: virtual public monstr

{

........

};

class lady: virtual public monstr

{

.......

};

class baby: public daemon, public lady

{

........

};

Клас baby містить тільки один екземпляр полів класу monstr. Якщо базовий клас успадковується і як віртуальний, і звичайним способом в похідному класі будуть присутні окремі екземпляри для кожного невіртуального вхож­дення і ще один екземпляр для віртуального.

Множинне спадкоємство застосовується для того, щоб забезпечити похідний клас властивостями двох або більше базових. Частіше всього один з цих класів є основним, а інші забезпечують деякі додаткові властивості, тому вони називаються класами підмішування. По можливості класи підмішування повинні бути віртуальними і створюватися за допомогою конструкторів без параметрів, що дозволяє уникнути багатьох проблем, які виникають ромбоподібному спадкоємстві (коли у базових класів є загальний предок).