Namespace CounterNameSpace 3 страница

};

 

// Елементи класу baseClass будуть закриті у межах класу derivedA.

class derivedA : private baseClass {

int kzm;

public:

// Виклики цих функцій цілком законні, оскільки змінні izm та jzm

// є одночасно private-членами класу derivedA.

void setKzm() { kzm = izm*jzm;} // OK

void showKzm() { cout << "kzm= " << kzm << "\n";}

};

 

// Доступ до членів izm, jzm, setBase() і showBase() не успадковується.

class derivedB : public derivedA {

int mzm;

public:

// Неправильно, оскільки члени izm та jzm закриті у межах

// класу derivedA.

void setMzm() { mzm = izm + jzm;} // помилка

void showMzm() { cout << "mzm= " << mzm << "\n";}

};

 

int main()

{

derivedA A_ob; // Створення об'єкта класу

derivedB B_ob; // Створення об'єкта класу

 

A_ob.setBase(1, 2); // Помилка: не можна викликати функцію setBase()

A_ob.showBase(); // Помилка: не можна викликати функцію showBase()

 

B_ob.setBase(3, 4); // Помилка: не можна викликати функцію setBase()

B_ob.showBase(); // Помилка: не можна викликати функцію showBase()

 

getch(); return 0;

}

Незважаючи на те, що клас baseClass успадковується класом derivedA закритим способом, однак клас derivedA має доступ до public- і protected-членів класу baseClass. Однак він не може цей привілей передати далі, тобто вниз за ієрархією класів. Ключове слово protected – це частина мови C++. Воно забезпечує механізм захисту певних елементів класу від модифікації функціями, які не є членами цього класу, але дає змогу передавати їх "за успадкуванням".

Специфікатор protected можна також використовувати стосовно структур. Але його не можна застосовувати до об'єднань, оскільки об'єднання не успадковується іншим класом. Деякі компілятори допускають використання специфікатора protected в оголошенні об'єднання, але, оскільки об'єднання не можуть брати участь в успадкуванні, то у цьому контексті ключове слово protected буде рівносильним ключовому слову private.

Специфікатор захищеного доступу може знаходитися в будь-якому місці оголошення класу, але, як правило, protected-члени оголошуються після (оголошуваних за замовчуванням) private-членів і перед public-членами. Таким чином, найзагальніший формат оголошення класу зазвичай має такий вигляд:

class ім'я_класу {

private-члени

protected:

protected-члени

public:

public-члени

};

Нагадаємо, що розділ захищених членів є необов'язковим.

15.3.2. Використання специфікатора protected для успадкування
базового класу

Специфікатор protected можна використовувати не тільки для надання членам класу статусу "захищеності", але і для успадкування базового класу. Якщо базовий клас успадковується як захищений, то всі його відкриті та закриті члени стають захищеними членами похідного класу. Для розуміння сказаного розглянемо такий приклад.

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

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

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

 

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

int izm;

protected:

int jzm;

public:

int kzm;

void setIzm(int a) { izm = a;}

int getIzm() { return izm;}

};

 

// Успадковуємо клас baseClass як protected-клас.

class derived : protectedbaseClass {

public:

void setJzm(int a) { jzm = а;} // jzm -- тут protected-член

void setKzm(int a) { kzm = а;} // kzm -- тут protected-член

int getJzm() { return jzm;}

int getKzm() { return kzm;}

};

 

int main()

{

derived D_ob; // Створення об'єкта класу

/* Наступний рядок неправомірний, оскільки функція setIzm() є

protected-членом класу derived, що робить його недоступним

за його межами. */

D_ob.setIzm(10);

 

cout << D_ob.getIzm(); // Неправильно, оскільки функція

// getIzm() -- protected-член.

D_ob.kzm = 10; // Неправильно, оскільки змінна kzm - protected-член

 

// Наступні настанови правомірні.

D_ob.setKzm(10);

cout << D_ob.getKzm() << " ";

D_ob.setJzm(12);

cout << D_ob.getJzm() << " ";

 

getch(); return 0;

}

Як зазначено у коментарях до цієї програми, члени (класу baseClass) kzm, jzm, setIzm() і getIzm() стають protected-членами класу derived. Це означає, що до них не можна отримати доступу з коду програми, "прописаного" поза класом derived. Тому посилання на ці члени у функції main() (через об'єкт D_ob) неправомірні.

15.3.3. Узагальнення інформації про використання специфікаторів public, protected і private

Оскільки права доступу, що визначаються специфікаторами public, protected і private, є принциповими для створення програм мовою C++, то є сенс узагальнити все те, що було сказано про ці ключові слова.

Оголошення членів класу:

● у випадку оголошення члена класу відкритим (з використанням ключового слова public), то до нього можна отримати доступ з будь-якої іншої частини програми;

● якщо член класу оголошується закритим (за допомогою специфікатора private), то до нього можуть отримувати доступ тільки члени того ж самого класу. Понад це, до закритих членів базового класу не мають доступу навіть похідні класи;

● якщо член класу оголошується захищеним (тобто protected-членом), то до нього можуть отримувати доступ тільки члени того ж самого класу або похідних від нього. Специфікатор доступу protected дає змогу успадковувати члени, але залишає їх закритими у межах ієрархії класів.

Успадкування базових класів:

● якщо базовий клас успадковується з використанням ключового слова public, то його public-члени стають public-членами похідного класу, а його protected-члени – protected-членами похідного класу;

● якщо базовий клас успадковується з використанням ключового слова private, то його public- і protected-члени стають private-членами похідного класу;

● якщо базовий клас успадковується з використанням специфікатора protected, то його public- і protected-члени стають protected-членами похідного класу;

● в усіх випадках private-члени базового класу залишаються закритими у межах цього класу і не успадковуються.

У міру збільшення досвіду створення програм мовою C++ застосування специфікаторів public, protected і private не завдаватиме Вам клопоту. А поки що, коли ще не вистачає упевненості у правильності використання того або іншого специфікатора доступу, спробуйте написати просту експериментальну програму і проаналізуйте отримані результати.

15.4. Успадкування декількох базових класів

Похідний клас може успадковувати два або більше базових класів. Наприклад, у цій короткій програмі клас derived успадковує обидва класи baseA і baseB.

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

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

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

 

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

protected:

int x;

public:

void showXzm() { cout << x << "\n";}

};

 

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

protected:

int y;

public:

void showYzm() { cout << y << "\n";}

};

 

// Успадкування двох базових класів.

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

class derived : public baseA, public baseB { // Оголошення базового класу

public:

void setXY(int izm, int jzm) {x = izm; y = jzm;}

};

 

int main()

{

derived D_ob; // Створення об'єкта класу

 

D_ob.setXY(10, 20); // член класу derived

D_ob.showXzm(); // функція з класу baseA

D_ob.showYzm(); // функція з класу baseB

 

getch(); return 0;

}

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

15.5. Використання конструкторів і деструкторів під час реалізації механізму успадкування

Під час реалізації механізму успадкування зазвичай виникають два важливі запитання, пов'язані з використання конструкторів і деструкторів:

● коли викликаються конструктори і деструктори базового і похідного класів?

● як можна передати параметри конструктору базового класу?

Відповіді на ці запитання викладено нижче у цьому підрозділі.

15.5.1. Прядок виконання конструкторів і деструкторів

Базовий і/або похідний клас може містити конструктор і/або деструктор. Важливо розуміти порядок, у якому виконуються ці функції під час створення об'єкта похідного класу і його (об'єкта) руйнування. Для розуміння сказаного розглянемо таку коротку програму.

Код програми 15.9. Демонстрація порядку виконання конструкторів і деструкторів

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

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

 

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

public:

baseClass() { cout << "Створення baseClass-об'єкта.\n";}

~baseClass() { cout << "Руйнування baseClass-об'єкта.\n";}

};

 

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

class derived : public baseClass {

public:

derived() { cout << "Створення derived-об'єкта.\n";}

~derived() { cout << "Руйнування derived-об'єкта.\n";}

};

 

int main()

{

derived D_ob; // Створення об'єкта класу

 

// Ніяких дій, окрім створення і руйнування об'єкта D_ob.

 

getch(); return 0;

}

Як зазначено у коментарях для функції main(), ця програма тільки створює і відразу руйнує об'єкт D_ob, який має тип derived. У процесі виконання програма відображає на екрані такі результати:

Створення baseClass-об'єкта.

Створення derived-об'єкта.

Руйнування derived-об'єкта.

Руйнування baseClass-об'єкта.

Аналізуючи отримані результати, бачимо, що спочатку виконується конструктор класу baseClass, а за ним – конструктор класу derived. Потім (через негайне руйнування об'єкта ob у цій програмі) викликається деструктор класу derived, а за ним – деструктор класу baseClass.

Результати описаного вище експерименту можна узагальнити таким чином. Під час створення об'єкта похідного класу спочатку викликається конструктор базового класу, а за ним – конструктор похідного класу. Під час руйнування об'єкта похідного класу спочатку викликається його "рідний" конструктор, а за ним – конструктор базового класу.

Конструктори викликаються у порядку походження класів, а деструктори – у зворотному порядку.

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

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

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

Код програми 15.10. Демонстрація порядку виконання конструкторів і деструкторів при розширеній ієрархії класів

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

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

 

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

public:

baseClass() { cout << "Створення baseClass-об'єкта.\n";}

~baseClass() { cout << "Руйнування baseClass-об'єкта.\n";}

};

 

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

class derivedA : public baseClass {

public:

derivedA() { cout << "Створення derivedA-об'єкта.\n";}

~derivedA() { cout << "Руйнування derivedA-об'єкта.\n";}

};

 

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

class derivedB : public derivedA {

public:

derivedB() { cout << "Створення derivedB-об'єкта.\n";}

~derivedB() { cout << "Руйнування derivedB-об'єкта.\n";}

};

 

int main()

{

derivedB B_ob; // Створення об'єкта класу

 

// Створення і руйнування об'єкта B_ob.

 

getch(); return 0;

}

У процесі виконання цієї програми на моніторі будуть відображені такі результати:

Створення baseClass-об'єкта.

Створення derivedA-об'єкта.

Створення derivedB-об'єкта.

Руйнування derivedB-об'єкта.

Руйнування derivedA-об'єкта.

Руйнування baseClass-об'єкта.

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

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

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

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

 

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

class baseA {

public:

baseA() { cout << "Створення baseA-об'єкта.\n";}

~baseA() { cout << "Руйнування baseA-об'єкта.\n";}

};

 

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

class baseB {

public:

baseB() { cout << "Створення baseB-об'єкта.\n";}

~baseB() { cout << "Руйнування baseB-об'єкта.\n";}

};

 

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

class derived : public baseA, public baseB {

public:

derived() { cout << "Створення derived-об'єкта.\n";}

~derived() { cout << "Руйнування derived-об'єкта.\n";}

};

 

int main()

{

derived D_ob; // Створення об'єкта класу

// Створення і руйнування об'єкта D_ob.

 

getch(); return 0;

}

У процесі виконання цієї програми генеруються такі результати:

Створення baseA-об'єкта.

Створення baseB-об'єкта.

Створення derived-об'єкта.

Руйнування derived-об'єкта.

Руйнування baseB-об'єкта.

Руйнування baseA-об'єкта.

Як бачите, конструктори викликаються у порядку походження їх класів, зліва направо, у порядку їх задавання у переліку успадкування для класу derived. Деструктори викликаються у зворотному порядку, справа наліво. Це означає, що якби клас baseB знаходився перед класом baseA у переліку класу derived, тобто відповідно до такої настанови:

class derived : public baseB, public baseA {,

то результати виконання попередньої програми були б такими:

Створення baseB-об'єкта.

Створення baseA-об'єкта.

Створення derived-об'єкта.

Руйнування derived-об'єкта.

Руйнування baseA-об'єкта.

Руйнування baseB-об'єкта.

15.5.2. Передача параметрів конструкторам базового класу

Дотепер жоден з попередніх прикладів не містив конструкторів, для яких потрібно було б передавати аргументи. У випадках, коли конструктор тільки похідного класу вимагає передачі одного або декількох аргументів, достатньо використовувати стандартний синтаксис параметризованого конструктора. Але як передати аргументи конструктору базового класу? У цьому випадку необхідно використовувати розширену форму оголошення конструктора похідного класу, у якому передбачено можливість передачі аргументів одному або декільком конструкторам базового класу.

Загальний формат розширеного оголошення конструктора похідного класу має такий вигляд:

конструктор_похідного_класу (список_аргументів):

baseA(список_аргументів),

baseB(список_аргументів),

. . . . . . . . . . . .

baseN(список_аргументів);

{

тіло конструктора похідного класу

}

Тут елементи baseA, ..., baseNозначають імена базових класів, що успадковуються похідним класом. Звернемо Вашу увагу на те, що оголошення конструктора похідного класу відділяється від переліку базових класів двокрапкою, а імена базових класів розділяються між собою комами (у разі успадкування декількох базових класів). Для розуміння сказаного розглянемо таку коротку програму.

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

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

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

 

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

protected:

int izm;

public:

baseClass(int x) { izm = x;

cout << "Створення baseClass-об'єкта.\n";}

~baseClass() { cout << "Руйнування baseClass-об'єкта.\n";}

};

 

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

class derived : public baseClass {

int jzm;

public:

// Клас derived використовує параметр х, а параметр y

// передається конструктору класу baseClass.

derived(int x, int y) : baseClass(y)

{ jzm = x; cout << "Створення derived-об'єкта.\n";}

~derived() { cout << "Руйнування derived-об'єкта.\n";}

void showBase()

{ cout << "izm= " << izm << "; jzm= " << jzm << "\n";}

};

 

int main()

{

derived D_ob(3, 4);

 

D_ob.showBase(); // Відображає числа 4 3

 

getch(); return 0;

}

У цій програмі конструктор класу derived оголошується з двома параметрами x і y. Проте конструктор derived() використовує тільки параметр х, а параметр y передається конструктору baseClass(). У загальному випадку конструктор похідного класу повинен оголошувати параметри, які приймає його клас, а також ті, які потрібні базовому класу. Як це показано у попередньому прикладі, будь-які параметри, що потрібні базовому класу, передаються йому у переліку аргументів базового класу, що вказується після двокрапки. Розглянемо приклад програми, у якій продемонстровано передача параметрів конструкторам декількох базових класів.

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

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

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

 

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

protected:

int izm;

public:

baseA(int x) { izm = x; cout << "Створення baseA-об'єкта.\n";}

~baseA() { cout << "Руйнування baseA-об'єкта.\n";}

};

 

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

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

protected:

int kzm;

public:

baseB(int x) { kzm = x; cout << "Створення baseB-об'єкта.\n";}

~baseB() { cout << "Руйнування baseB-об'єкта.\n";}

};

 

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

class derived : public baseA, public baseB {

int jzm;

public:

derived(int x, int y, int z): baseA(y), baseB(z)

{ jzm = x; cout << "Створення derived-об'єкта.\n";}

~derived() { cout << "Руйнування derived-об'єкта.\n";}

void showBase() {cout << "izm= " << izm << "; jzm= "

<< jzm << "; kzm= " << kzm << "\n";}

};

 

int main()

{

derived D_ob(3, 4, 5);

 

D_ob.showBase(); // Відображає числа izm= 4; jzm= 3; kzm= 5

 

getch(); return 0;

}

Важливо розуміти, що аргументи для конструктора базового класу передаються через аргументи, які приймаються конструктором похідного класу. Навіть якщо конструктор похідного класу не використовує ніяких аргументів, то він повинен оголосити один або декілька аргументів, якщо базовий клас приймає один або декілька аргументів. У цій ситуації аргументи, що передаються похідному класу, "транзитом" передаються базовому. Наприклад, у наведеному нижче коді програми конструктори baseA() і baseB(), на відміну від конструктора класу derived, приймають аргументи.

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

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

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

 

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

protected:

int izm;

public:

baseA(int x) { izm = x; cout << "Створення baseA-об'єкта.\n";}

~baseA() { cout << "Руйнування baseA-об'єкта.\n";}

};

 

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

protected:

int kzm;

public:

baseB(int x) { kzm = x; cout << "Створення baseB-об'єкта.\n";}

~baseB() { cout << "Руйнування baseB-об'єкта.\n";}

};

 

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

class derived : public baseA, public baseB {

public:

/* Конструктор класу derived не використовує параметрів, але повинен

оголосити їх, щоб передати конструкторам базових класів. */

 

derived(int x, int y) : baseA(x), baseB(y)

{ cout << "Створення derived-об'єкта.\n";}

~derived() { cout << "Руйнування derived-об'єкта.\n";}

void showBase()

{ cout << "izm =" << izm << "; kzm= " << kzm << "\n";}

};

 

int main()

{

derived D_ob(3, 4);

 

D_ob.showBase(); // Відображає числа izm= 3; kzm= 4

 

getch(); return 0;

}

Конструктор похідного класу може використовувати будь-які (або всі) параметри, які ним оголошені для прийому, незалежно від того, чи передаються вони (один або декілька) базовому класу. Іншими словами, той факт, що деякий аргумент передається базовому класу, не заважає його використанню і самим похідним класом. Наприклад, цей фрагмент коду програми є абсолютно допустимим:

class derived : public baseClass {

int jzm;

public:

// Клас derived використовує обидва параметри х і y,

// а також передає їх класу baseClass.

derived(int x, int y) : baseClass(x, y)

{ jzm = x*y; cout << "Створення derived-об'єкта.\n";}

//. . .

}

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

15.6. Повернення успадкованим членам початкової специфікації доступу

Коли базовий клас успадковується закритим способом (як private-клас), то всі його члени (відкриті, захищені та закриті) стають private-членами похідного класу. Але за певних обставин один або декілька успадкованих членів необхідно повернути до їх початкової специфікації доступу. Наприклад, незважаючи на те, що базовий клас успадковується як private-клас, деяким визначеним його public-членам потрібно надати public-статус у похідному класі. Це можна зробити двома способами: