Menu
Coddy logo textTech

Виртуальные функции: повторение

Часть раздела Объектно-ориентированное программирование путешествия по 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 проверить, что вы действительно переопределяете существующую виртуальную функцию.

challenge icon

Задание

Легко

Давайте создадим систему уведомлений, демонстрирующую возможности ключевых слов 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: считайте три входных значения (каждое в отдельной строке):
    1. Имя получателя
    2. Домен электронной почты
    3. Текст сообщения

    Динамически создайте три объекта уведомителей: базовый 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;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Онлайн-компилятор C++