Ответ в теме: Зачем нужен виртуальный деструктор в С++

#2721

У каждой функции есть адрес, с которого в памяти располагаются ее команды. При вызове функции этот адрес помещается в регистр программного счетчика процессора, который всегда указывает на текущую выполняемую команду. Это справедливо как для обычных функций, так и для функций-членов классов. Однако вызов виртуальной функции немного отличается, т.к. выбор функции должен происходить в зависимости от динамического типа объекта. Рассмотрим пример:

#include <iostream>
#include <vector>
#include <string>

class Profession {
public:
  virtual void work() {
     std::cout << "do not work" << std::endl;
  }
};

class Programmer: public Profession {
  std::string m_code;
public:
  Programmer() {
    m_code = "programmer writes code";
  }
  virtual void work() {
    std::cout << m_code << std::endl;
  }
};

class Pilot: public Profession {
public:
  virtual void work() {
    std::cout << "pilot flying" << std::endl;
  }
};

int main() {
  std::vector<Profession*> workers; 
  Pilot pilot; 

  workers.push_back(new Pilot());
  workers.push_back(new Programmer());

  for (Profession* worker : workers) {
    worker->work();
  }

  pilot.work();
  
  for (Profession* worker : workers) {
    delete worker;
  }
}

В данном примере создается список профессий, в который помещается один пилот и один программист. У каждого из них вызывается виртуальная функция work(). Вызов функции у нас происходит через указатель на базовый класс, статический тип объекта – Profession, поэтому если бы функция была не виртуальной – сработала бы функция базового класса. Однако из-за того, что функция виртуальная – используется динамический тип объекта (Programmer и Pilot соответственно).

Такой эффект достигается за счет использования таблицы виртуальных функций, которая есть в каждом объекте, содержащем хоть одну виртуальную функцию. В этой таблице хранятся адреса функций, которые помещаются в таблицу конструктором и используются при вызове.

Кроме того, в примере создается объект pilot (на стеке, как и vector). Деструктор для него будет вызван по окончанию работы функции main(). Деструктор дочернего класса освобождает память из своих данных-членов и всегда вызывает деструктор базового класса. Таким образом, в данном случае будет вызван сначала ~Pilot(), а затем ~Profession().

В рассмотренном примере есть проблема, связанная с тем, что компилятор автоматически создаст в классе невиртуальный деструктор. В связи с этим, при разрушении объекта будет вызван деструктор, соответствующий статическому типу объекта – для обоих рабочих вызовется ~Profession(). Однако, в классе Programmer есть поле, в которое помещается строка – его деструктор базового класса удалять не будет. Убедиться в этом можно с помощью valgrind memcheck:

virtual_destructor_valgrind

Чтобы решить проблему достаточно сделать деструктор базового класса виртуальным – в этом случае при разрешении объекта правильная реализация деструктора будет выбираться из таблицы виртуальных функций, т.е. использоваться динамический тип объекта:

  virtual ~Profession() {
  }