Дочерние классы по умолчанию наследуют все методы родительского класса. На этом уроке мы рассмотрим, как это происходит, а также то, как можно изменить методы родительских классов в дочерних классах.
Вызов методов родительского класса
При вызове метода через объект дочернего класса, компилятор сначала смотрит, существует ли этот метод в дочернем классе. Если нет, то он начинает продвигаться по цепочке наследования вверх и проверяет, был ли этот метод определен в любом из родительских классов. Компилятор будет использовать первое найденное определение. Например:
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(), и ура — у нас получился бесконечный цикл. Поэтому использование оператора разрешения области видимости является обязательным условием при изменении методов родительского класса.
Если в дочернем классе не вызывать переопрделенный метод напрямую, а вызвать его через другой метод родительского класса то будет вызван метод родительского класса:
Результат STATE: 0(false).
Получается, что если вызывать метод (setFlag) не напрямую, а через другой метод (doSomething) РОДИТЕЛЬСКОГО класса (не переопределенный в дочернем), то вызовется метод (setFlag) родительского класса, хотя объект владеет переопределенным методом (setFlag).
Теперь мой вопрос: есть ли способ решить данную проблему не прибегая к виртуальным методам?
Кажется, в этом случае наиболее правильным было бы просто переопределить doSomething в дочернем классе.
У меня именно так и получилось в прошлом тестовом задании когда я решил что под каждый потомок класса Fruit заново перегружать оператор вывода << бредово и попытался вызвать << родительского класса для того чтобы вывести name и color и добавить к нему функционал вывода fiber.
Но я так и не понял как вызвать родительский операторв вывода << . С обычной функцией и методом все просто . Вряд ли мне кто-то ответит (зайду завтра-послезавтра еще глянуть) , а пока полезу в интернет искать решение
Ответ для всех, кто также пытался это сделать (я, кстати, тоже)
*** и ура — у нас получился бесконечный цикл
у нас получилась рекурсия 🙂
которая в С++ окажется весьма конечной, причем довольно быстро — вызов функций сожрет память на стеке и вылетит с ошибкой времени выполнения 🙂
вызов рекурсии и есть цикл с определенным кол-вом вызовов(итераций)
Не правда. Цикл и рекурсия — это все таки разные вещи
Можно говорить о том, что хвостовая рекурсия и цикл — взаимозаменяемы. Мало того, часто оптимизатор компилятора/интерпретатора умеет такую рекурсию преобразовывать в цикл
Но в общем случае далеко не всегда рекурсия и цикл тождественны. Довольно часто для замены рекурсии на цикл нужно радикально менять математику