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

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

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

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

20.01.2009 12:58:21
Naveen 20.01.2009 13:04:11
Каждый деструктор вниз не вызывается ни на что. virtualубедитесь, что он начинается сверху, а не посередине.
Mooing Duck 29.06.2013 00:32:17
@MooingDuck, это несколько вводящий в заблуждение комментарий.
Euri Pinhollow 30.07.2017 14:34:39
@FranklinYu Хорошо, что вы спросили, потому что теперь я не вижу проблем с этим комментарием (кроме попыток дать ответ в комментариях).
Euri Pinhollow 8.11.2017 07:02:38
16 ОТВЕТОВ
РЕШЕНИЕ

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

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!

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

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

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

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

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

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

1552
23.10.2018 18:57:56
Это объяснило бы, почему у меня были большие утечки, используя фабрику, которую я сделал прежде. Все имеет смысл сейчас. Спасибо
Lodle 20.01.2009 13:08:34
Ну, это плохой пример, так как нет членов данных. Что если Baseи Derivedесть все переменные автоматического хранения? т.е. нет никакого «специального» или дополнительного пользовательского кода для выполнения в деструкторе. Можно ли вообще писать какие-либо деструкторы? Или у производного класса все еще будет утечка памяти?
bobobobo 8.07.2012 18:27:35
Из статьи Херба Саттера: «Указание № 4: деструктор базового класса должен быть либо общедоступным, либо виртуальным, либо защищенным и не виртуальным».
Sundae 9.02.2016 08:22:25
Также из статьи - «если вы удаляете полиморфно без виртуального деструктора, вы вызываете страшный призрак« неопределенного поведения », призрака, которого я лично не хотел бы встретить даже в умеренно хорошо освещенном переулке, большое спасибо». лол
Bondolin 29.02.2016 14:30:21

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

40
20.01.2009 13:02:36

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

193
20.01.2009 13:11:51
+ «Если у класса есть какая-либо виртуальная функция, у него должен быть виртуальный деструктор, и что классы, не предназначенные для того, чтобы быть базовыми классами или не предназначенные для полиморфного использования, не должны объявлять виртуальные деструкторы.»: Существуют ли случаи, в которых имеет смысл нарушать это правило? Если нет, то имеет ли смысл, чтобы компилятор проверял это условие и выдавал ошибку, если она не выполняется?
Giorgio 6.05.2012 09:29:26
@ Джорджио Я не знаю никаких исключений из правил. Но я бы не оценил себя как эксперта по C ++, поэтому вы можете разместить это как отдельный вопрос. Предупреждение компилятора (или предупреждение от инструмента статического анализа) имеет смысл для меня.
Bill the Lizard 6.05.2012 13:08:22
Классы могут быть спроектированы так, чтобы их нельзя было удалить с помощью указателя определенного типа, но при этом все еще иметь виртуальные функции - типичным примером является интерфейс обратного вызова. Один не удаляет свою реализацию через указатель интерфейса обратного вызова, поскольку он предназначен только для подписки, но у него есть виртуальные функции.
dascandy 15.01.2016 05:05:24
@dascandy именно - что или все многие другие ситуации , в которых мы используем полиморфное поведение , но не выполняют управление хранением данных с помощью указателей - например , поддерживая автоматические или статическому продолжительность объектов, с указателями , которые используются только в качестве наблюдательных маршрутов. Нет необходимости / цели в реализации виртуального деструктора в таких случаях. Поскольку мы просто цитируем здесь людей, я предпочитаю Саттера сверху: «Рекомендация № 4: деструктор базового класса должен быть либо общедоступным и виртуальным, либо защищенным и не виртуальным». Последнее гарантирует, что любому, кто попытается случайно удалить через базовый указатель, будет показана ошибка их пути
underscore_d 23.04.2016 15:58:38
@Giorgio На самом деле есть хитрость, которую можно использовать, чтобы избежать виртуального вызова деструктора: привязать через const ссылку на производный объект к базе, например const Base& = make_Derived();. В этом случае Derivedбудет вызываться деструктор prvalue, даже если он не виртуальный, поэтому сохраняются накладные расходы, представленные vtables / vpointers. Конечно сфера довольно ограничена. Андрей Александреску упомянул об этом в своей книге « Современный дизайн C ++» .
vsoftco 2.11.2016 20:06:56

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

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

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

45
23.05.2017 12:10:45
Я посмотрел на ваш вопрос и увидел, что вы объявили базовый деструктор виртуальным. Таким образом, «удаление указателя базового класса, когда нет виртуального деструктора, приведет к неопределенному поведению», остается в силе в отношении вашего вопроса? Поскольку в этом вопросе при вызове удаления производный класс (созданный его новым оператором) сначала проверяется на совместимость с версией. Так как он нашел там, он был назван. Итак, не думаете ли вы, что было бы лучше сказать, что «удаление указателя базового класса, когда деструктора нет, приведет к неопределенному поведению»?
ubuntugod 23.02.2016 07:09:11
Это почти то же самое. Конструктор по умолчанию не является виртуальным.
BigSandwich 26.02.2016 23:30:25

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

9
8.11.2012 16:28:11
Это другой взгляд на один и тот же вопрос. Если мы думаем с точки зрения интерфейсов вместо базового класса против производного класса, то это естественный вывод: если это часть интерфейса, то сделайте его виртуальным. Если это не так.
Dragan Ostojic 9.11.2012 18:58:19
+1 за утверждение сходства концепции ОО интерфейса и чисто виртуального класса C ++ . Относительно деструктора, как ожидается, будет реализовано : это часто не нужно. Если класс не управляет ресурсом, таким как необработанная динамически распределенная память (например, не с помощью интеллектуального указателя), дескриптором файла или дескриптором базы данных, использование деструктора по умолчанию, созданного компилятором, подходит для производных классов. И обратите внимание, что если деструктор (или любая функция) объявлен virtualв базовом классе, он автоматически попадает virtualв производный класс, даже если он не объявлен таковым.
DavidRR 11.07.2013 13:22:26
Это упускает важную деталь, что деструктор не обязательно является частью интерфейса. Можно легко программировать классы, которые имеют полиморфные функции, но которые вызывающий не управляет / не может удалить. Тогда у виртуального деструктора нет цели. Конечно, чтобы гарантировать это, не виртуальный - вероятно, по умолчанию - деструктор должен быть закрытым. Если бы мне пришлось угадывать, я бы сказал, что такие классы чаще используются внутри проектов, но это не делает их менее значимыми в качестве примера / нюанса во всем этом.
underscore_d 23.04.2016 16:12:23

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

#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

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

215
30.05.2019 08:37:57
«Виртуальный конструктор невозможен» означает, что вам не нужно писать виртуальный конструктор самостоятельно. Строительство производного объекта должно следовать цепочке строительства от производного до базового. Поэтому вам не нужно писать виртуальное ключевое слово для вашего конструктора. Спасибо
Tunvir Rahman Tusher 19.04.2013 06:50:48
@Murkantilism, «виртуальные конструкторы не могут быть сделаны» действительно верно. Конструктор не может быть помечен как виртуальный.
cmeub 21.04.2013 20:09:39
@cmeub, но есть идиома для достижения того, что вы хотели бы от виртуального конструктора. См. Parashift.com/c++-faq-lite/virtual-ctors.html
cape1232 3.10.2013 12:58:05
@TunvirRahmanTusher не могли бы вы объяснить, почему называется Base Destructor ??
rimalonfire 11.11.2017 08:49:08
@rimiro Это автоматически c ++. Вы можете
Tunvir Rahman Tusher 11.11.2017 12:01:02

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

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

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

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

#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

3
29.01.2016 19:33:49

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

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.

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

2
23.07.2017 09:49:18
Нет, это лишь восстанавливает основы виртуальных функций, полностью игнорируя нюанс того, когда и почему деструктор должен быть единым - что не так интуитивно понятно, поэтому ОП задал вопрос. (Кроме того, почему здесь ненужное динамическое выделение? Просто сделайте B b{}; A& a{b}; a.foo();. Проверка NULL- что должно быть nullptr- до delete- с неправильной неопределенностью - не требуется: delete nullptr;определяется как неиспользование . Во всяком случае, вы должны были проверить это перед вызовом ->foo(), иначе может произойти неопределенное поведение, если что- newто не получится.)
underscore_d 23.04.2016 16:15:30
Это безопасно для вызова deleteна NULLуказатель (то есть, вам не нужен if (a != NULL)охранник).
James Adkison 13.10.2016 19:57:25
@SaileshD Да, я знаю. Это то, что я сказал в своем комментарии
James Adkison 18.04.2018 16:02:19

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

0
9.09.2014 07:02:02

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

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

-1
2.01.2015 16:27:51
Я бы сказал, что это необходимо только «если на него может указывать указатель базового класса» и его можно публично удалить. Но я полагаю, что не повредит привычке добавлять виртуальные dtors на случай, если они понадобятся позже.
underscore_d 23.04.2016 16:21:26

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

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

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

8
7.01.2020 11:40:17

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

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.
}
12
18.05.2015 13:38:00
Нужно ли явно декларировать ~Derived()во всех производных классах, даже если это просто ~Derived() = default? Или это подразумевается языком (делая его безопасным опускать)?
Ponkadoodle 18.08.2016 15:20:27
@Wallacoloo нет, объявляйте это только тогда, когда это необходимо. Например, чтобы положить в protectedраздел, или чтобы убедиться, что это виртуально с помощью override.
Abyx 18.08.2016 17:51:38

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

 #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

7
26.08.2016 05:33:24
Отсутствие базового виртуального деструктора и вызов deleteбазового указателя приводит к неопределенному поведению.
James Adkison 13.10.2016 20:00:23
@JamesAdkison почему это приводит к неопределенному поведению ??
rimalonfire 11.11.2017 11:06:44
@rimiro Это то, что говорит стандарт . У меня нет копии, но ссылка ведет вас к комментарию, где кто-то ссылается на местоположение в стандарте.
James Adkison 11.11.2017 13:52:40
@rimiro "Если удаление, следовательно, может быть выполнено полиморфно через интерфейс базового класса, то оно должно вести себя виртуально и должно быть виртуальным. Действительно, язык требует этого - если вы удаляете полиморфно без виртуального деструктора, вы вызываете ужасный призрак «неопределенное поведение», призрак, которого я лично не хотел бы встретить даже в умеренно освещенном переулке, большое спасибо ». ( gotw.ca/publications/mill18.htm ) - Херб Саттер
James Adkison 11.11.2017 13:54:16

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

4
17.07.2017 07:44:24

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

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

1
23.03.2017 18:22:30

Если вы используете 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
2
6.03.2019 19:27:34
Хотя это возможно, я бы никому не рекомендовал использовать это. Затраты виртуального деструктора незначительны, и это только позволяет запутаться, особенно менее опытному программисту, который этого не знает. Это маленькое virtualключевое слово может спасти вас от многих мучений.
Michal Štein 21.03.2020 12:18:45