Лекция 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());