Лекция 10

05.09.2025 Обновлено: 05.09.2025

Лекция 10. ООП. Полиморфизм

Полиморфизм

  • Свойство системы, позволяющее использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта.

Динамический полиморфизм

  • Позднее (dynamic) и раннее (static) связывание.
  • Реализуется через виртуальные функции.

Виртуальные функции

  • Без знания реального типа класса позволяют вызывать метод того класса, чем переменная фактически является.
  • Ключевое слово virtual — говорит о том, что в классах-потомках функция может быть переопределена.
  • Ключевое слово override (необязательно, но рекомендуется) — явно указывает, что метод переопределяет функцию родителя (помогает на уровне компиляции отслеживать ошибки изменения сигнатуры).
  • Ключевое слово final (у виртуальной функции) — переопределять метод в классах-потомках нельзя.
  • Если метод не виртуальный — берётся метод того типа, через который вызываем (статический тип). Дёшево и сердито!
class Base {
public:
    virtual void foo();
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void foo() override;  // переопределение
};

Таблица виртуальных функций (vtable)

  • Таблица заводится для любого класса с виртуальной функцией.
  • Вызов виртуального метода — это вызов метода по адресу из таблицы (немного долговато из-за дополнительной косвенности).
  • Каждый объект класса с виртуальными функциями хранит указатель на vtable (vptr).

Стандарт не определяет механизм реализации виртуальных функций, однако большинство компиляторов реализуют именно таблицу виртуальных функций.

Виртуальный деструктор

  • Нужен в любом базовом классе, от которого есть хотя бы один наследник.
  • Иначе при использовании полиморфизма (например, Base* p = new Derived(); delete p;) будет вызван только деструктор базового класса → утечка ресурсов / UB.

«Никогда не знаешь, будет ли класс наследоваться…» Варианты решения:

  • Можем делать все деструкторы виртуальными (но это проигрыш в памяти из-за vtable).
  • Тогда можем явно указывать final для тех классов, которые точно не будут наследоваться.
  • Либо жёстко следить за всем и вся (открыть третий глаз — опционально 👁️).

Абстрактный класс

  • Класс, экземпляр которого не может быть создан.
  • Обычно используется в качестве базового класса.
  • Содержит хотя бы 1 pure virtual function (чисто виртуальную функцию): virtual void f() = 0; — не определена в базовом классе, но может быть (точнее, должна быть) переопределена в потомке.
class AbstractShape {
public:
    virtual double area() const = 0;  // pure virtual
    virtual ~AbstractShape() = default;
};

NVI Idiom (Non-Virtual Interface)

Идиома невиртуального интерфейса — подход, позволяющий клиентам вызывать закрытые виртуальные функции опосредованно, через открытые невиртуальные функции-члены.

Применение:

  • Перегрузка оператора вывода только для базового класса, где будет вызываться виртуальная функция базового класса.
  • Во всех наследниках переопределяем эту виртуальную функцию.
class Base {
public:
    void print(std::ostream& os) const { doPrint(os); }  // public non-virtual
private:
    virtual void doPrint(std::ostream& os) const;  // private virtual
};

Стоимость виртуальных функций

  • Лишнее обращение к таблице vtable вместо явного адреса.
  • Невозможно сделать inline optimization (при вызове из метода другой функции — не очень большой — код может подставиться напрямую, а виртуальный вызов так оптимизировать сложно).
  • Для коллекций объектов — они всегда оказываются в куче (т.к. полиморфные коллекции хранят указатели).
  • Порядок объектов в памяти также может влиять на скорость (кэш-локальность).

Коллекция объектов

  • Контейнер (массив) указателей на базовый класс — стандартный способ хранить разнотипные объекты иерархии.
std::vector<Base*> shapes;
shapes.push_back(new Circle());
shapes.push_back(new Square());