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


Answers

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

Question

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

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

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




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

 #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



Я подумал, что было бы полезно обсудить «неопределенное» поведение или, по крайней мере, неопределенное поведение «сбой», которое может возникнуть при удалении через базовый класс (/ struct) без виртуального деструктора или, точнее, vtable. В приведенном ниже коде перечислены несколько простых структур (то же самое верно для классов).

#include <iostream>
using namespace std;

struct a
{
    ~a() {}

    unsigned long long i;
};

struct b : a
{
    ~b() {}

    unsigned long long j;
};

struct c : b
{
    ~c() {}

    virtual void m3() {}

    unsigned long long k;
};

struct d : c
{
    ~d() {}

    virtual void m4() {}

    unsigned long long l;
};

int main()
{
    cout << "sizeof(a): " << sizeof(a) << endl;
    cout << "sizeof(b): " << sizeof(b) << endl;
    cout << "sizeof(c): " << sizeof(c) << endl;
    cout << "sizeof(d): " << sizeof(d) << endl;

    // No issue.

    a* a1 = new a();
    cout << "a1: " << a1 << endl;
    delete a1;

    // No issue.

    b* b1 = new b();
    cout << "b1: " << b1 << endl;
    cout << "(a*) b1: " << (a*) b1 << endl;
    delete b1;

    // No issue.

    c* c1 = new c();
    cout << "c1: " << c1 << endl;
    cout << "(b*) c1: " << (b*) c1 << endl;
    cout << "(a*) c1: " << (a*) c1 << endl;
    delete c1;

    // No issue.

    d* d1 = new d();
    cout << "d1: " << d1 << endl;
    cout << "(c*) d1: " << (c*) d1 << endl;
    cout << "(b*) d1: " << (b*) d1 << endl;
    cout << "(a*) d1: " << (a*) d1 << endl;
    delete d1;

    // Doesn't crash, but may not produce the results you want.

    c1 = (c*) new d();
    delete c1;

    // Crashes due to passing an invalid address to the method which
    // frees the memory.

    d1 = new d();
    b1 = (b*) d1;
    cout << "d1: " << d1 << endl;
    cout << "b1: " << b1 << endl;
    delete b1;  

/*

    // This is similar to what's happening above in the "crash" case.

    char* buf = new char[32];
    cout << "buf: " << (void*) buf << endl;
    buf += 8;
    cout << "buf after adding 8: " << (void*) buf << endl;
    delete buf;
*/
}

Я не предлагаю, нужны ли вам виртуальные деструкторы или нет, хотя я думаю, что это хорошая практика для их использования. Я просто указываю причину, по которой вы можете столкнуться с сбоем, если ваш базовый класс (/ struct) не имеет vtable и ваш производный класс (/ struct) делает, и вы удаляете объект через базовый класс (/ struct) указатель. В этом случае адрес, который вы передаете в бесплатную процедуру кучи, является недействительным и, следовательно, причиной сбоя.

Если вы запустите указанный выше код, вы увидите, когда проблема возникнет. Когда этот указатель базового класса (/ struct) отличается от этого указателя производного класса (/ struct), вы столкнетесь с этой проблемой. В приведенном выше примере структуры a и b не имеют vtables. У структур c и d есть vtables. Таким образом, a или b указатель на экземпляр ac или d object будет исправлен для учета vtable. Если вы передадите этот указатель a или b для его удаления, произойдет сбой из-за того, что адрес недействителен для бесплатной процедуры кучи.

Если вы планируете удалять производные экземпляры, имеющие vtables из указателей базового класса, вам необходимо убедиться, что базовый класс имеет таблицу vtable. Один из способов сделать это - добавить виртуальный деструктор, который вы, возможно, захотите правильно очистить ресурсы.




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




Вызов деструктора с помощью указателя на базовый класс

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.
}



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

class A
{
public:
    A() {}
    virtual void foo()
    {
        cout << "This is A." << endl;
    }
};

class B : public A
{
public:
    B() {}
    void foo()
    {
        cout << "This is B." << endl;
    }
};

int main(int argc, char* argv[])
{
    A *a = new B();
    a->foo();
    if(a != NULL)
    delete a;
    return 0;
}

Будет распечатан:

This is B.

Без virtual он будет распечатываться:

This is A.

И теперь вы должны понять, когда использовать виртуальные деструкторы.




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

Как должно быть отменено удаление в C ++?

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






Related