Виртуальные функции: повторение
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 58 из 104.
Теперь, когда мы понимаем разницу между полиморфизмом времени компиляции и времени выполнения, давайте более подробно рассмотрим виртуальные функции и ключевое слово override, которое делает их использование более безопасным.
Когда вы помечаете функцию как virtual в базовом классе, производные классы могут предоставить свою собственную реализацию. Спецификатор override явно сообщает компилятору, что вы намерены переопределить виртуальную функцию:
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};Использование override крайне важно, так как это позволяет обнаруживать ошибки на этапе компиляции. Если вы случайно допустите опечатку в имени функции или используете неверные параметры, компилятор предупредит вас вместо того, чтобы просто создать новую функцию:
class Cat : public Animal {
public:
void speek() override { // Ошибка компиляции: нет функции для переопределения
std::cout << "Meow!" << std::endl;
}
};Спецификатор final предотвращает дальнейшее переопределение. Используйте его, когда производный класс должен быть последним, кто переопределяет конкретную функцию:
class Bulldog : public Dog {
public:
void speak() override final {
std::cout << "Gruff woof!" << std::endl;
}
};
class TinyBulldog : public Bulldog {
void speak() override {} // Ошибка: нельзя переопределить функцию с модификатором final
};Всегда используйте override при переопределении виртуальных функций. Это документирует ваше намерение и позволяет компилятору проверить, что вы действительно переопределяете существующую виртуальную функцию.
Задание
ЛегкоДавайте создадим систему уведомлений, которая демонстрирует возможности ключевых слов override и final при работе с виртуальными функциями. Вы создадите иерархию обработчиков уведомлений, где некоторые методы можно настраивать дальше, а другие заблокированы для предотвращения изменений.
Вы организуете свой код в трех файлах:
Notifier.h: Определите базовый классNotifier, который представляет любого отправителя уведомлений:- Защищенный член
std::string recipient - Конструктор, который принимает и сохраняет имя получателя
- Виртуальный метод
send(const std::string& message), который выводит:Notifying <recipient>: <message> - Виртуальный метод
getType(), который возвращает строку"Generic" - Виртуальный деструктор
- Защищенный член
EmailNotifier.h: Определите классEmailNotifier, который наследуется отNotifier:- Приватный член
std::string domain - Конструктор, который принимает получателя и домен, передавая получателя в базовый класс
- Переопределите
send(), чтобы вывести:Emailing <recipient>@<domain>: <message> - Переопределите
getType()и пометьте его какfinal— он должен возвращать"Email"
Затем определите класс
UrgentEmailNotifier, который наследуется отEmailNotifier:- Конструктор, который принимает получателя и домен, передавая оба в
EmailNotifier - Переопределите
send(), чтобы вывести:[URGENT] Emailing <recipient>@<domain>: <message> - Примечание: Вы не можете переопределить
getType()здесь, потому что он был помечен какfinalвEmailNotifier
- Приватный член
main.cpp: Считайте три входных значения (каждое на отдельной строке):- Имя получателя
- Домен электронной почты
- Текст сообщения
Динамически создайте три объекта уведомлений: базовый
Notifier,EmailNotifierиUrgentEmailNotifier— все с использованием одного и того же получателя (и домена, где это применимо). Сохраните их в массиве указателейNotifier*.Пройдите циклом по массиву и для каждого уведомления выведите его тип с помощью
getType(), затем вызовитеsend()с вашим сообщением. Форматируйте каждую запись следующим образом:Type: <type> <send output>Выводите пустую строку между уведомлениями. По завершении очистите динамически выделенные объекты.
Например, при входных данных Alice, company.com и Meeting at 3pm:
Type: Generic
Notifying Alice: Meeting at 3pm
Type: Email
Emailing Alice@company.com: Meeting at 3pm
Type: Email
[URGENT] Emailing Alice@company.com: Meeting at 3pmОбратите внимание, как UrgentEmailNotifier может переопределять send() для настройки формата сообщения, но наследует тип "Email" от EmailNotifier, так как getType() был помечен как final. Используйте ключевое слово override для всех переопределяемых методов, чтобы обнаружить любые несоответствия сигнатур во время компиляции.
Шпаргалка
Спецификатор override явно сообщает компилятору, что вы намерены переопределить виртуальную функцию из базового класса:
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};Использование override позволяет выявлять ошибки на этапе компиляции. Если вы допустите опечатку в имени функции или используете неверные параметры, компилятор предупредит вас:
class Cat : public Animal {
public:
void speek() override { // Ошибка компиляции: нет функции для переопределения
std::cout << "Meow!" << std::endl;
}
};Спецификатор final предотвращает дальнейшее переопределение в производных классах:
class Bulldog : public Dog {
public:
void speak() override final {
std::cout << "Gruff woof!" << std::endl;
}
};
class TinyBulldog : public Bulldog {
void speak() override {} // Ошибка: невозможно переопределить функцию с пометкой final
};Всегда используйте override при переопределении виртуальных функций, чтобы задокументировать свои намерения и включить проверку компилятором.
Попробуйте сами
#include <iostream>
#include <string>
#include "Notifier.h"
#include "EmailNotifier.h"
using namespace std;
int main() {
// Чтение входных данных
string recipient;
string domain;
string message;
getline(cin, recipient);
getline(cin, domain);
getline(cin, message);
// TODO: Создайте массив указателей `Notifier*` из 3 элементов
// TODO: Динамически создайте:
// - Базовый `Notifier` с `recipient`
// - `EmailNotifier` с `recipient` и `domain`
// - `UrgentEmailNotifier` с `recipient` и `domain`
// TODO: Пройдите циклом по массиву и для каждого уведомления:
// - Выведите "Type: ", а затем результат `getType()`
// - Вызовите `send()` с `message`
// - Выведите пустую строку между уведомлениями (но не после последнего)
// TODO: Очистите динамически выделенные объекты
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Итоги — Обобщенный контейнер