{ count = с; }
// друк даних
void Increment:: print () const
{
cout << "count = " << count
"", increment = " " increment << endl; }
main ()
{
Increment value (10,5);
cout << "Перед збільшенням: "; value. print ();
for (int j = 1; j <= 3;) }
value. addlncrement ();
cout << "Після збільшення " << j "": "; value. print ();
}
return 0; }
Перед збільшенням: count = 10, increment = 5
Після збільшення 1: count = 15, increment = 5
Після збільшення 2: count = 20, increment = 5
Після збільшення 3: count = 25, increment = 5
Мал.7. Використання ініціалізаторів елементів для ініціалізації даних константного типу убудованого типу
Нехай визначені два класи: vector (вектор) і matrix (матриця). Кожний з них приховує своє подання даних, але дає повний набір операцій для роботи з об'єктами його типу. Допустимо, треба визначити функцію, що множить матрицю на вектор. Для простоти припустимо, що вектор має чотири елементи з індексами від 0 до 3, а в матриці чотири вектори теж з індексами від 0 до 3. Доступ до елементів вектора забезпечується функцією elem (), і аналогічна функція є для матриці. Можна визначити глобальну функцію multiply (помножити) у такий спосіб:
vector multiply (const matrix& m, const vector& v);
{
vector r;
for (int i = 0; i<3; i++) { // r [i] = m [i] * v;
r. elem (i) = 0;
for (int j = 0; j<3; j++)
r. elem (i) +=m. elem (i,j) * v. elem (j);
}
return r;
}
Це цілком природнє рішення, але воно може виявитися дуже неефективним. При кожному виклику multiply () функція elem () буде викликатися 4* (1+4*3) раз. Якщо в elem () проводиться контроль границь масиву, то на такий контроль буде витрачено значно більше часу, ніж на виконання самої функції, і в результаті вона виявиться непридатної для користувачів. З іншого боку, якщо elem () є якийсь спеціальний варіант доступу без контролю, то тим самим ми засмічуємо інтерфейс із вектором і матрицею особливою функцією доступу, що потрібна тільки для обходу контролю.
Якщо можна було б зробити multiply членом обох класів vector і matrix, ми могли б обійтися без контролю індексу при звертанні до елемента матриці, але в той же час не вводити спеціальної функції elem (). Однак, функція не може бути членом двох класів. Треба мати в мові можливість надавати функції, що не є членом, право доступу до приватних членів класу. Функція - не член класу, але має доступ до його закритої частини, називається другом цього класу. Функція може стати другом класу, якщо в його описі вона описана як friend (друг). Наприклад:
class matrix;
class vector {
float v [4];
// ...
friend vector multiply (const matrix&, const vector&);
};
class matrix {
vector v [4];
// ...
friend vector multiply (const matrix&, const vector&);
};
Функція-друг не має ніяких особливостей, за винятком права доступу до закритої частини класу. Зокрема, у такій функції не можна використати вказівник this, якщо тільки вона дійсно не є членом класу. Опис friend є дійсним описом. Воно вводить ім'я функції в область видимості класу, у якому вона була описана, і при цьому відбуваються звичайні перевірки на наявність інших описів такого ж імені в цій області видимості. Опис friend може перебуває як у загальній, так і в приватній частинах класу, це не має значення.
Тепер можна написати функцію multiply, використовуючи елементи вектора й матриці безпосередньо:
vector multiply (const matrix& m, const vector& v)
{
vector r;
for (int i = 0; i<3; i++) { // r [i] = m [i] * v;
r. v [i] = 0;
for (int j = 0; j<3; j++)
r. v [i] +=m. v [i] [j] * v. v [j];
}
return r;
}
Відзначимо, що подібно функції-члену дружня функція явно описується в описі класу, з яким дружить. Тому вона є невід'ємною частиною інтерфейсу класу нарівні з функцією-членом.
Функція-член одного класу може бути другом іншого класу:
class x {
// ...
void f ();
};
class y {
// ...
friend void x:: f ();
};
Цілком можливо, що всі функції одного класу є друзями іншого класу. Для цього є коротка форма запису:
class x {
friend class y;
// ...
};
У результаті такого опису всі функції-члени y стають друзями класу x.
Ця глава присвячена поняттю похідного класу. Похідні класи - це простий, гнучкий і ефективний засіб визначення класу. Нові можливості додаються до вже існуючого класу, не вимагаючи його перепрограмування або перетрансляції. За допомогою похідних класів можна організувати загальний інтерфейс із декількома різними класами так, що в інших частинах програми можна буде одноманітно працювати з об'єктами цих класів. Вводиться поняття віртуальної функції, що дозволяє використати об'єкти належним чином навіть у тих випадках, коли їхній тип на стадії трансляції невідомий. Основне призначення похідних класів - спростити програмістові завдання вираження спільності класів.
Обговоримо, як написати програму обліку службовців деякої фірми. У ній може використатися, наприклад, така структура даних:
struct employee { // службовець
char* name; // ім'я
short age; // вік
short department; // відділ
int salary; // оклад
employee* next;
// ...
};
Поле next потрібно для зв'язування в список записів про службовців одного відділу (employee). Тепер спробуємо визначити структуру даних для керуючого (manager):
struct manager {
employee emp; // запис employee для керуючого
employee* group; // підлеглий колектив
short level;
// ...
};
Керуючий також є службовцем, тому запис employee зберігається в члені emp об'єкта manager. Для людини ця спільність очевидна, але для транслятора член emp нічим не відрізняється від інших членів класу. Вказівник на структуру manager (manager*) не є вказівником на employee (employee*), тому не можна вільно використати один замість іншого. Зокрема, без спеціальних дій не можна об'єкт manager включити до списку об'єктів типу employee. Доведеться або використати явне приведення типу manager*, або в список записів employee включити адресу члена emp. Обоє рішень некрасиві й можуть бути досить заплутаними. Правильне рішення полягає в тому, щоб тип manager був типом employee з деякою додатковою інформацією:
struct manager: employee {
employee* group;
short level;
// ...
};
Клас manager є похідним від employee, і, навпаки, employee є базовим класом для manager. Крім члена group у класі manager є члени класу employee (name, age і т.д.). Графічно відношення спадкування звичайно зображується у вигляді стрілки від похідних класів до базового:
employee
manager
Звичайно говорять, що похідний клас успадковує базовий клас, тому й відношення між ними називається успадкуванням. Іноді базовий клас називають суперкласом, а похідний - підлеглим класом. Але ці терміни можуть викликати здивування, оскільки об'єкт похідного класу містить об'єкт свого базового класу. Взагалі похідний клас більше свого базового в тому розумінні, що в ньому утримується більше даних і визначено більше функцій.
Маючи визначення employee і manager, можна створити список службовців, частина з яких є й керуючими:
void f ()
{
manager m1, m2;
employee e1, e2;
employee* elist;
elist = &m1; // помістити m1 в elist
m1. next = &e1; // помістити e1 в elist
e1. next = &m2; // помістити m2 в elist
m2. next = &e2; // помістити m2 в elist
e2. next = 0; // кінець списку
}
Оскільки керуючий є також службовцем, вказівник manager* можна використати як employee*. У той же час службовець не обов'язково є керуючим, і тому employee* не можна використати як manager*.
У загальному випадку, якщо клас derived має загальний базовий клас base, то вказівник на derived можна без явних перетворень типу привласнювати змінній, що має тип вказівника на base. Зворотне перетворення від вказівника на base до вказівника на derived може бути тільки явним:
void g ()
{
manager mm;
employee* pe = &mm; // нормально
employee ee;
manager* pm = ⅇ // помилка:
// не всякий службовець є керуючим
pm->level = 2; // катастрофа: при розміщенні ee
// пам'ять для члена 'level' не виділялася
pm = (manager*) pe; // нормально: насправді pe
// не настроєно на об'єкт mm типу manager
pm->level = 2; // відмінно: pm указує на об'єкт mm
// типу manager, а в ньому при розміщенні
// виділена пам'ять для члена 'level'
}
Іншими словами, якщо робота з об'єктом похідного класу йде через вказівник, то його можна розглядати як об'єкт базового класу. Зворотне невірно. Відзначимо, що у звичайній реалізації С++ не передбачається динамічного контролю над тим, щоб після перетворення типу, подібного тому, що використовувалося в присвоюванні pe в pm, отриманий у результаті вказівник дійсно був налаштований на об'єкт необхідного типу.
Прості структури даних начебто employee і manager самі по собі не занадто цікаві, а часто й не дуже корисні. Тому додамо до них функції:
class employee {
char* name;
// ...
public:
employee* next; // перебуває в загальній частині, щоб
// можна було працювати зі списком
void print () const;
// ...
};
class manager: public employee {
// ...
public:
void print () const;
// ...
};
Треба відповісти на деякі питання. Яким чином функція-член похідного класу manager може використати члени базового класу employee? Які члени базового класу employee можуть використати функції-члени похідного класу manager? Які члени базового класу employee може використати функція, що не є членом об'єкта типу manager? Які відповіді на ці питання повинна давати реалізація мови, щоб вони максимально відповідали завданню програміста?
Розглянемо приклад:
void manager:: print () const
{
cout << " ім'я " << name << '\n';
}
Член похідного класу може використати ім'я із загальної частини свого базового класу нарівні з усіма іншими членами, тобто без вказівки імені об'єкта. Передбачається, що є об'єкт, на який настроєний this, тому коректним звертанням до name буде this->name. Однак, при трансляції функції manager:: print () буде зафіксована помилка: члену похідного класу не надане право доступу до приватних членів його базового класу, значить name недоступно в цій функції.