Дочірні класи за замовчуванням наслідують всі методи батьківського класу. На цьому уроці ми розглянемо, як це відбувається, а також те, як можна змінити методи батьківських класів в дочірніх класах.
Виклик методів батьківського класу
При виклику методу через об’єкт дочірнього класу, компілятор спочатку дивиться, чи існує цей метод в дочірньому класі. Якщо ні, то він починає просуватися по “ланцюжку” спадкування вгору і перевіряє, чи був цей метод визначений в будь-якому з батьківських класів. Компілятор використовуватиме перше знайдене визначення. Наприклад:
|
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 |
#include <iostream> class Parent { protected: int m_value; public: Parent(int value) : m_value(value) { } void identify() { std::cout << "I am a Parent!\n"; } }; class Child : public Parent { public: Child(int value) : Parent(value) { } }; int main() { Parent parent(6); parent.identify(); Child child(8); child.identify(); return 0; } |
Результат виконання програми:
I am a Parent!
I am a Parent!
При виклику child.identify(), компілятор дивиться, чи визначений метод identify() в класі Child. Ні, тому компілятор переходить до класу Parent. У класі Parent є визначення методу identify(), тому компілятор використовує саме це визначення.
Перевизначення методів батьківського класу
Однак, якби ми визначили метод identify() в класі Child, то використовувалося б саме це визначення. Це означає, що ми можемо змусити батьківські методи працювати по-іншому з нашими дочірніми класами, просто перевизначаючи їх в дочірніх класах!
Вищенаведений приклад стане кращим, якщо child.identify() виводитиме I am a Child!. Давайте змінимо метод identify() в класі Child так, щоб він повертав правильну відповідь.
Перевизначення батьківського методу в дочірньому класі відбувається, як звичайне визначення методу:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Child : public Parent { public: Child(int value) : Parent(value) { } int getValue() { return m_value; } // Ось наш змінюваний метод батьківського класу void identify() { std::cout << "I am a Child!\n"; } }; |
Ось той же код функції main(), що і у вищенаведеному прикладі, але вже з внесеними змінами в клас Child:
|
1 2 3 4 5 6 7 8 9 10 |
int main() { Parent parent(6); parent.identify(); Child child(8); child.identify(); return 0; } |
Результат:
I am a Parent!
I am a Child!
Зверніть увагу, коли ми перевизначаємо батьківський метод в дочірньому класі, то дочірній метод не наслідує специфікатор доступу батьківського методу з тим же ім’ям. Використовується той специфікатор доступу, який вказаний в дочірньому класі. Таким чином, метод, визначений як private в батьківському класі, може бути перевизначений як public в дочірньому класі, або навпаки!
|
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 |
#include <iostream> class Parent { private: void print() { std::cout << "Parent!"; } }; class Child : public Parent { public: void print() { std::cout << "Child!"; } }; int main() { Child child; child.print(); // виклик child::print(), який є public return 0; } |
Розширення функціоналу батьківських методів
Можуть бути випадки, коли нам не потрібно повністю замінювати метод батьківського класу, але потрібно просто розширити його функціонал. Зверніть увагу, у вищенаведеному прикладі метод Child::identify() повністю перекриває Parent::identify()! Можливо, це не те, що нам потрібно. Ми можемо викликати метод батьківського класу з тим же ім’ям в методі дочірнього класу (для повторного використання коду), а потім додати свій код.
Щоб метод дочірнього класу викликав метод батьківського класу з тим же ім’ям, потрібно просто виконати звичайний виклик функції, але з доданням імені батьківського класу і оператора дозволу області видимості. У наступному прикладі ми виконаємо перевизначення identify() в класі Child, викликаючи спочатку Parent::identify(), а потім додаючи вже свій код:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Child : public Parent { public: Child(int value) : Parent(value) { } int GetValue() { return m_value; } void identify() { Parent::identify(); // спочатку виконується виклик Parent::identify() std::cout << "I am a Child!\n"; // потім вже виводиться цей текст } }; |
Разом з:
|
1 2 3 4 5 6 7 8 9 10 |
int main() { Parent parent(6); parent.identify(); Child child(8); child.identify(); return 0; } |
Дає результат:
I am a Parent!
I am a Parent!
I am a Child!
При виконанні child.identify() виконується виклик Child::identify(). У Child::identify() ми спочатку викликаємо Parent::identify(), який виводить I am a Parent!. Коли Parent::identify() завершує своє виконання, Child::identify() продовжує своє виконання і виводить I am a Child!.
Все просто. Навіщо тоді потрібно використовувати оператор дозволу області видимості (::)? А потім, що, якби ми визначили Child::identify() наступним чином:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Child : public Parent { public: Child(int value) : Parent(value) { } int GetValue() { return m_value; } void identify() { identify(); // немає оператора дозволу області видимості! std::cout << "I am a Child!"; } }; |
То виклик методу identify() без вказування оператора дозволу області видимості призвів би до виклику identify() в поточному класі, тобто Child::identify(). Потім знову виклик Child::identify(), і ура — у нас вийшов нескінченний цикл. Тому використання оператора дозволу області видимості є обов’язковою умовою при зміні методів батьківського класу.
