Виртуальные функции [в разработке]
Отрывок с собеса на YouTube
Предположим, у нас есть класс Mammal
:
class Mammal {
public:
void speak()
{
cout << "I'm Mammal\n";
}
};
И мы от него наследуем класс Cat
:
class Cat : public Mammal {
public:
void speak()
{
cout << "Meow\n";
}
}
Предположим, что после всех этих операций, например, в функции main
, мы запустим код:
Cat cat;
cat.speak();
В итоге у нас в консоль выведется строка Meow
.
В данном случае мы сокрыли имя базового класса.
Если нам нужно будет запустить именно функцию из родительского класса, мы можем как-то переименовать функцию speak
в классе Cat
, ну или сделать так:
Cat cat;
cat.Mammal::speak();
Но в некоторых случаях нам нужно иметь доступ к полиморфизму.
Хочется обращаться к объекту через указатель на базовый класс, ну или через ссылку на базовый класс.
Например, было бы полезно иметь std::vector
с объектами разных классов, унаследованных от класса Mammal
(массив собак, кошек, и вообще кого угодно).
Если для текущего примера мы напишем следующий код:
unique_ptr<Mammal> cat = make_unique<Cat>();
cat->speak();
На экран будет выводится строка I'm Mammal
.
Что происходит, когда в нашем нерабочем примере вызывается cat->speak()
?
Во время выполнения программы рантайм видит, что просто происходит вызов функции speak()
объекта класса Mammal
. Происходит раннее связывание
. Мы ранним связыванием связываем имя нашей функции с её адресом.
Чтобы наш код работал, нам и нужен механизм виртуальных функций.
Всё меняется, если мы добавляем ключевое слово virtual
к объявлению функции speak
в классе Mammal
. Тут уже будет производиться позднее связывание
, то есть связывание через указатель
.
Для функции speak
в дочернем классе можно дописать override
: c С++11 это хорошая практика. Но вообще, если в родительском классе функция определена как virtual
, то функция с таким же заголовком (хз правильно ли так говорит) в дочернем классе тоже автоматически будет виртуальной.
У каждого класса будет своя виртуальная таблица, в которой для каждой виртуальной функции будет записываться адрес этой функции.
Эту таблицу можно увидеть в дебаггере как отдельное поле void *vfptr
(возможно тип не void
, но сути не меняет).
Таким образом, для нашего примера, у классов Mammal
и Cat
будут созданы виртуальные таблицы, в которых для функции speak
будут свои записи.
Когда мы проинициализируем указатель на Mammal
как полиморфный класс типа Cat
, в виртуальную таблицу класса Mammal
, по аналогии с аргументом конструктора, будут записаны адреса функций для дочернего класса. И при выполнении тестового кода на экран будет выводиться уже Meow
.
Чисто виртуальные функции
В общих чертах
Если мы не хотим, чтобы в родительском классе была реализация виртуальной функции, и хотим, чтобы все дочерние классы принудительно переопределяли её, мы можем использовать чисто виртуальные функции:
virtual void speak() = 0;
Класс с чисто виртуальной функцией будет называться абстрактным. Создавать объекты абстрактных классов запрещается (по понятным причинам).
Зачем нужны
- Определение интерфейса
- Предотвращение создания экземпляров базового класса
- Поддержка полиморфизма
pure virtual call
У нас могут быть проблемы, если мы в конструкторе абстрактного класса попытаемся вызвать чисто виртуа льную функцию: потому что на этапе выполнения кода ещё может просто "не создасться" дочерняя переопределённая функция, и произойдёт ошибка. Если попробовать вызвать чисто виртуальную функцию напрямую в конструкторе, нам даже не дадут скомпилировать программу. Но если это как-то спрятать от компилятора (например, вызвать чисто виртуальную функцию через другую функцию в конструкторе), то программа скомпилируется и произойдёт ошибка вызова чисто виртуальной функции (программа аварийно завершится). Проблема тут, опять же, в том, что конструктор базового (в данном случае абстрактного) класса вызовется раньше, чем конструктор дочернего класса. Отсюда и проблема с тем, что в виртуальной таблице класса функция будет ссылаться на какой-то мусор.
Зачем нужен виртуальный деструктор
easyoffer Нужен чтобы гарантрировать, что в дочернем классе, созданном полиморфно от родительского класса, будет вызван именно дочерний деструктор. Вызов цепочки деструкторов: Виртуальный деструктор гарантирует, что при удалении объекта через указатель на базовый класс сначала вызовется деструктор производного класса, а затем деструкторы всех базовых классов по цепочке наследования Виртуальный деструктор необходим, если:
- Вы работаете с наследованием и объектами базового класса могут быть расширены в производных классах.
- Вы удаляете объекты через указатель на базовый класс.
Просто так помечать деструктор в классе как virtual
не надо.
Что такое виртуальное наследование?
easyoffer Владимир Балун Вообще, если полиморфно создавать от базового класса классы-наследники, то в каждом дочернем классе будет создан "внутри" родительский класс. Но что же происходит, если у нас происходит ромбовидное наследование?
flowchart TD subgraph SA direction LR A --> B A --> C B --> D C --> D end SA
В таком случае у нас по идее создался бы объект класса A внутри объектов классов B и C, и потом внутри класса D по объекту классов B и С. Какие же у такой ситуации проблемы?
- Два раза хранится один и тот же класс A
- Два раза будет вызываться конструктор и деструктор класса A Как избежать таких проблем? С помощью витруального наследования!
class B : virtual A {};
Что же будет тогда в памяти по классу D?
void *vbptr; // указатель класса B на родительский
int b_data;
void *vbptr; // указатель класса C на родительский
int c_data;
int d_data;
int a_data; // в самом конце класс на который ссылаются B и C