На попередніх уроках ми говорили про те, як працює спадкування в мові C++. У всіх наших прикладах ми використовували відкрите спадкування.
На цьому уроці ми розглянемо детально цей тип спадкування, а також два інших типи: private і protected. Також поговоримо про те, як ці типи наслідувань взаємодіють зі специфікаторами доступу для дозволу або обмеження доступу до членів.
Специфікатор доступу protected
Ми вже розглядали специфікатори доступу private і public, які визначають, хто може мати доступ до членів класу. В якості нагадування: доступ до public-членів відкритий для всіх, до private-членів доступ мають тільки члени того ж класу, в якому знаходиться private-член. Це означає, що дочірні класи не можуть напряму звертатися до private-членів батьківського класу!
1 2 3 4 5 6 7 |
class Parent { private: int m_private; // доступ до цього члену є тільки у інших членів класу Parent і у дружніх класів/функцій (але не у дочірніх класів) public: int m_public; // доступ до цього члену відкритий для всіх об'єктів }; |
Все просто.
Примітка: public = «відкритий», private = «закритий», protected = «захищений».
У мові C++ є третій специфікатор доступу, про який ми ще не говорили, тому що він корисний тільки в контексті спадкування. Специфікатор доступу protected відкриває доступ до членів класу дружнім і дочірнім класам. Доступ до protected-члену поза тілом класу закритий.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class Parent { public: int m_public; // доступ до цього члену відкритий для всіх об'єктів private: int m_private; // доступ до цього члену відкритий тільки для інших членів класу Parent і для дружніх класів/функцій (але не для дочірніх класів) protected: int m_protected; // доступ до цього члену відкритий для інших членів класу Parent, дружніх класів/функцій, дочірніх класів }; class Child: public Parent { public: Child() { m_public = 1; // дозволено: доступ до відкритих членів батьківського класу з дочірнього класу m_private = 2; // заборонено: доступ до закритих членів батьківського класу з дочірнього класу m_protected = 3; // дозволено: доступ до захищених членів батьківського класу з дочірнього класу } }; int main() { Parent parent; parent.m_public = 1; // дозволено: доступ до відкритих членів класу ззовні parent.m_private = 2; // заборонено: доступ до закритих членів класу ззовні parent.m_protected = 3; // заборонено: доступ до захищених членів класу ззовні } |
У прикладі, наведеному вище, ви можете бачити, що член m_protected
класу Parent напряму доступний дочірньому класу Child, але доступ до нього для членів ззовні — закритий.
Коли слід використовувати специфікатор доступу protected?
До protected-членів батьківського класу доступ відкритий для членів дочірнього класу, а це означає, що якщо ви пізніше зміните що-небудь в protected-члені (тип даних, значення), то вам доведеться внести зміни як до батьківського, так і в усі дочірні класи. Тому використання специфікатора доступу protected найбільш корисне, коли ви наслідуєте тільки свої класи і кількість дочірніх класів невелика. Таким чином, якщо ви внесете зміни в реалізацію батьківського класу, і вам знадобиться оновити всі дочірні класи, ви зможете зробити ці оновлення самі і це не займе багато часу (тому що дочірніх класів буде небагато).
Створення private-членів надає кращу інкапсуляцію і ізолює батьківські класи від змін, викликаних дочірніми класами. Але ціна цьому додаткове створення відкритого або захищеного інтерфейсу (способу взаємодії інших об’єктів з класами і їх членами, тобто геттери і сеттери). Це додаткова робота, яка не варта того, якщо ви самі працюєте зі своїми ж класами (чужі класи не звертаються до вашого класу) і кількість дочірніх класів невелика.
Типи спадкувань. Доступ до членів
Існує три типи спадкування класів:
public;
private;
protected.
Для визначення типу спадкування потрібно просто вказати потрібне ключове слово біля успадкованого класу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Відкрите спадкування class Pub: public Parent { }; // Закрите спадкування class Pri: private Parent { }; // Захищене спадкування class Pro: protected Parent { }; class Def: Parent // за замовчуванням мова C++ встановлює закрите спадкування { }; |
Якщо ви самі не визначили тип спадкування, то в мові C++ за замовчуванням буде обрано тип спадкування private (аналогічно і для членів класу, які за замовчуванням є private, якщо не вказано інше).
Це дає нам 9 комбінацій: 3 специфікатори доступу (public, private і protected) і 3 типи спадкування (public, private і protected).
Так в чому ж різниця між ними? Якщо коротко, то при спадкуванні специфікатор доступу члена батьківського класу може бути змінений в дочірньому класі (в залежності від типу спадкування). Іншими словами, члени, які були public або protected в батьківському класі, можуть стати private в дочірньому класі.
Це може здатися трохи заплутаним, але все не так уже й погано. Ми зараз з усім цим розберемося, але перед цим згадаємо наступні правила:
Клас завжди має доступ до своїх (не успадкованих) членів.
Доступ до члену класу базується на його специфікаторі доступу.
Дочірній клас має доступ до успадкованих членів батьківського класу на основі специфікатора доступу цих членів в батьківському класі.
Спадкування типу public
Відкрите спадкування є одним з найбільш використовуваних типів спадкування. Дуже рідко ви побачите або будете використовувати інші типи. На щастя, відкрите спадкування є найлегшим і найпростішим з усіх типів. Коли ви відкрито наслідуєте батьківський клас, то успадковані public-члени залишаються public, успадковані protected-члени залишаються protected, а успадковані private-члени залишаються недоступними для дочірнього класу. Нічого не змінюється.
Специфікатор доступу в батьківському класі | Специфікатор доступу при спадкуванні типу public в дочірньому класі |
public | public |
private | Недоступний |
protected | protected |
Наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
class Parent { public: int m_public; private: int m_private; protected: int m_protected; }; class Pub: public Parent // відкрите спадкування { // Відкрите спадкування означає, що: // - public-члени залишаються public в дочірньому класі; // - protected-члени залишаються protected в дочірньому класі; // - private-члени залишаються недоступними в дочірньому класі. public: Pub() { m_public = 1; // дозволено: доступ до m_public відкритий m_private = 2; // заборонено: доступ до m_private в дочірньому класі з батьківського класу закритий m_protected = 3; // дозволено: доступ до m_protected в дочірньому класі з батьківського класу відкритий } }; int main() { Parent parent; parent.m_public = 1; // дозволено: m_public доступний ззовні через батьківський клас parent.m_private = 2; // заборонено: m_private недоступний ззовні через батьківський клас parent.m_protected = 3; // заборонено: m_protected недоступний ззовні через батьківський клас Pub pub; pub.m_public = 1; // дозволено: m_public доступний ззовні через дочірній клас pub.m_private = 2; // заборонено: m_private недоступний ззовні через дочірній клас pub.m_protected = 3; // заборонено: m_protected недоступний ззовні через дочірній клас } |
Правило: Використовуйте відкрите спадкування, якщо у вас немає вагомих причин робити інакше.
Спадкування типу private
При закритому спадкуванні всі члени батьківського класу наслідуються як закриті. Це означає, що private-члени залишаються недоступними, а protected- і public-члени стають private в дочірньому класі.
Зверніть увагу, це не впливає на те, як дочірній клас отримує доступ до членів батьківського класу! Це впливає тільки на те, як іншими об’єктами здійснюється доступ до цих членів через дочірній клас:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
class Parent { public: int m_public; private: int m_private; protected: int m_protected; }; class Priv: private Parent // закрите спадкування { // Закрите спадкування означає, що: // - public-члени стають private (m_public тепер private) в дочірньому класі; // - protected-члени стають private (m_protected тепер private) в дочірньому класі; // - private-члени залишаються недоступними (m_private недоступний) в дочірньому класі. public: Priv() { m_public = 1; // дозволено: m_public тепер private в Priv m_private = 2; // заборонено: дочірні класи не мають доступу до закритих членів батьківського класу m_protected = 3; // дозволено: m_protected тепер private в Priv } }; int main() { Parent parent; parent.m_public = 1; // дозволено: m_public доступний ззовні через батьківський клас parent.m_private = 2; // заборонено: m_private недоступний ззовні через батьківський клас parent.m_protected = 3; // заборонено: m_protected недоступний ззовні через батьківський клас Priv priv; priv.m_public = 1; // заборонено: m_public недоступний ззовні через дочірній клас priv.m_private = 2; // заборонено: m_private недоступний ззовні через дочірній клас priv.m_protected = 3; // заборонено: m_protected недоступний ззовні через дочірній клас } |
Резюмуємо:
Специфікатор доступу в батьківському класі | Специфікатор доступу при спадкуванні типу private в дочірньому класі |
public | private |
private | Недоступний |
protected | private |
Закрите спадкування може бути корисним, коли дочірній клас не має очевидного зв’язку з батьківським класом, але використовує його в своїй реалізації. В такому випадку ми не хочемо, щоб відкритий інтерфейс батьківського класу був доступний через об’єкти дочірнього класу (як це було, коли ми використовували відкритий тип спадкування).
На практиці спадкування типу private використовується рідко.
Спадкування типу protected
Цей тип спадкування майже ніколи не використовується, за винятком особливих випадків. З захищеним спадкуванням, public- і protected-члени стають protected, а private-члени залишаються недоступними.
Оскільки цей тип спадкування дуже рідко використовується, то ми пропустимо приклад на практиці і відразу перейдемо до таблиці:
Специфікатор доступу в батьківському класі | Специфікатор доступу при спадкуванні типу protected в дочірньому класі |
public | protected |
private | Недоступний |
protected | protected |
Фінальний приклад
1 2 3 4 5 6 7 8 9 |
class Parent { public: int m_public; private: int m_private; protected: int m_protected; }; |
Клас Parent може звертатися до своїх членів безперешкодно. Доступ до m_public
відкритий для всіх. Дочірні класи можуть звертатися як до m_public
, так і до m_protected
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class D2 : private Parent // закрите спадкування { // Закрите спадкування означає, що: // - public-члени стають private в дочірньому класі; // - protected-члени стають private в дочірньому класі; // - private-члени недоступні для дочірнього класу. public: int m_public2; private: int m_private2; protected: int m_protected2; }; |
Клас D2 може безперешкодно звертатися до своїх членів. D2 має доступу до членів m_public
і m_protected
класу Parent, але не до m_private
. Оскільки D2 наслідує клас Parent закрито, то m_public
і m_protected
тепер стають закритими при доступі через D2. Це означає, що інші об’єкти не зможуть отримати доступ до цих членів через використання об’єкту D2, а також будь-які інші класи, які будуть дочірніми класу D2, не матимуть доступ до цих членів:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class D3 : public D2 { // Відкрите спадкування означає, що: // - успадковані public-члени залишаються public в дочірньому класі; // - успадковані protected-члени залишаються protected в дочірньому класі; // - успадковані private-члени залишаються недоступними в дочірньому класі. public: int m_public3; private: int m_private3; protected: int m_protected3; }; |
Клас D3 може безперешкодно звертатися до своїх членів. D3 має доступ до членів m_public2
і m_protected2
класу D2, але не до m_private2
. Оскільки D3 наслідує D2 відкрито, то m_public2
і m_protected2
зберігають свої специфікатори доступу і залишаються public і protected при доступі через D3. D3 не має доступу до m_private
класу Parent. Він також не має доступу до m_protected
або m_public
класу Parent, обидва з яких стали закритими, коли D2 успадкував їх.
Висновки
Спосіб взаємодії специфікаторів доступу, типів спадкування і дочірніх класів може викликати плутанину. Щоб це усунути, з’ясуємо все ще раз:
По-перше, клас завжди має доступ до своїх власних НЕ успадкованих членів (і дружні йому класи також мають доступ). Специфікатори доступу впливають тільки на те, чи можуть об’єкти поза класом і дочірні класи звертатися до цих членів.
По-друге, коли дочірні класи успадковують члени батьківських класів, то члени батьківського класу можуть змінювати свої специфікатори доступу в дочірньому класі. Це ніяк не впливає на власні (не успадковані) члени дочірніх класів (які визначені в дочірньому класі і мають свої власні специфікатори доступу). Це впливає тільки на те, чи можуть об’єкти ззовні і класи, дочірні нашим дочірнім класам, отримати доступ до успадкованих членів батьківського класу.
Загальна таблиця специфікаторів доступу і типів спадкування:
Специфікатор доступу в батьківському класі | Специфікатор доступу при спадкуванні типу public в дочірньому класі | Специфікатор доступу при спадкуванні типу private в дочірньому класі | Специфікатор доступу при спадкуванні типу protected в дочірньому класі |
public | public | private | protected |
private | Недоступний | Недоступний | Недоступний |
protected | protected | private | protected |
Хоча у вищенаведених прикладах ми розглядали використання змінних-членів, ці правила дійсні для всіх членів класів (і для методів, і для типів, оголошених всередині класу).