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

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

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

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

Ответы

Ответ 1

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

class Base 
{
    // some virtual methods
};

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

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

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

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

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

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

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

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

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

Ответ 2

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

Ответ 3

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

#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

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

Ответ 4

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

Как следует отменять удаление в С++?

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

Ответ 5

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

Ответ 6

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

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

Ответ 7

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

Ответ 8

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

 #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

Ответ 9

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

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

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

Ответ 10

Что такое виртуальный деструктор или как использовать виртуальный деструктор

Деструктор класса - это функция с тем же именем класса, предшествующего ~, который перераспределит память, выделенную классом. Почему нам нужен виртуальный деструктор

См. следующий пример с некоторыми виртуальными функциями

В примере также рассказывается, как вы можете конвертировать письмо в верхний или нижний

#include "stdafx.h"
#include<iostream>
using namespace std;
// program to convert the lower to upper orlower
class convertch
{
public:
  //void convertch(){};
  virtual char* convertChar() = 0;
  ~convertch(){};
};

class MakeLower :public convertch
{
public:
  MakeLower(char *passLetter)
  {
    tolower = true;
    Letter = new char[30];
    strcpy(Letter, passLetter);
  }

  virtual ~MakeLower()
  {
    cout<< "called ~MakeLower()"<<"\n";
    delete[] Letter;
  }

  char* convertChar()
  {
    size_t len = strlen(Letter);
    for(int i= 0;i<len;i++)
      Letter[i] = Letter[i] + 32;
    return Letter;
  }

private:
  char *Letter;
  bool tolower;
};

class MakeUpper : public convertch
{
public:
  MakeUpper(char *passLetter)
  {
    Letter = new char[30];
    toupper = true;
    strcpy(Letter, passLetter);
  }

  char* convertChar()
  {   
    size_t len = strlen(Letter);
    for(int i= 0;i<len;i++)
      Letter[i] = Letter[i] - 32;
    return Letter;
  }

  virtual ~MakeUpper()
  {
    cout<< "called ~MakeUpper()"<<"\n";
    delete Letter;
  }

private:
  char *Letter;
  bool toupper;
};


int _tmain(int argc, _TCHAR* argv[])
{
  convertch *makeupper = new MakeUpper("hai"); 
  cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" ";     
  delete makeupper;
  convertch *makelower = new MakeLower("HAI");;
  cout<<"Eneterd : HAI = " <<makelower->convertChar()<<" "; 
  delete makelower;
  return 0;
}

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

См. следующий пример с виртуальным деструктором

#include "stdafx.h"
#include<iostream>

using namespace std;
// program to convert the lower to upper orlower
class convertch
{
public:
//void convertch(){};
virtual char* convertChar() = 0;
virtual ~convertch(){}; // defined the virtual destructor

};
class MakeLower :public convertch
{
public:
MakeLower(char *passLetter)
{
tolower = true;
Letter = new char[30];
strcpy(Letter, passLetter);
}
virtual ~MakeLower()
{
cout<< "called ~MakeLower()"<<"\n";
      delete[] Letter;
}
char* convertChar()
{
size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
{
Letter[i] = Letter[i] + 32;

}

return Letter;
}

private:
char *Letter;
bool tolower;

};
class MakeUpper : public convertch
{
public:
MakeUpper(char *passLetter)
{
Letter = new char[30];
toupper = true;
strcpy(Letter, passLetter);
}
char* convertChar()
{

size_t len = strlen(Letter);
for(int i= 0;i<len;i++)
{
Letter[i] = Letter[i] - 32;
}
return Letter;
}
virtual ~MakeUpper()
{
      cout<< "called ~MakeUpper()"<<"\n";
delete Letter;
}
private:
char *Letter;
bool toupper;
};


int _tmain(int argc, _TCHAR* argv[])
{

convertch *makeupper = new MakeUpper("hai");

cout<< "Eneterd : hai = " <<makeupper->convertChar()<<" \n";

delete makeupper;
convertch *makelower = new MakeLower("HAI");;
cout<<"Eneterd : HAI = " <<makelower->convertChar()<<"\n ";


delete makelower;
return 0;
}

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

Или посетите ссылку

https://web.archive.org/web/20130822173509/http://www.programminggallery.com/article_details.php?article_id=138

Ответ 11

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

Ответ 12

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

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.

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

Ответ 13

Я подумал, что было бы полезно обсудить поведение "undefined" или, по крайней мере, поведение "сбой" undefined, которое может возникать при удалении через базовый класс (/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 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 указатель на экземпляр объекта c или d будут исправлены для учета vtable. Если вы передадите этот указатель a или b для его удаления, произойдет сбой из-за того, что адрес недействителен для бесплатной процедуры кучи.

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

Ответ 14

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

Ответ 15

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

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