Когда использовать виртуальные деструкторы? - PullRequest
1358 голосов
/ 20 января 2009

У меня есть четкое понимание большинства ОО-теорий, но одна вещь, которая меня сильно смущает, это виртуальные деструкторы.

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

Когда вы собираетесь сделать их виртуальными и почему?

Ответы [ 16 ]

1453 голосов
/ 20 января 2009

Виртуальные деструкторы полезны, когда вы потенциально можете удалить экземпляр производного класса через указатель на базовый класс:

class Base 
{
    // some virtual methods
};

class Derived : public Base
{
    ~Derived()
    {
        // Do some important cleanup
    }
};

Здесь вы заметите, что я не объявил деструктором Базы значение virtual. Теперь давайте посмотрим на следующий фрагмент:

Base *b = new Derived();
// use b
delete b; // Here's the problem!

Поскольку деструктор Базы не является virtual, а b является Base*, указывающим на Derived объект, delete b имеет неопределенное поведение :

[In delete b], если статический тип удаляемый объект отличается от его динамического типа, статического тип должен быть базовым классом динамического типа объекта, который будет удален и статический тип должен иметь виртуальный деструктор или поведение не определено .

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

Подводя итог, всегда делайте деструкторы базовых классов virtual, когда они предназначены для полиморфного манипулирования.

Если вы хотите предотвратить удаление экземпляра с помощью указателя базового класса, вы можете сделать деструктор базового класса защищенным и не виртуальным; при этом компилятор не позволит вам вызвать delete для указателя базового класса.

Вы можете узнать больше о виртуальности и виртуальном деструкторе базового класса в этой статье от Херба Саттера .

186 голосов
/ 09 апреля 2013

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

#include <iostream>

using namespace std;

class Base
{
public:
    Base(){
        cout << "Base Constructor Called\n";
    }
    ~Base(){
        cout << "Base Destructor called\n";
    }
};

class Derived1: public Base
{
public:
    Derived1(){
        cout << "Derived constructor called\n";
    }
    ~Derived1(){
        cout << "Derived destructor called\n";
    }
};

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

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

Base Constructor Called
Derived constructor called
Base Destructor called

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

#include <iostream>

using namespace std;

class Base
{ 
public:
    Base(){
        cout << "Base Constructor Called\n";
    }
    virtual ~Base(){
        cout << "Base Destructor called\n";
    }
};

class Derived1: public Base
{
public:
    Derived1(){
        cout << "Derived constructor called\n";
    }
    ~Derived1(){
        cout << "Derived destructor called\n";
    }
};

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

Выход изменился следующим образом:

Base Constructor Called
Derived Constructor called
Derived destructor called
Base destructor called

Таким образом, уничтожение базового указателя (который занимает выделение для производного объекта!) Следует правилу уничтожения, то есть сначала Derived, а затем Base. С другой стороны, нет ничего похожего на виртуальный конструктор.

182 голосов
/ 20 января 2009

Объявление виртуальных деструкторов в полиморфных базовых классах. Это пункт 7 в «Скотте Мейерсе» Effective C ++ . Далее Майерс резюмирует, что если класс имеет любую виртуальную функцию , он должен иметь виртуальный деструктор, и что классы, не предназначенные для того, чтобы быть базовыми классами или не предназначенные для полиморфного использования, должны не объявить виртуальные деструкторы.

41 голосов
/ 21 января 2009

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

Как должно происходить переопределение удаления в C ++?

Я использую C ++ в течение многих лет, и мне все еще удается повеситься.

37 голосов
/ 20 января 2009

Делайте деструктор виртуальным всякий раз, когда ваш класс полиморфен.

11 голосов
/ 18 мая 2015

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

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

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

Base* base = new Derived;
base->f(); // calls Derived::f
base->~Base(); // calls 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 is not going to own "base" (i.e. call "delete &base;").
// It will only call Base::f() so it doesn't need to access Base::~Base.

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

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

int main() {
  Derived derived;
  CallsF(derived);
  // No need for virtual destructor here as well.
}
9 голосов
/ 08 ноября 2012

Мне нравится думать об интерфейсах и реализациях интерфейсов. В C ++ говорят, что интерфейс является чисто виртуальным классом. Деструктор является частью интерфейса и должен быть реализован. Поэтому деструктор должен быть чисто виртуальным. Как насчет конструктора? Конструктор фактически не является частью интерфейса, потому что объект всегда создается в явном виде.

6 голосов
/ 26 августа 2016

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

 #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;
 }

OUTPUT:
B()
D()
~D()
~B()

==============
If you don't give ~B()  as virtual. then output would be 
B()
D()
~B()
where destruction of ~D() is not done which leads to leak

5 голосов
/ 29 января 2015

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

Base *myObj = new Derived();
// Some code which is using myObj object
myObj->fun();
//Now delete the object
delete myObj ; 

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

3 голосов
/ 24 января 2017

Виртуальные деструкторы базового класса являются «лучшей практикой» - вы всегда должны использовать их, чтобы избежать (трудно обнаружить) утечек памяти. Используя их, вы можете быть уверены, что все деструкторы в цепочке наследования ваших классов вызываются (в правильном порядке). Наследование от базового класса с использованием виртуального деструктора делает деструктор унаследованного класса также автоматически виртуальным (поэтому вам не нужно повторно вводить «virtual» в объявлении деструктора унаследованного класса).

...