Виртуальные функции: повторение
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy. Урок 58 из 104.
Теперь, когда мы понимаем разницу между полиморфизмом времени компиляции и полиморфизмом времени выполнения, давайте глубже рассмотрим виртуальные функции и ключевое слово override, которое делает их использование более безопасным.
Когда вы помечаете function как virtual в base class, производные классы могут предоставить собственную реализацию. Спецификатор override явно сообщает компилятору, что вы намерены переопределить виртуальную function:
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 имеет решающее значение, поскольку позволяет обнаруживать ошибки во время компиляции. Если ты случайно неправильно напишешь имя function или укажешь неверные параметры, компилятор предупредит тебя вместо того, чтобы молча создать новую function:
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 при переопределении виртуальных функций. Это документирует ваше намерение и позволяет Compiler проверить, что вы действительно переопределяете существующую виртуальную функцию.
Задание
ЛегкоДавайте создадим систему уведомлений, демонстрирующую возможности ключевых слов 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(), поскольку вEmailNotifierон был помечен какfinal
- закрытый член
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 для всех переопределённых методов, чтобы обнаруживать несоответствия сигнатур во время компиляции.
Попробуйте сами
#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: Пройти циклом по массиву и для каждого notifier:
// - Вывести "Type: " за которым следует результат getType()
// - Вызвать send() с message
// - Вывести пустую строку между notifier'ами (не после последнего)
// 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Итоги — Обобщенный контейнерПотренируйтесь самостоятельно: Онлайн-компилятор C++