17.7 – Вызов унаследованных функций и переопределение поведения
По умолчанию производные классы наследуют все поведения, определенные в базовом классе. В этом уроке мы более подробно рассмотрим, как выбираются функции-члены, а также как мы можем использовать это для изменения поведения в производном классе.
Вызов функции базового класса
Когда функция-член вызывается с объектом производного класса, компилятор сначала проверяет, существует ли этот член в производном классе. Если нет, он начинает переходить по цепочке наследования и проверять, определен ли член в каком-либо из родительских классов. И использует первый найденный.
Рассмотрим следующий пример:
class Base
{
protected:
int m_value;
public:
Base(int value)
: m_value(value)
{
}
void identify() { std::cout << "I am a Base\n"; }
};
class Derived: public Base
{
public:
Derived(int value)
: Base(value)
{
}
};
int main()
{
Base base(5);
base.identify();
Derived derived(7);
derived.identify();
return 0;
}
Эта программа печатает
I am a Base
I am a Base
Когда вызывается derived.identify()
, компилятор проверяет, определена ли функция identify()
в классе Derived
. Ее там нет. Затем он начинает искать наследуемые классы (в данном случае это Base
). Base
определил функцию identify()
, поэтому компилятор использует ее. Другими словами, Base::identify()
использовалась, потому что Derived::identify()
не существует.
Это означает, что если поведение, обеспечиваемое базовым классом, достаточно, мы можем просто использовать его.
Переопределение поведения
Однако если бы мы определили Derived::identify()
в классе Derived
, то использовалась бы эта функция.
Это означает, что мы можем заставить функции работать с нашими производными классами по-другому, переопределив их в производных классах!
В нашем примере выше было бы более правильно, если бы derived.identify()
напечатала "I am a Derived". Давайте изменим функцию identify()
в классе Derived
, чтобы она возвращала правильный ответ, когда мы вызываем identify()
с объектом Derived
.
Чтобы изменить в производном классе способ работы функции, определенной в базовом классе, просто переопределите эту функцию в производном классе.
class Derived: public Base
{
public:
Derived(int value)
: Base(value)
{
}
int getValue() { return m_value; }
// Здесь наша измененная функция
void identify() { std::cout << "I am a Derived\n"; }
};
Вот тот же пример, что и выше, с использованием новой функции Derived::identify()
:
int main()
{
Base base(5);
base.identify();
Derived derived(7);
derived.identify();
return 0;
}
I am a Base
I am a Derived
Обратите внимание, что когда вы переопределяете функцию в производном классе, производная функция не наследует спецификатор доступа функции с тем же именем в базовом классе. Она использует любой спецификатор доступа, который определяется в производном классе. Следовательно, функция, которая определена как закрытая в базовом классе, может быть переопределена как открытая в производном классе, и наоборот!
class Base
{
private:
void print()
{
std::cout << "Base";
}
};
class Derived : public Base
{
public:
void print()
{
std::cout << "Derived ";
}
};
int main()
{
Derived derived;
derived.print(); // вызывает derived::print(), которая является открытой
return 0;
}
Добавление дополнений к существующей функциональности
Иногда мы не хотим полностью заменять функцию базового класса, а вместо этого хотим добавить к ней дополнительный функционал. Обратите внимание, что в приведенном выше примере Derived::identify()
полностью скрывает Base::identify()
! Возможно, это не то, что нам нужно. Допускается, чтобы наша производная функция вызывала базовую версию функции с тем же именем (для повторного использования кода), а затем добавляла к ней дополнительный функционал.
Чтобы производная функция вызывала базовую функцию с тем же именем, просто выполните обычный вызов функции, но добавив перед именем функции квалификатор области видимости (имя базового класса и два двоеточия). В следующем примере переопределяется Derived::identify()
, поэтому она сначала вызывает Base::identify()
, а затем выполняет свои собственные дополнительные действия.
class Derived: public Base
{
public:
Derived(int value)
: Base(value)
{
}
int GetValue() { return m_value; }
void identify()
{
Base::identify(); // сначала вызываем Base::identify()
std::cout << "I am a Derived\n"; // затем идентифицируем себя
}
};
Теперь рассмотрим следующий пример:
int main()
{
Base base(5);
base.identify();
Derived derived(7);
derived.identify();
return 0;
}
I am a Base
I am a Base
I am a Derived
Когда выполняется derived.identify()
, вызов преобразуется в Derived::identify()
. Однако первое, что делает Derived::identify()
, – это вызывает Base::identify()
, который выводит "I am a Base". Когда Base::identify()
возвращает управление, Derived::identify()
продолжает выполнение и печатает "I am a Derived".
Это должно быть довольно просто. Почему нам нужно использовать оператор разрешения области видимости (::
)? Если бы мы определили Derived::identify()
следующим образом:
class Derived: public Base
{
public:
Derived(int value)
: Base(value)
{
}
int GetValue() { return m_value; }
void identify()
{
identify(); // обратите внимание:
// без разрешения области видимости!
cout << "I am a Derived";
}
};
Вызов функции identify()
без квалификатора разрешения области видимости по умолчанию будет использовать identify()
в текущем классе, которой будет Derived::identify()
. Это приведет к тому, что Derived::identify()
вызовет себя, что приведет к бесконечному циклу!
Есть одна хитрость, с которой мы можем столкнуться при попытке вызвать дружественные функции в базовых классах, такие как operator<<
. Поскольку дружественные функции базового класса фактически не являются частью базового класса, использование квалификатора разрешения области видимости не сработает. Вместо этого нам нужен способ временно сделать так, чтобы наш класс Derived
выглядел как Base
, чтобы можно было вызвать правильную версию функции.
К счастью, это легко сделать с помощью static_cast
. Например:
#include <iostream>
class Base
{
private:
int m_value{};
public:
Base(int value)
: m_value{ value }
{
}
friend std::ostream& operator<< (std::ostream &out, const Base &b)
{
out << "In Base\n";
out << b.m_value << '\n';
return out;
}
};
class Derived : public Base
{
public:
Derived(int value)
: Base{ value }
{
}
friend std::ostream& operator<< (std::ostream &out, const Derived &d)
{
out << "In Derived\n";
// статическое приведение объекта Derived к Base,
// чтобы мы могли вызвать правильную версию operator<<
out << static_cast<const Base&>(d);
return out;
}
};
int main()
{
Derived derived{ 7 };
std::cout << derived << '\n';
return 0;
}
Поскольку Derived
«является» Base
, мы можем выполнить статическое приведение нашего объекта Derived
в Base
, чтобы вызывалась соответствующая версия operator<<
, которая использует Base
.
Эта программа печатает:
In derived
In base
7