Виртуальные функции [в разработке]

Отрывок с собеса на 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