Багато класів подібні із класом employee тим, що в них можна дати розумне визначення віртуальним функціям. Однак, є й інші класи. Деякі, наприклад, клас shape, представляють абстрактне поняття (фігура), для якого не можна створити об'єкти. Клас shape набуває сенсу тільки як базовий клас у деякому похідному класі. Причиною є те, що неможливо дати осмислене визначення віртуальних функцій класу shape:
class shape {
// ...
public:
virtual void rotate (int) { error ("shape:: rotate"); }
virtual void draw () { error ("shape:: draw"): }
// не можна не обертати, не малювати абстрактну фігуру
// ...
};
Створення об'єкта типу shape (абстрактної фігури) законна, хоча зовсім безглузда операція:
shape s; // нісенітниця: ''фігура взагалі''
Вона безглузда тому, що будь-яка операція з об'єктом s приведе до помилки.
Краще віртуальні функції класу shape описати як чисто віртуальні. Зробити віртуальну функцію чисто віртуальної можна, додавши ініціалізатор = 0:
class shape {
// ...
public:
virtual void rotate (int) = 0; // чисто віртуальна функція
virtual void draw () = 0; // чисто віртуальна функція
};
Клас, у якому є віртуальні функції, називається абстрактним. Об'єкти такого класу створити не можна:
shape s; // помилка: змінна абстрактного класу shape
Абстрактний клас можна використати тільки в якості базового для іншого класу:
class circle: public shape {
int radius;
public:
void rotate (int) { } // нормально:
// перевизначення shape:: rotate
void draw (); // нормально:
// перевизначення shape:: draw
circle (point p, int r);
};
Якщо чиста віртуальна функція не визначається в похідному класі, то вона й залишається такою, а значить похідний клас теж є абстрактним. При такому підході можна реалізовувати класи поетапно:
class X {
public:
virtual void f () = 0;
virtual void g () = 0;
};
X b; // помилка: опис об'єкта абстрактного класу X
class Y: public X {
void f (); // перевизначення X:: f
};
Y b; // помилка: опис об'єкта абстрактного класу Y
class Z: public Y {
void g (); // перевизначення X:: g
};
Z c; // нормально
Абстрактні класи потрібні для завдання інтерфейсу без уточнення яких-небудь конкретних деталей реалізації. Наприклад, в операційній системі деталі реалізації драйвера пристрою можна сховати таким абстрактним класом:
class character_device {
public:
virtual int open () = 0;
virtual int close (const char*) = 0;
virtual int read (const char*, int) =0;
virtual int write (const char*, int) = 0;
virtual int ioctl (int. .) = 0;
// ...
};
Дійсні драйвери будуть визначатися як похідні від класу character_device.
Можливість мати більше одного базового класу спричиняє можливість кількаразового входження класу як базового. Припустимо, класи task і displayed є похідними класу link, тоді в satellite (зроблений на їх основі) він буде входити двічі:
class task: public link {
// link використається для зв'язування всіх
// завдань у список (список диспетчера)
// ...
};
class displayed: public link {
// link використається для зв'язування всіх
// зображуваних об'єктів (список зображень)
// ...
};
Але проблем не виникає. Два різних об'єкти link використаються для різних списків, і ці списки не конфліктують один з одним. Звичайно, без ризику неоднозначності не можна звертатися до членів класу link, але як це зробити коректно, показано в наступному розділі. Графічно об'єкт satellite можна представити так:
Але можна привести приклади, коли загальний базовий клас не повинен представлятися двома різними об'єктами.
Природно, у двох базових класів можуть бути функції-члени з однаковими іменами:
class task {
// ...
virtual debug_info* get_debug ();
};
class displayed {
// ...
virtual debug_info* get_debug ();
};
При використанні класу satellite подібна неоднозначність функцій повинна бути дозволена:
void f (satellite* sp)
{
debug_info* dip = sp->get_debug (); // помилка: неоднозначність
dip = sp->task:: get_debug (); // нормально
dip = sp->displayed:: get_debug (); // нормально
}
Однак, явний дозвіл неоднозначності клопітно, тому для її усунення найкраще визначити нову функцію в похідному класі:
class satellite: public task, public derived {
// ...
debug_info* get_debug ()
{
debug_info* dip1 = task: get_debug ();
debug_info* dip2 = displayed:: get_debug ();
return dip1->merge (dip2);
}
};
Тим самим локалізується інформація з базових для satellite класів. Оскільки satellite:: get_debug () є перевизначенням функцій get_debug () з обох базових класів, гарантується, що саме вона буде викликатися при всякім звертанні до get_debug () для об'єкта типу satellite.
Транслятор виявляє колізії імен, що виникають при визначенні того самого імені в більш, ніж одному базовому класі. Тому програмістові не треба вказувати яке саме ім'я використається, крім випадку, коли його використання дійсно неоднозначно. Як правило використання базових класів не приводить до колізії імен. У більшості випадків, навіть якщо імена збігаються, колізія не виникає, оскільки імена не використаються безпосередньо для об'єктів похідного класу.
Якщо неоднозначності не виникає, зайво вказувати ім'я базового класу при явному звертанні до його члена. Зокрема, якщо множинне успадкування не використовується, цілком достатньо використати позначення типу "десь у базовому класі". Це дозволяє програмістові не запам'ятовувати ім'я прямого базового класу й рятує його від помилок (втім, рідких), що виникають при перебудові ієрархії класів.
void manager:: print ()
{
employee:: print ();
// ...
}
передбачається, що employee - прямій базовий клас для manager. Результат цієї функції не зміниться, якщо employee виявиться непрямим базовим класом для manager, а в прямому базовому класі функції print () немає. Однак, хтось міг би в такий спосіб перешикувати класи:
class employee {
// ...
virtual void print ();
};
class foreman: public employee {
// ...
void print ();
};
class manager: public foreman {
// ...
void print ();
};
Тепер функція foreman:: print () не буде викликатися, хоча майже напевно передбачався виклик саме цієї функції. За допомогою невеликої хитрості можна перебороти ці труднощі:
class foreman: public employee {
typedef employee inherited;
// ...
void print ();
};
class manager: public foreman {
typedef foreman inherited;
// ...
void print ();
};
void manager:: print ()
{
inherited:: print ();
// ...
}
Правила областей видимості, зокрема ті, які ставляться до вкладених типів, гарантують, що виниклі кілька типів inherited не будуть конфліктувати один з одним. Взагалі ж справа смаку, використовувати рішення з типом inherited наочним чи ні.
У попередніх розділах множинне спадкування розглядалося як істотного фактора, що дозволяє за рахунок злиття класів безболісно інтегрувати незалежно, що створювалися програми. Це саме основне застосування множинного спадкування, і, на щастя (але не випадково), це найпростіший і надійний спосіб його застосування.
Іноді застосування множинного спадкування припускає досить тісний зв'язок між класами, які розглядаються як "братні" базові класи. Такі класи-брати звичайно повинні проектуватися спільно. У більшості випадків для цього не потрібен особливий стиль програмування, що істотно відрізняється від того, котрий ми тільки що розглядали. Просто на похідний клас покладається деяка додаткова робота. Звичайно вона зводиться до перевизначення однієї або декількох віртуальних функцій. У деяких випадках класи-брати повинні мати загальну інформацію. Оскільки С++ - мову зі строгим контролем типів, спільність інформації можлива тільки при явній вказівці того, що є загальним у цих класах. Способом такої вказівки може служити віртуальний базовий клас.
Віртуальний базовий клас можна використати для подання "головного" класу, що може конкретизуватися різними способами:
class window {
// головна інформація
virtual void draw ();
};
Для простоти розглянемо тільки один вид загальної інформації із класу window - функцію draw (). Можна визначати різні більше розвинені класи, що представляють вікна (window). У кожному визначається своя (більше розвинена) функція малювання (draw):
class window_w_border: public virtual window {
// клас "вікно з рамкою"
// визначення, пов'язані з рамкою
void draw ();
};
class window_w_menu: public virtual window {
// клас "вікно з меню"
// визначення, пов'язані з меню
void draw ();
};
Тепер хотілося б визначити вікно з рамкою й меню:
class Clock: public virtual window,
public window_w_border,
public window_w_menu {
// клас "вікно з рамкою й меню"
void draw ();
};
Кожний похідний клас додає нові властивості вікна. Щоб скористатися комбінацією всіх цих властивостей, ми повинні гарантувати, що той самий об'єкт класу window використається для подання входжень базового класу window у ці похідні класи. Саме це забезпечує опис window у всіх похідних класах як віртуального базового класу.