Переопределение методов
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 51 из 104.
Переопределение методов происходит, когда производный класс предоставляет свою собственную реализацию метода, который уже существует в базовом классе. Это позволяет производным классам настраивать унаследованное поведение, сохраняя при этом ту же сигнатуру метода.
Чтобы переопределить метод, просто определите метод в производном классе с тем же самым именем, типом возвращаемого значения и параметрами:
class Animal {
public:
void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
d.speak(); // Вывод: Woof!Версия производного класса скрывает версию базового класса. Тем не менее, вы все еще можете получить доступ к методу базового класса, используя оператор разрешения области видимости:
class Dog : public Animal {
public:
void speak() {
Animal::speak(); // Вызов версии базового класса
std::cout << "Woof!" << std::endl;
}
};У этой базовой формы переопределения есть важное ограничение. Когда вы обращаетесь к производному объекту через указатель или ссылку на базовый класс, вместо этого вызывается версия базового класса:
Dog d;
Animal* ptr = &d;
ptr->speak(); // Вывод: Some sound (не Woof!)Это происходит потому, что компилятор определяет, какой метод вызвать, основываясь на типе указателя, а не на фактическом типе объекта. Чтобы добиться истинного полиморфизма времени выполнения, при котором вызывается правильный метод в зависимости от фактического объекта, вам понадобятся виртуальные функции, которые мы рассмотрим в следующем уроке.
Задание
ЛегкоДавайте создадим систему уведомлений, которая демонстрирует переопределение методов в действии. Вы создадите базовый класс Notifier и два специализированных уведомления, которые настраивают способ доставки сообщений.
Вы организуете свой код в трех файлах:
Notifier.h: Определите базовый классNotifierс:- Защищенным членом
std::string recipient - Конструктором, который принимает имя получателя и сохраняет его
- Публичным методом
send(const std::string& message), который выводит:Sending to <recipient>: <message>
- Защищенным членом
EmailNotifier.h: Определите классEmailNotifier, который публично наследуется отNotifier:- Конструктор, который принимает получателя и передает его в базовый класс
- Переопределите метод
send(), чтобы сначала вызвать версию базового класса с помощьюNotifier::send(), а затем вывести:[Email sent successfully]
main.cpp: Считайте два входных значения (каждое на отдельной строке):- Имя получателя (строка)
- Содержание сообщения (строка)
Определите класс
SMSNotifierнепосредственно в main.cpp, который публично наследуется отNotifier:- Конструктор, который принимает получателя и передает его в базовый класс
- Переопределите метод
send(), чтобы вывести:SMS to <recipient>: <message>(полностью заменяя базовое поведение без его вызова)
Создайте по одному объекту каждого типа уведомления (базовый
Notifier,EmailNotifierиSMSNotifier), используя одного и того же получателя. Вызовитеsend()для каждого с введенным сообщением, выводя разделительную линию между ними:--- Base Notifier --- <output> --- Email Notifier --- <output> --- SMS Notifier --- <output>
Например, при входных данных Alice и Hello there!:
--- Base Notifier ---
Sending to Alice: Hello there!
--- Email Notifier ---
Sending to Alice: Hello there!
[Email sent successfully]
--- SMS Notifier ---
SMS to Alice: Hello there!Обратите внимание, как EmailNotifier расширяет базовое поведение, сначала вызывая родительский метод, в то время как SMSNotifier полностью заменяет его собственной реализацией. Не забудьте про guard-инструкции заголовков и включение соответствующих заголовочных файлов в каждый файл.
Шпаргалка
Переопределение методов позволяет производному классу предоставить собственную реализацию метода базового класса, сохраняя ту же сигнатуру метода (имя, тип возвращаемого значения и параметры).
Базовый синтаксис переопределения:
class Animal {
public:
void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
d.speak(); // Output: Woof!Метод производного класса скрывает версию базового класса. Чтобы вызвать метод базового класса из производного класса, используйте оператор разрешения области видимости:
class Dog : public Animal {
public:
void speak() {
Animal::speak(); // Вызов версии базового класса
std::cout << "Woof!" << std::endl;
}
};Важное ограничение: При доступе к производному объекту через указатель или ссылку базового класса вызывается версия базового класса:
Dog d;
Animal* ptr = &d;
ptr->speak(); // Output: Some sound (not Woof!)Это происходит потому, что компилятор определяет, какой метод вызывать, на основе типа указателя, а не фактического типа объекта. Для истинного полиморфизма во время выполнения используйте виртуальные функции.
Попробуйте сами
#include <iostream>
#include <string>
#include "Notifier.h"
#include "EmailNotifier.h"
using namespace std;
// TODO: Определите здесь класс SMSNotifier, который публично наследуется от Notifier
// - Конструктор, который передает recipient в базовый класс
// - Переопределите send(), чтобы вывести: SMS to <recipient>: <message>
// (полностью замените базовое поведение, не вызывайте родительский send())
int main() {
string recipient;
string message;
getline(cin, recipient);
getline(cin, message);
// TODO: Создайте объект Notifier с recipient
// Выведите "--- Base Notifier ---"
// Вызовите send() с message
// TODO: Создайте объект EmailNotifier с recipient
// Выведите "--- Email Notifier ---"
// Вызовите send() с message
// TODO: Создайте объект SMSNotifier с recipient
// Выведите "--- SMS Notifier ---"
// Вызовите send() с message
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер