Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
+ООП_Навч_посібник.doc
Скачиваний:
7
Добавлен:
01.07.2025
Размер:
6.58 Mб
Скачать

15.7. Віртуальні базові класи

Під час успадкування декількох базових класів у С++-програму може бути внесений елемент невизначеності. Для розуміння сказаного розглянемо таку некоректну програму, яка містить помилку і не скомпілюється.

Код програми 15.16. Демонстрація невизначеності під час успадкування декількох базових класів

#include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

class baseClass { // Оголошення базового класу

public:

int izm;

};

// Клас derivedA успадковує клас baseClass.

class derivedA : public baseClass {

public:

int jzm;

};

// Клас derivedB успадковує клас baseClass.

class derivedB : public baseClass {

public:

int kzm;

};

/* Клас derivedC успадковує обидва класи derivedA і derivedB.

Це означає, що у класі derivedC існує дві копії класу baseClass! */

class derivedC : public derivedA, public derivedB {

public:

int sum;

};

int main()

{

derivedC C_ob;

C_ob.izm = 10; // Це і є неоднозначність: який саме

// член izm маємо на увазі???

C_ob.jzm = 20;

C_ob.kzm = 30;

// І тут теж неоднозначність з членом izm.

C_ob.sum = C_ob.izm + C_ob.jzm + C_ob.kzm;

// І тут теж неоднозначність з членом izm.

cout << C_ob.izm << " ";

cout << C_ob.jzm << " " << C_ob.kzm << " ";

cout << C_ob.sum;

getch(); return 0;

}

Як зазначено у коментарях до цієї програми, обидва класи derivedA і derivedB успадковують клас baseClass. Але клас derivedC успадковує як клас derivedA, так і клас derivedB. У результаті в об'єкті типу derivedC присутні дві копії класу baseClass, тому у такому виразі

C_ob.izm = 20;

не зрозуміло, на яку саме копію члена izm тут дане посилання: на член, успадкований від класу derivedA або від класу derivedB? Оскільки в об'єкті C_ob є обидві копії класу baseClass, то у ньому існують і два члени C_ob.izm! Через те ця настанова izm є успадкуванням неоднозначним (істотно невизначеним).

Є два способи виправити попередню програму. Перший полягає у застосуванні оператора дозволу контексту (дозволи області видимості), за допомогою якого можна "вручну" вказати потрібного члена izm. Наприклад, наступна версія цієї програми успішно скомпілюється та виконається так, як очікувалося.

Код програми 15.17. Демонстрація механізму використання оператора дозволу контексту для вибору потрібного члена

#include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

class baseClass { // Оголошення базового класу

public:

int izm;

};

// Клас derivedA успадковує клас baseClass.

class derivedA : public baseClass {

public:

int jzm;

};

// Клас derivedB успадковує клас baseClass.

class derivedB : public baseClass {

public:

int kzm;

};

/* Клас derivedC успадковує обидва класи derivedA і derivedB.

Це означає, що у класі derivedC існує дві копії класу baseClass! */

class derivedC : public derivedA, public derivedB {

public:

int sum;

};

int main()

{

derivedC C_ob;

C_ob.derivedA::izm = 10; // Контекст дозволений, використовується

// член izm класу derivedA.

C_ob.jzm = 20;

C_ob.kzm = 30;

// Контекст дозволений і тут.

C_ob.sum = C_ob.derivedA::izm + C_ob.jzm + C_ob.kzm;

// Виникнення неоднозначності ліквідована і тут.

cout << C_ob.derivedA::izm << " ";

cout << C_ob.jzm << " " << C_ob.kzm << " ";

cout << C_ob.sum;

getch(); return 0;

}

Застосування оператора "::" дає змогу програмі "ручним способом" вибрати версію класу baseClass (успадковану класом derivedA). Але після такого рішення виникає цікаве запитання: а що, коли насправді потрібна тільки одна копія класу baseClass? Чи можна якимсь чином запобігти включенню двох копій у клас derivedC? Відповідь, як, напевно, Ви здогадалися, позитивна. Це рішення досягається за допомогою віртуальних базових класів.

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

Віртуальне успадкування базового класу гарантує, що в будь-якому похідному класі наявна тільки одна його копія.

Для ілюстрації цього процесу наведемо ще одну версію попередньої програми. Цього разу клас derivedC містить тільки одну копію класу baseClass.

Код програми 15.18. Демонстрація механізму використання віртуальних базових класів

#include <iostream> // Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

class baseClass { // Оголошення базового класу

public:

int izm;

};

// Клас derivedA успадковує клас baseClass як віртуальний.

class derivedA : virtual public baseClass {

public:

int jzm;

};

// Клас derivedB успадковує клас baseClass як віртуальний.

class derivedB : virtual public baseClass {

public:

int kzm;

};

/* Клас derivedC успадковує обидва класи derivedA і derivedB.

Цього разу він містить тільки одну копію класу baseClass. */

class derivedC : public derivedA, public derivedB {

public:

int sum;

};

int main()

{

derivedC C_ob;

C_ob.izm = 10; // Тепер неоднозначності немає.

C_ob.jzm = 20;

C_ob.kzm = 30;

// Тепер неоднозначності немає.

C_ob.sum = C_ob.izm + C_ob.jzm + C_ob.kzm;

// Тепер неоднозначності немає.

cout << C_ob.izm << " ";

cout << C_ob.jzm << " " << C_ob.kzm << " ";

cout << C_ob.sum;

getch(); return 0;

}

Як бачите, ключове слово virtual передує решті частини специфікації успадкованого класу. Тепер обидва класи derivedA і derivedB успадковують клас baseClass як віртуальний, і тому при будь-якому багаторазовому їх успадкуванні у похідний клас буде включено тільки одну його копію. Отже, у класі derivedC є тільки одна копія класу baseClass, а настанова C_ob.izm = 10 тепер є абсолютно допустимою і не містить ніякої неоднозначності.

Необхідно пам'ятати! Навіть якщо обидва класи derivedA і derivedB задають клас baseClass як virtual-клас, він, як і раніше, присутній в об'єкті будь-якого типу.

Наприклад, така послідовність настанов є цілком допустимою:

// Оголошення похідного класу типу derivedA.

derivedA myClass;

myClass.izm = 88;

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

15.8. Читання С++-графів успадкування

Іноді ієрархії С++-класів зображують графічно, що полегшує їх розуміння. Але деколи різні способи зображення графів успадкування класів вводять новачків у оману. Розглянемо, наприклад, ситуацію, у якій клас А успадковується класом В, який, своєю чергою, успадковується класом С. Використовуючи стандартну С++-систему позначень, цю ситуацію можна відобразити так, як це показано на рис. 15.1, а.

а)

б)

Рис. 15.1. Графічне представлення ієрархії С++-класів успадкування

Як бачите, стрілки на цьому рисунку спрямовані вгору, а не вниз. Багато хто спочатку вважає такий напрям стрілок алогічним, але саме цей стиль прийнятий більшістю С++-програмістів. Згідно зі стильовою графікою C++, стрілка повинна вказувати на базовий клас Отже, стрілка означає "виведений з", а не "породжує". Розглянемо інший приклад, у якому спробуємо самостійно описати словами значення зображення, яке показано на рис. 15.1, б.

З цього графа виходить, що клас Е виведений з обох класів С і D. Іншими словами, клас Е має два базові класи С і D. При цьому клас С виведений з класу А, а клас D – з класу В. Незважаючи на те, що напрям стрілок спочатку може видатися для Вас незвичним, все ж таки краще познайомитися з цим стилем графічних позначень, оскільки він широко використовується у книгах, журналах і документації на компілятори.

Розділ 16. Віртуальні функції та поліморфізм

Одною з трьох основних особливостей об'єктно-орієнтованого програмування є поліморфізм. Стосовно мови програмування C++, під терміном поліморфізм розуміють опис процесу, у якому різні результати реалізації функції можуть бути доступні за допомогою одного і того ж імені. З цієї причини поліморфізм іноді характеризується фразою "один інтерфейс, багато методів". Це означає, що до всіх функцій-членів загального класу можна отримати доступ одним і тим самим способом, незважаючи на можливу відмінність у конкретних діях, пов'язаних з кожною окремою операцією.

Поліморфізм – це назва процесу, у якому різні результати реалізації функції доступні за допомогою одного її імені.

У мові програмування C++ поліморфізм підтримується як у процесі виконання, так у період компілювання програми. Перевантаження операторів і функцій – це приклади поліморфізму, що належить до моменту компілювання. Але, попри потужність механізму перевантаження операторів і функцій, він не у змозі вирішити всі завдання, які виникають в реальних додатках об'єктно-орієнтованої мови програмування. Тому у мові C++ також реалізований поліморфізм періоду виконання, який базується на використанні похідних класів і віртуальних функцій, що і становлять основні теми цього розділу.

Почнемо ж розділ з короткого опису покажчиків на похідні типи, оскільки саме вони забезпечують підтримку динамічного поліморфізму.