19

Когда использовать виртуальные деструкторы?

8

У меня есть хорошее понимание большинства теорий объектно-ориентированного программирования (OOP), но один момент, который меня сильно пугает, — это виртуальные деструкторы.

Я полагал, что деструктор всегда вызывается, вне зависимости от ситуации, и для каждого объекта в иерархии классов.

В каких случаях следует делать деструкторы виртуальными и почему это важно?

5 ответ(ов)

3

Виртуальные конструкторы невозможны, но виртуальные деструкторы — вполне реальная вещь. Давайте проведем эксперимент.

Вот пример кода:

#include <iostream>

using namespace std;

class Base
{
public:
    Base(){
        cout << "Вызван конструктор Base\n";
    }
    ~Base(){
        cout << "Вызван деструктор Base\n";
    }
};

class Derived1: public Base
{
public:
    Derived1(){
        cout << "Вызван конструктор Derived\n";
    }
    ~Derived1(){
        cout << "Вызван деструктор Derived\n";
    }
};

int main()
{
    Base *b = new Derived1();
    delete b;
}

Данный код выводит следующее:

Вызван конструктор Base
Вызван конструктор Derived
Вызван деструктор Base

Как видно, при создании объекта производного класса порядок вызовов конструкторов соблюдается, но при удалении указателя b (который является указателем на базовый класс) вызывается только деструктор базового класса. Это не совсем правильно. Чтобы поведение было корректным, нам нужно сделать деструктор базового класса виртуальным. Теперь посмотрим, что произойдет, если мы внесем это изменение:

#include <iostream>

using namespace std;

class Base
{ 
public:
    Base(){
        cout << "Вызван конструктор Base\n";
    }
    virtual ~Base(){
        cout << "Вызван деструктор Base\n";
    }
};

class Derived1: public Base
{
public:
    Derived1(){
        cout << "Вызван конструктор Derived\n";
    }
    ~Derived1(){
        cout << "Вызван деструктор Derived\n";
    }
};

int main()
{
    Base *b = new Derived1();
    delete b;
}

Теперь вывод изменился следующим образом:

Вызван конструктор Base
Вызван конструктор Derived
Вызван деструктор Derived
Вызван деструктор Base

Как видно, порядок разрушения объектов теперь соответствует правилам: сначала вызывается деструктор производного класса, затем — деструктор базового класса. Это связано с тем, что виртуальный деструктор позволяет обеспечить правильное поведение удаления объектов через указатели базового класса.

С другой стороны, конструкторы не могут быть виртуальными. Это ограничение языка C++, и его нельзя обойти.

0

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

0

Вызов деструктора через указатель на базовый класс

struct Base {
  virtual void f() {}
  virtual ~Base() {}
};

struct Derived : Base {
  void f() override {}
  ~Derived() override {}
};

Base* base = new Derived;
base->f(); // вызывает Derived::f
base->~Base(); // вызывает Derived::~Derived

Вызов виртуального деструктора не отличается от вызова любой другой виртуальной функции.

Для вызова base->f() будет выполнен диспетчеризация на Derived::f(), и точно так же для вызова base->~Base() – вызовется переопределяющая функция Derived::~Derived().

Такое же поведение происходит, когда деструктор вызывается косвенно, например, при использовании delete base;. Оператор delete вызовет base->~Base(), который будет диспетчеризирован на Derived::~Derived().

Абстрактный класс с невиртуальным деструктором

Если вы не собираетесь удалять объект через указатель на его базовый класс, то необходимость в виртуальном деструкторе отсутствует. Просто сделайте его protected, чтобы он не мог быть вызван случайно:

// library.hpp

struct Base {
  virtual void f() = 0;

protected:
  ~Base() = default;
};

void CallsF(Base& base);
// CallsF не будет владеть "base" (т.е. не вызовет "delete &base;").
// Он просто вызовет Base::f(), поэтому ему не нужно иметь доступ к Base::~Base.

//-------------------
// application.cpp

struct Derived : Base {
  void f() override { ... }
};

int main() {
  Derived derived;
  CallsF(derived);
  // Здесь также не требуется виртуальный деструктор.
}
0

Ключевое слово virtual для деструктора необходимо, когда вы хотите, чтобы деструкторы производных классов вызывались в правильном порядке при удалении объектов через указатель базового класса.

Например:

Base *myObj = new Derived();
// Некоторый код, использующий объект myObj
myObj->fun();
// Теперь удаляем объект
delete myObj; 

Если деструктор базового класса является виртуальным, то при уничтожении объектов будет соблюден порядок: сначала будет вызван деструктор производного класса, а затем деструктор базового класса. Если деструктор базового класса НЕ является виртуальным, то будет вызван только деструктор базового класса (поскольку указатель — это Base *myObj). В этом случае произойдет утечка памяти для производного объекта.

Таким образом, чтобы избежать утечек памяти и обеспечить корректное уничтожение объектов, всегда следует объявлять деструкторы базового класса как виртуальные, если в нем есть производные классы.

0

В данном вопросе речь идет о важности виртуального деструктора в иерархии классов на C++. Вот краткое объяснение:

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

Рассмотрим следующий пример кода:

#include<iostream>
using namespace std;

class B {
public:
    B() {
        cout << "B()\n";
    }
    
    virtual ~B() { 
        cout << "~B()\n";
    }
};

class D : public B {
public:
    D() {
        cout << "D()\n";
    }
    
    ~D() {
        cout << "~D()\n";
    }
};

int main() {
    B *b = new D();
    delete b;
    return 0;
}

Вывод программы будет следующим:

B()
D()
~D()
~B()

Как видно, сначала вызывается конструктор базового класса B, затем производного класса D, а при разрушении, наоборот: сначала вызывается деструктор производного класса ~D(), а затем деструктор базового класса ~B().

Теперь, если вы не объявите деструктор ~B() как виртуальный, вывод будет следующим:

B()
D()
~B()

В этом случае деструктор ~D() не будет вызван, что приведет к утечке памяти, так как ресурсы, выделенные производным классом, не будут корректно освобождены.

Таким образом, всегда следует использовать виртуальные деструкторы в базовых классах, когда предполагается удаление экземпляров производных классов через указатели базового класса.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь