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

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

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

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

20 ответов

Решение

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

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 имеет неопределенное поведение:

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

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

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

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

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

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

#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

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

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

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

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

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

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

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

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

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

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

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

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

 #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

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

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

Если вы используете shared_ptr(только shared_ptr, а не unique_ptr), вам не нужно иметь виртуальный деструктор базового класса:

#include <iostream>
#include <memory>

using namespace std;

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

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

int main()
{
    shared_ptr<Base> b(new Derived());
}

выход:

Base Constructor Called
Derived constructor called
Derived destructor called
Base Destructor called

Я предлагаю следующее: если класс или структура не , вы должны определить для него виртуальный деструктор.

Я знаю, что это выглядит как чрезмерно бдительное излишество, чтобы стать эмпирическим правилом. Но это единственный способ убедиться, что кто-то, производный от вашего класса, не будет иметь UB при удалении с помощью базового указателя.

Рекомендация Скотта Мейерса в книге «Эффективный C++», приведенная ниже, хороша, но ее недостаточно, чтобы быть уверенным.

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

Например, в приведенной ниже программе базовый класс B не имеет виртуальных функций, поэтому, по словам Мейера, вам не нужно писать виртуальный деструктор. Тем не менее, у вас есть UB ниже, если вы этого не сделаете:

      #include <iostream>

struct A
{
    ~A()
    {
        std::cout << "A::~A()" << std::endl;
    }
};

struct B
{
};

struct C : public B
{
    A a;
};

int main(int argc, char *argv[])
{
    B *b = new C;
    delete b; // UB, and won't print "A::~A()"
    return 0;
}

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

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.

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

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

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

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

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

#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

Я подумал, что было бы полезно обсудить "неопределенное" поведение или, по крайней мере, "аварийное" неопределенное поведение, которое может возникнуть при удалении через базовый класс (/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) указатель. В этом случае адрес, который вы передаете свободной подпрограмме кучи, является недействительным и, следовательно, причиной сбоя.

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

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

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

Этот вид обходит вопрос, поэтому позвольте мне уточнить: как многие отметили, вы получите нежелательное поведение, если вызовете delete base_ptrи деструктор не виртуальный. Однако здесь есть несколько предположений, которые необходимо сделать явными:

  • Если ваш класс не является базовым, вы, надеюсь, не будете писать подобный код. В данном случае я не имею в виду ручное управление памятью, которое само по себе плохо, но происходит из этого класса. Класс, не являющийся базовым классом, не должен наследоваться, например, std::string. C ++ позволяет прострелить себе ногу. Однако это ваша вина, а не то, что базовый класс не имеет виртуального деструктора.
  • Если деструктор недоступен (защищен или закрыт), этот код не будет компилироваться, поэтому нежелательное поведение не может возникнуть. Наличие защищенного деструктора полезно, особенно для миксинов, но также (в меньшей степени) для интерфейсов. Вы не хотите нести накладные расходы на виртуальные функции, если вы не используете их на самом деле. Вместо этого защита деструктора предотвращает нежелательное поведение, но не ограничивает вас в противном случае.
  • Если вы действительно напишете класс, который должен быть производным от, у вас все равно будут виртуальные функции. Как пользователь их, вы обычно будете использовать их только через указатель на базовый класс. Когда это использование включает их утилизацию, тогда оно также должно быть полиморфным. Это тот случай, когда вы должны сделать деструктор виртуальным.

Для получения аналогичного другого взгляда на эту тему также прочтите Когда не следует использовать виртуальные деструкторы?

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

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

Иначе случится такое зло:

Предположим, у вас есть массив указателей Fruit с объектами Apple и Orange.

Когда вы удаляете из коллекции объектов Fruit, ~ Apple () и ~ Orange () не могут быть вызваны, если ~ Fruit () не является виртуальным.

Пример сделан правильно:

      #include <iostream>
using namespace std;
struct Fruit { // good
  virtual ~Fruit() { cout << "peel or core should have been tossed" << endl; } 
};
struct Apple:  Fruit { virtual ~Apple()  {cout << "toss core" << endl; } };
struct Orange: Fruit { virtual ~Orange() {cout << "toss peel" << endl; } };

int main() { 
  Fruit *basket[]={ new Apple(), new Orange() };
  for (auto fruit: basket) delete fruit;
};

хороший выход

      toss core
peel or core should have been tossed
toss peel
peel or core should have been tossed

Пример сделан неправильно:

      #include <iostream>
using namespace std;
struct Fruit { // bad 
  ~Fruit() { cout << "peel or core should have been tossed" << endl; } 
};
struct Apple:  Fruit { virtual ~Apple()  {cout << "toss core" << endl; } };
struct Orange: Fruit { virtual ~Orange() {cout << "toss peel" << endl; } };

int main() { 
  Fruit *basket[]={ new Apple(), new Orange() };
  for (auto fruit: basket) delete fruit;
};

плохой вывод

      peel or core should have been tossed
peel or core should have been tossed

(Примечание: там, где я использовал struct для краткости, обычно использую class и указываю public)

Основное определение о virtual определяет, может ли функция-член класса быть переопределена в ее производных классах.

D-tor класса вызывается в основном в конце области видимости, но возникает проблема, например, когда мы определяем экземпляр в куче (динамическое размещение), мы должны удалить его вручную.

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

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

В конце области действия, если деструктор одного из силовых элементов (Actuator) не вызывается, будут фатальные последствия.

#include <iostream>

class Mother{

public:

    Mother(){

          std::cout<<"Mother Ctor"<<std::endl;
    }

    virtual~Mother(){

        std::cout<<"Mother D-tor"<<std::endl;
    }


};

class Child: public Mother{

    public:

    Child(){

        std::cout<<"Child C-tor"<<std::endl;
    }

    ~Child(){

         std::cout<<"Child D-tor"<<std::endl;
    }
};

int main()
{

    Mother *c = new Child();
    delete c;

    return 0;
}

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

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

Другие вопросы по тегам