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

1305

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

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

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

  • 6
    Смотрите это: Виртуальный деструктор
  • 125
    Каждый деструктор вниз не вызывается ни на что. virtual убедитесь, что он начинается сверху, а не посередине.
Показать ещё 6 комментариев
Теги:
polymorphism
virtual-destructor

15 ответов

1471
Лучший ответ

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

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 для указателя базового класса.

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

  • 153
    Это объясняет, почему у меня были большие утечки на фабрике, которую я сделал раньше. Все имеет смысл сейчас. Спасибо
  • 0
    Это было исключение, о котором я говорил, но вы правы, стоит упомянуть.
Показать ещё 10 комментариев
183

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

#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

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

  • 1
    «Виртуальный конструктор невозможен» означает, что вам не нужно писать виртуальный конструктор самостоятельно. Построение производного объекта должно следовать цепочке строительства от производного до основания. Поэтому вам не нужно писать виртуальное ключевое слово для вашего конструктора. Спасибо
  • 3
    @Murkantilism, «виртуальные конструкторы не могут быть сделаны» действительно верно. Конструктор не может быть помечен как виртуальный.
Показать ещё 3 комментария
169

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

  • 12
    + «Если у класса есть какая-либо виртуальная функция, у него должен быть виртуальный деструктор, и что классы, не предназначенные для того, чтобы быть базовыми классами или не предназначенные для полиморфного использования, не должны объявлять виртуальные деструкторы.»: Существуют ли случаи, в которых имеет смысл нарушать это правило? Если нет, то имеет ли смысл, чтобы компилятор проверял это условие и выдавал ошибку, если она не выполняется?
  • 0
    @ Джорджио Я не знаю никаких исключений из правил. Но я бы не оценил себя как эксперта по C ++, поэтому вы можете разместить это как отдельный вопрос. Предупреждение компилятора (или предупреждение от инструмента статического анализа) имеет смысл для меня.
Показать ещё 5 комментариев
40

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

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

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

  • 0
    Я посмотрел на ваш вопрос и увидел, что вы объявили базовый деструктор виртуальным. Таким образом, «удаление указателя базового класса, когда нет виртуального деструктора, приведет к неопределенному поведению», остается в силе в отношении вашего вопроса? Поскольку в этом вопросе, когда вы вызываете delete, производный класс (созданный его новым оператором) сначала проверяется на совместимость с версией. Так как он нашел там, он был назван. Итак, не думаете ли вы, что было бы лучше сказать, что «удаление указателя базового класса, когда нет деструктора, приведет к неопределенному поведению»?
  • 0
    Это в значительной степени то же самое. Конструктор по умолчанию не является виртуальным.
33

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

11

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

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.
}
  • 0
    Необходимо ли явно объявлять ~Derived() во всех производных классах, даже если это просто ~Derived() = default ? Или это подразумевается языком (делая его безопасным опускать)?
  • 0
    @Wallacoloo нет, объявляйте это только тогда, когда это необходимо. Например, поместить в protected раздел или убедиться, что он виртуальный, с помощью override .
8

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

  • 5
    Как это улучшает уже принятый ответ?
  • 2
    Это другой взгляд на один и тот же вопрос. Если мы думаем с точки зрения интерфейсов, а не базового класса против производного класса, то это естественный вывод: если это часть интерфейса, то сделайте его виртуальным. Если это не так.
Показать ещё 2 комментария
6

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

 #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

  • 0
    Отсутствие базового виртуального деструктора, и вызов delete для базового указателя ведет к неопределенному поведению.
  • 0
    @JamesAdkison почему это приводит к неопределенному поведению ??
Показать ещё 2 комментария
5

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

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

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

2

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

2

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

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

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

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

#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

1

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

1

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

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.

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

  • 0
    Нет, это лишь восстанавливает основы виртуальных функций, полностью игнорируя нюанс того, когда и почему деструктор должен быть единым - что не так интуитивно понятно, поэтому ОП задал вопрос. (Кроме того, почему здесь ненужное динамическое распределение? Просто выполните B b{}; A& a{b}; a.foo(); Проверка на NULL - которая должна быть nullptr - перед delete - с неправильной отменой - не требуется) : delete nullptr; определяется как no-op. Во всяком случае, вы должны были проверить это перед вызовом ->foo() , так как в противном случае может произойти неопределенное поведение, если new как-то не получилось.)
  • 2
    Безопасно вызывать delete для указателя NULL (т. Е. Вам не требуется защита if (a != NULL) ).
Показать ещё 1 комментарий
0

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

-1

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

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

  • 0
    Я бы сказал, что это необходимо только «если на него может указывать указатель базового класса» и его можно публично удалить. Но я полагаю, что не повредит привычке добавлять виртуальные dtors на случай, если они понадобятся позже.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню