48870 (608819), страница 4
Текст из файла (страница 4)
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.
1.14.8 Множинне входження базового класу
Можливість мати більше одного базового класу спричиняє можливість кількаразового входження класу як базового. Припустимо, класи task і displayed є похідними класу link, тоді в satellite (зроблений на їх основі) він буде входити двічі:
class task: public link {
// link використається для зв'язування всіх
// завдань у список (список диспетчера)
// ...
};
class displayed: public link {
// link використається для зв'язування всіх
// зображуваних об'єктів (список зображень)
// ...
};
Але проблем не виникає. Два різних об'єкти link використаються для різних списків, і ці списки не конфліктують один з одним. Звичайно, без ризику неоднозначності не можна звертатися до членів класу link, але як це зробити коректно, показано в наступному розділі. Графічно об'єкт satellite можна представити так:
Але можна привести приклади, коли загальний базовий клас не повинен представлятися двома різними об'єктами.
1.14.9 Вирішення неоднозначності
Природно, у двох базових класів можуть бути функції-члени з однаковими іменами:
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 наочним чи ні.
1.14.10 Віртуальні базові класи
У попередніх розділах множинне спадкування розглядалося як істотного фактора, що дозволяє за рахунок злиття класів безболісно інтегрувати незалежно, що створювалися програми. Це саме основне застосування множинного спадкування, і, на щастя (але не випадково), це найпростіший і надійний спосіб його застосування.
Іноді застосування множинного спадкування припускає досить тісний зв'язок між класами, які розглядаються як "братні" базові класи. Такі класи-брати звичайно повинні проектуватися спільно. У більшості випадків для цього не потрібен особливий стиль програмування, що істотно відрізняється від того, котрий ми тільки що розглядали. Просто на похідний клас покладається деяка додаткова робота. Звичайно вона зводиться до перевизначення однієї або декількох віртуальних функцій. У деяких випадках класи-брати повинні мати загальну інформацію. Оскільки С++ - мову зі строгим контролем типів, спільність інформації можлива тільки при явній вказівці того, що є загальним у цих класах. Способом такої вказівки може служити віртуальний базовий клас.
Віртуальний базовий клас можна використати для подання "головного" класу, що може конкретизуватися різними способами:
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 у всіх похідних класах як віртуального базового класу.
Можна в такий спосіб зобразити состав об'єкта класу window_w_border_and_menu:
Щоб побачити різницю між звичайним і віртуальним спадкуванням, зрівняєте цей малюнок з малюнком, що показує состав об'єкта класу satellite. У графі спадкування кожний базовий клас із даним ім'ям, що був зазначений як віртуальний, буде представлений єдиним об'єктом цього класу. Навпроти, кожний базовий клас, що при описі спадкування не був зазначений як віртуальний, буде представлений своїм власним об'єктом.
Тепер треба написати всі ці функції draw (). Це не занадто важко, але для необережного програміста тут є пастка. Спочатку підемо найпростішим шляхом, що саме до неї й веде:
void window_w_border:: draw ()
{
window:: draw ();
// малюємо рамку
}
void window_w_menu:: draw ()
{
window:: draw ();
// малюємо меню
}
Поки всі добре. Все це очевидно, і ми додержуємося зразка визначення таких функцій за умови єдиного спадкування, що працював прекрасно. Однак, у похідному класі наступного рівня з'являється пастка:
void clock:: draw () // пастка!
{
window_w_border:: draw ();
window_w_menu:: draw ();
// тепер операції, що ставляться тільки
// до вікна з рамкою й меню
}
На перший погляд все цілком нормально. Як звичайно, спочатку виконуються всі операції, необхідні для базових класів, а потім ті, які ставляться властиво до похідних класів. Але в результаті функція window:: draw () буде викликатися двічі! Для більшості графічних програм це не просто зайвий виклик, а псування картинки на екрані. Звичайно друга видача на екран затирає першу.
Щоб уникнути пастки, треба діяти не так поспішно. Ми відокремимо дії, виконувані базовим класом, від дій, виконуваних з базового класу. Для цього в кожному класі введемо функцію _draw (), що виконує потрібні тільки для нього дії, а функція draw () буде виконувати ті ж дії плюс дії, потрібні для кожного базового класу. Для класу window зміни зводяться до введення зайвої функції:
class window {
// головна інформація
void _draw ();
void draw ();
};
Для похідних класів ефект той же:
class window_w_border: public virtual window {
// клас "вікно з рамкою"
// визначення, пов'язані з рамкою
void _draw ();
void draw ();
};
void window_w_border:: draw ()
{
window:: _draw ();
_draw (); // малює рамку
};
Тільки для похідного класу наступного рівня проявляється відмінність функції, що і дозволяє обійти пастку з повторним викликом window:: draw (), оскільки тепер викликається window:: _draw () і тільки один раз:
class clock
: public virtual window,
public window_w_border,
public window_w_menu {
void _draw ();
void draw ();
};
void clock:: draw ()
{
window:: _draw ();
window_w_border:: _draw ();
window_w_menu:: _draw ();
_draw (); // тепер операції, що ставляться тільки
// до вікна з рамкою й меню
}
Не обов'язково мати обидві функції window:: draw () і window:: _draw (), але наявність їх дозволяє уникнути різних простих описок.
У цьому прикладі клас window служить сховищем загальної для window_w_border і window_w_menu інформації й визначає інтерфейс для спілкування цих двох класів.
Якщо використається єдине спадкування, то спільність інформації в дереві класів досягається тим, що ця інформація пересувається до кореня дерева доти, поки вона не стане доступна всім зацікавленим у ній вузловим класам.
У результаті легко виникає неприємний ефект: корінь дерева або близькі до нього класи використаються як простір глобальних імен для всіх класів дерева, а ієрархія класів вироджується в безліч незв'язаних об'єктів.
Істотно, щоб у кожному із класів-братів перевизначалися функції, певні в загальному віртуальному базовому класі. У такий спосіб кожний із братів може одержати свій варіант операцій, відмінний від інших. Нехай у класі window є загальна функція уведення get_input ():
class window {
// головна інформація
virtual void draw ();
virtual void get_input ();
};