void Z2:: f (Y1* py1, Y2* py2, Y3* py3)
{
X* px = py1; // нормально: X - загальний базовий клас Y1
py1->a = 7; // нормально
px = py2; // нормально: X - захищений базовий клас Y2, // а Z2 - похідний клас Y2
py2->a = 7; // нормально
px = py3; // помилка: X - приватний базовий клас Y3
py3->a = 7; // помилка
}
Нарешті, розглянемо:
class Y3: private X { void f (); };
Оскільки X - приватний базовий клас Y3, тільки друзі й члени Y3 можуть при необхідності перетворювати (неявно) Y3* в X*. Крім того вони можуть звертатися до загальних і захищених членів класу X:
void Y3:: f (Y1* py1, Y2* py2, Y3* py3)
{
X* px = py1; // нормально: X - загальний базовий клас Y1
py1->a = 7; // нормально
px = py2; // помилка: X - захищений базовий клас Y2
py2->a = 7; // помилка
px = py3; // нормально: X - приватний базовий клас Y3, // а Y3:: f член Y3
py3->a = 7; // нормально
}
Якщо визначити функції operator new () і operator delete (), керування пам'яттю для класу можна взяти у свої руки. Це також можна, (а часто й більш корисно), зробити для класу, що служить базовим для багатьох похідних класів. Допустимо, нам потрібні були свої функції розміщення й звільнення пам'яті для класу employee ($$6.2.5) і всіх його похідних класів:
class employee {
// ...
public:
void* operator new (size_t);
void operator delete (void*, size_t);
};
void* employee:: operator new (size_t s)
{
// відвести пам'ять в 's' байтів
// і повернути покажчик на неї
}
void employee:: operator delete (void* p, size_t s)
{
// 'p' повинне вказувати на пам'ять в 's' байтів,
// відведену функцією employee:: operator new ();
// звільнити цю пам'ять для повторного використання
}
Призначення до цієї пори загадкового параметра типу size_t стає очевидним. Це - розмір об'єкта, що звільняє. При видаленні простого службовця цей параметр одержує значення sizeof (employee), а при видаленні керуючого - sizeof (manager). Тому власні функції класи для розміщення можуть не зберігати розмір кожного розташованого об'єкта. Звичайно, вони можуть зберігати ці розміри (подібно функціям розміщення загального призначення) і ігнорувати параметр size_t у виклику operator delete (), але тоді навряд чи вони будуть краще, ніж функції розміщення й звільнення загального призначення.
Як транслятор визначає потрібний розмір, якому треба передати функції operator delete ()? Поки тип, зазначений в operator delete (), відповідає щирому типу об'єкта, все просто; але розглянемо такий приклад:
class manager: public employee {
int level;
// ...
};
void f ()
{
employee* p = new manager; // проблема
delete p;
}
У цьому випадку транслятор не зможе правильно визначити розмір. Як і у випадку видалення масиву, потрібна допомога програміста.
Він повинен визначити віртуальний деструктор у базовому класі employee:
class employee {
// ...
public:
// ...
void* operator new (size_t);
void operator delete (void*, size_t);
virtual ~employee ();
};
Навіть порожній деструктор вирішить нашу проблему:
employee:: ~employee () { }
Тепер звільнення пам'яті буде відбуватися в деструкторі (а в ньому розмір відомий), а будь-який похідний від employee клас також буде змушений визначати свій деструктор (тим самим буде встановлений потрібний розмір), якщо тільки користувач сам не визначить його. Тепер наступний приклад пройде правильно:
void f ()
{
employee* p = new manager; // тепер без проблем
delete p;
}
Розміщення відбувається за допомогою (створеного транслятором) виклику
employee:: operator new (sizeof (manager))
а звільнення за допомогою виклику
employee:: operator delete (p,sizeof (manager))
Іншими словами, якщо потрібно мати коректні функції розміщення й звільнення для похідних класів, треба або визначити віртуальний деструктор у базовому класі, або не використати у функції звільнення параметр size_t. Звичайно, можна було при проектуванні мови передбачити засоби, що звільняють користувача від цієї проблеми. Але тоді користувач "звільнився" би й від певних переваг більше оптимальної, хоча й менш надійної системи.
У загальному випадку, завжди має сенс визначати віртуальний деструктор для всіх класів, які дійсно використаються як базові, тобто з об'єктами похідних класів працюють і, можливо, видаляють їх, через покажчик на базовий клас:
class X {
// ...
public:
// ...
virtual void f (); // в X є віртуальна функція, тому
// визначаємо віртуальний деструктор
virtual ~X ();
};
Довідавшись про віртуальні деструктори, природно запитати: "Чи можуть конструктори те ж бути віртуальними?" Якщо відповісти коротко - немає. Можна дати більше довга відповідь: "Ні, але можна легко одержати необхідний ефект".
Конструктор не може бути віртуальним, оскільки для правильної побудови об'єкта він повинен знати його тип. Більше того, конструктор - не зовсім звичайна функція. Він може взаємодіяти з функціями керування пам'яттю, що неможливо для звичайних функцій. Від звичайних функцій-членів він відрізняється ще тим, що не викликається для існуючих об'єктів. Отже не можна одержати вказівник на конструктор.
Але ці обмеження можна обійти, якщо визначити функцію, що містить виклик конструктора й повертає побудований об'єкт. Це вдало, оскільки нерідко буває потрібно створити новий об'єкт, не знаючи його реального типу. Наприклад, при трансляції іноді виникає необхідність зробити копію дерева, що представляє вираз, що розбирається. У дереві можуть бути вузли виражень різних видів. Припустимо, що вузли, які містять повторювані у вираженні операції, потрібно копіювати тільки один раз. Тоді нам буде потрібно віртуальна функція розмноження для вузла вираження.
Як правило "віртуальні конструктори" є стандартними конструкторами без параметрів або конструкторами копіювання, параметром яких служить тип результату:
class expr {
// ...
public:
expr (); // стандартний конструктор
virtual expr* new_expr () { return new expr (); }
};
Віртуальна функція new_expr () просто повертає стандартно ініціалізований об'єкт типу expr, розміщений у вільній пам'яті. У похідному класі можна перевизначити функцію new_expr () так, щоб вона повертала об'єкт цього класу:
class conditional: public expr {
// ...
public:
conditional (); // стандартний конструктор
expr* new_expr () { return new conditional (); }
};
Це означає, що, маючи об'єкт класу expr, користувач може створити об'єкт в "точності такого ж типу":
void user (expr* p1, expr* p2)
{
expr* p3 = p1->new_expr ();
expr* p4 = p2->new_expr ();
// ...
}
Змінним p3 і p4 привласнюються вказівники невідомого, але підходящого типу.
Тим же способом можна визначити віртуальний конструктор копіювання, названий операцією розмноження, але треба підійти більш ретельно до специфіки операції копіювання:
class expr {
// ...
expr* left;
expr* right;
public:
// ...
// копіювати 's' в 'this'
inline void copy (expr* s);
// створити копію об'єкта, на який дивиться this
virtual expr* clone (int deep = 0);
};
Параметр deep показує розходження між копіюванням властивому об'єкту (поверхневе копіювання) і копіюванням усього піддерева, коренем якого служить об'єкт (глибоке копіювання). Стандартне значення 0 означає поверхневе копіювання.
Функцію clone () можна використати, наприклад, так:
void fct (expr* root)
{
expr* c1 = root->clone (1); // глибоке копіювання
expr* c2 = root->clone (); // поверхневе копіювання
// ...
}
Будучи віртуальної, функція clone () здатна розмножувати об'єкти будь-якого похідного від expr класу. Дійсне копіювання можна визначити так:
void expr:: copy (expression* s, int deep)
{
if (deep == 0) { // копіюємо тільки члени
*this = *s;
}
else { // пройдемося по вказівником:
left = s->clone (1);
right = s->clone (1);
// ...
}
}
Функція expr:: clone () буде викликатися тільки для об'єктів типу expr (але не для похідних від expr класів), тому можна просто розмістити в ній і повернути з її об'єкт типу expr, що є власною копією:
expr* expr:: clone (int deep)
{
expr* r = new expr (); // будуємо стандартне вираження
r->copy (this,deep); // копіюємо '*this' в 'r'
return r;
}
Таку функцію clone () можна використати для похідних від expr класів, якщо в них не з'являються члени-дані (а це саме типовий випадок):
class arithmetic: public expr {
// ...
// нових член-член-даних немає =>
// можна використати вже певну функцію clone
};
З іншого боку, якщо додані члени-дані, то потрібно визначати власну функцію clone ():
class conditional: public expression {
expr* cond;
public:
inline void copy (cond* s, int deep = 0);
expr* clone (int deep = 0);
// ...
};
Функції copy () і clone () визначаються подібно своїм двійникам з expression:
expr* conditional:: clone (int deep)
{
conditional* r = new conditional ();
r->copy (this,deep);
return r;
}
void conditional:: copy (expr* s, int deep)
{
if (deep == 0) {
*this = *s;
}
else {
expr:: copy (s,1); // копіюємо частину expr
cond = s->cond->clone (1);
}
}
Визначення останньої функції показує відмінність дійсного копіювання в expr:: copy () від повного розмноження в expr:: clone () (тобто створення нового об'єкта й копіювання в нього). Просте копіювання виявляється корисним для визначення більш складних операцій копіювання й розмноження. Розходження між copy () і clone () еквівалентно розходженню між операцією присвоювання й конструктором копіювання і еквівалентно розходженню між функціями _draw () і draw (). Відзначимо, що функція copy () не є віртуальною. Їй і не треба бути такою, оскільки віртуальна викликаюча її функція clone (). Очевидно, що прості операції копіювання можна також визначати як функції-підстановки.
Звичайно в програмах використовуються об'єкти, що є конкретним поданням абстрактних понять. Наприклад, у С++ тип даних int разом з операціями +, - , *, / і т.д. реалізує (хоча й обмежено) математичне поняття цілого. Звичайно з поняттям зв'язується набір дій, які реалізуються в мові у вигляді основних операцій над об'єктами, що задають у стислому, зручному й звичному виді. На жаль, у мовах програмування безпосередньо представляється тільки мале число понять. Так, поняття комплексних чисел, алгебри матриць, логічних сигналів і рядків у С++ не мають безпосереднього вираження. Можливість задати подання складних об'єктів разом з набором операцій, котрі виконуються над такими об'єктами, реалізують у С++ класи. Дозволяючи програмістові визначати операції над об'єктами класів, ми одержуємо більше зручну й традиційну систему позначень для роботи із цими об'єктами в порівнянні з тієї, у якій всі операції задаються як звичайні функції. Приведемо приклад: