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

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

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

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

Смотрите это: Виртуальный деструктор

Naveen 20.01.2009 16:04

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

Mooing Duck 29.06.2013 04:32

@MooingDuck, это несколько вводящий в заблуждение комментарий.

Euri Pinhollow 30.07.2017 17:34

@EuriPinhollow Mind разрабатывает?

Franklin Yu 08.11.2017 01:02

@FranklinYu, хорошо, что вы спросили, потому что теперь я не вижу никаких проблем с этим комментарием (кроме попытки дать ответ в комментариях).

Euri Pinhollow 08.11.2017 10:02

@FranklinYu Я, вероятно, думал о том, что должно быть более конкретное утверждение (т.е. когда разрушение может начинаться в середине дерева наследования, а не сверху), но это то, что ответы уже подробно описаны.

Euri Pinhollow 08.11.2017 10:07

Меня также смущает ответ @MooingDuck. Разве это не должно быть вверх вместо вниз, если вы используете понятие подкласса (ниже) и суперкласса (выше)?

Nibor 20.06.2019 12:31

@Nibor: Да, если вы используете это понятие. Около половины людей, с которыми я разговариваю, рассматривают суперклассы как «верхние», а половина - как «нижние», так что оба являются противоречивыми стандартами, что сбивает все с толку. Я думаю, что суперкласс «выше» встречается немного чаще, но меня этому не учили :(

Mooing Duck 20.06.2019 20:09

эта статья может помочь. medium.com/@tunvirrahmantusher/…

Tunvir Rahman Tusher 26.12.2019 14:54
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1 595
10
733 907
17
Перейти к ответу Данный вопрос помечен как решенный

Ответы 17

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

Ответ принят как подходящий

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

class Base 
{
    // some virtual methods
};

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

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

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

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

[In delete b], if the static type of the object to be deleted is different from its dynamic type, the static type shall be a base class of the dynamic type of the object to be deleted and the static type shall have a virtual destructor or the behavior is undefined.

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

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

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

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

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

Lodle 20.01.2009 16:08

Что ж, это плохой пример, поскольку нет членов данных. Что, если Base и Derived имеют переменные автоматического хранения все? т.е. в деструкторе нет «специального» или дополнительного пользовательского кода. Можно ли вообще перестать писать деструкторы? Или у производного класса по-прежнему будет утечка памяти?

bobobobo 08.07.2012 22:27

@ workmad3 не виртуальный для этого не нужен, не так ли? Может быть, бесполезно, но отсутствовать не обязательно.

Steven Kramer 15.03.2014 00:44

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

ransh 15.02.2015 10:20

@ransh: как @bobobobo уже сказал в предыдущем комментарии, даже если классы не имеют переменных-членов, вызов delete на Base*, который указывает на объект Derived, является неопределенным поведением. Это означает, что программа может делать что угодно, включая утечку памяти.

Luc Touraille 16.02.2015 10:48

Из статьи Херба Саттера: «Рекомендация №4: деструктор базового класса должен быть либо общедоступным и виртуальным, либо защищенным и невиртуальным».

Sundae 09.02.2016 11:22

Также из статьи - «если вы удалите полиморфно без виртуального деструктора, вы вызовете ужасный призрак« неопределенного поведения », призрак, которого я лично не хотел бы встречать даже в умеренно хорошо освещенном переулке, большое вам спасибо». ржу не могу

Bondolin 29.02.2016 17:30

@Sundae Пожалуйста, объясните protected and non-virtual part.

ajaysinghnegi 26.07.2019 08:49

@Jos: Из статьи: «Короче говоря, у вас остается одна из двух ситуаций. Либо: а) вы хотите разрешить полиморфное удаление с помощью базового указателя, и в этом случае деструктор должен быть виртуальным и общедоступным; или б) вы этого не сделаете, и в этом случае деструктор должен быть невиртуальным и защищенным, последнее - для предотвращения нежелательного использования ".

Sundae 05.08.2019 16:36

Чтобы прояснить, похоже, что виртуальный деструктор необходим, если тип будет передан в delete, когда он был выделен как производный тип. Производному типу нет нужен виртуальный деструктор, если к нему не применимо то же самое. Таким образом, класс final никогда не нуждается в виртуальном деструкторе, даже если он является производным от класса, у которого он есть.

fuzzyTew 22.03.2020 17:34

@fuzzyTew Если класс D является производным от класса B с виртуальным деструктором, то D также имеет виртуальный деструктор, даже если он не объявляет его явно. В большинстве случаев виртуальный деструктор необходимо объявлять только классу на вершине иерархии, а не остальной части иерархии.

Luc Touraille 24.03.2020 10:40

@Luc Touraille: да, ваш вывод решает мою проблему. Всегда можно создать экземпляр любого неабстрактного класса. Поэтому, чтобы предотвратить это «неопределенное поведение», любой неабстрактный класс всегда должен определяться с помощью виртуального деструктора. Но большинство классов, с которыми я работал, вообще не имеют виртуального деструктора. Так что, возможно, у одного из их базовых классов он уже есть!

LHC 21.01.2021 12:24

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

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

Giorgio 06.05.2012 13:29

@Giorgio Я не знаю никаких исключений из правил. Но я бы не стал оценивать себя как эксперта по C++, поэтому вы можете опубликовать это как отдельный вопрос. Предупреждение компилятора (или предупреждение от инструмента статического анализа) имеет для меня смысл.

Bill the Lizard 06.05.2012 17:08

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

dascandy 15.01.2016 08:05

@dascandy Exactly - это или все многие другие ситуации, когда мы используем полиморфное поведение, но не выполняем управление хранилищем с помощью указателей - например, поддержание объектов с автоматической или статической продолжительностью, с указателями, используемыми только в качестве маршрутов наблюдения. Нет необходимости / цели в реализации виртуального деструктора в любых таких случаях. Поскольку здесь мы просто цитируем людей, я предпочитаю Саттера сверху: «Рекомендация №4: деструктор базового класса должен быть либо общедоступным и виртуальным, либо защищенным и невиртуальным». Последнее гарантирует, что любой, кто случайно пытается удалить с помощью базового указателя, показывает ошибку своего пути.

underscore_d 23.04.2016 18:58

@Giorgio На самом деле есть уловка, которую можно использовать и избежать виртуального вызова деструктора: привязать через константную ссылку производный объект к базе, например const Base& = make_Derived();. В этом случае будет вызываться деструктор prvalue Derived, даже если он не виртуальный, поэтому можно сэкономить накладные расходы, вносимые vtables / vpointers. Конечно, возможности довольно ограничены. Андрей Александреску упомянул об этом в своей книге Современный дизайн C++.

vsoftco 02.11.2016 23:06

@Giorgio есть предупреждение в gcc только для этой ситуации. dascandy, они изначально не смогли принять вашу точку зрения как задокументировано здесь.

davidvandebunte 25.11.2017 02:12

Почему все работает, если нет виртуального метода без полиморфного деструктора? РЕДАКТИРОВАТЬ ответ: нет, но в этом случае вам никогда не понадобится писать Base *b = new Derived1, который является источником всех проблем, а скорее Derived1 *b = new Derived1. Кроме того, без new не может быть проблем, поскольку компилятор всегда знает настоящий тип того, что выходит за рамки.

Ciro Santilli新疆棉花TRUMP BAN BAD 14.02.2018 17:35

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

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

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

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

ubuntugod 23.02.2016 10:09

Это почти то же самое. Конструктор по умолчанию не виртуальный.

BigSandwich 27.02.2016 02:30

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

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

Dragan Ostojic 09.11.2012 22:58

+1 за указание сходства объектно-ориентированной концепции интерфейс и C++ чистый виртуальный класс. Что касается деструктор, как ожидается, будет реализован: в этом часто нет необходимости. Если класс не управляет ресурсом, таким как необработанная динамически выделяемая память (например, не через интеллектуальный указатель), дескриптор файла или дескриптор базы данных, использование деструктора по умолчанию, созданного компилятором, нормально в производных классах. И обратите внимание, что если деструктор (или любая функция) объявлен virtual в базовом классе, он автоматически становится virtual в производном классе, даже если он не объявлен так.

DavidRR 11.07.2013 17:22

При этом упускается важная деталь, заключающаяся в том, что деструктор является частью интерфейса не обязательно. Можно легко запрограммировать классы, которые имеют полиморфные функции, но которые вызывающий не управляет / не может удалить. Тогда у виртуального деструктора нет цели. Конечно, чтобы гарантировать это, невиртуальный деструктор - вероятно, по умолчанию - должен быть закрытым. Если бы мне пришлось угадывать, я бы сказал, что такие классы чаще используются внутри проектов, но это не делает их менее актуальными в качестве примера / нюанса во всем этом.

underscore_d 23.04.2016 19:12

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

#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

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

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

Tunvir Rahman Tusher 19.04.2013 10:50

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

cmeub 22.04.2013 00:09

@cmeub, но есть идиома, чтобы добиться от виртуального конструктора того, что вы хотели бы. См. parashift.com/c++-faq-lite/virtual-ctors.html

cape1232 03.10.2013 16:58

@TunvirRahmanTusher не могли бы вы объяснить, почему называется Base Destructor ??

rimalroshan 11.11.2017 11:49

@rimiro Автоматически на C++. Вы можете перейти по ссылке stackoverflow.com/questions/677620/…

Tunvir Rahman Tusher 11.11.2017 15:01

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

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

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

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

#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

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

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.

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

Нет, это только воспроизводит самые основы виртуальных функций, полностью игнорируя нюансы того, когда и почему деструктор должен быть одним - что не так интуитивно понятно, поэтому OP задал вопрос. (Кроме того, почему здесь ненужное динамическое размещение? Просто сделайте B b{}; A& a{b}; a.foo();. Проверка NULL - который должен быть nullptr - перед deleteing - с неправильным отступом - не требуется: delete nullptr; определен как бездействующий. Во всяком случае, вы должны проверить это перед вызовом ->foo(), поскольку в противном случае может возникнуть неопределенное поведение, если new каким-то образом выйдет из строя.)

underscore_d 23.04.2016 19:15

Вызывать delete по указателю NULL безопасно (т.е. вам не нужна защита if (a != NULL)).

James Adkison 13.10.2016 22:57

@SaileshD Да, я знаю. Это то, что я сказал в мой комментарий

James Adkison 18.04.2018 19:02

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

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

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

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

underscore_d 23.04.2016 19:21

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

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

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

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

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

Нужно ли явно объявлять ~Derived() во всех производных классах, даже если это просто ~Derived() = default? Или это подразумевается языком (что позволяет безопасно опустить)?

Ponkadoodle 18.08.2016 18:20

@Wallacoloo нет, объявляйте это только тогда, когда это необходимо. Например. поместить в раздел protected, или сделать его виртуальным с помощью override.

Abyx 18.08.2016 20:51

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

 #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

Отсутствие базового виртуального деструктора и вызов delete по базовому указателю приводит к неопределенному поведению.

James Adkison 13.10.2016 23:00

@JamesAdkison, почему это приводит к неопределенному поведению ??

rimalroshan 11.11.2017 14:06

@rimiro Это то, что говорит стандарт. У меня нет копии, но ссылка приведет вас к комментарию, где кто-то ссылается на местоположение в стандарте.

James Adkison 11.11.2017 16:52

@rimiro "Если удаление, следовательно, может быть выполнено полиморфно через интерфейс базового класса, тогда оно должно вести себя виртуально и должно быть виртуальным. Действительно, язык требует этого - если вы удалите полиморфно без виртуального деструктора, вы вызовете ужасный призрак «неопределенное поведение» - призрак, которого я лично не хотел бы встречать даже в умеренно хорошо освещенном переулке, большое вам спасибо ». (gotw.ca/publications/mill18.htm) - Сатер с травами

James Adkison 11.11.2017 16:54

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

Я подумал, что было бы полезно обсудить поведение «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'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), вы столкнетесь с этой проблемой. В приведенном выше примере структуры a и b не имеют vtables. в структурах c и d есть таблицы vtables. Таким образом, указатель a или b на экземпляр объекта c или d будет зафиксирован для учета vtable. Если вы передадите этот указатель a или b для удаления, он выйдет из строя из-за того, что адрес недействителен для бесплатной процедуры кучи.

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

Если вы используете 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

Хотя это возможно, я бы отговорил кого-либо от его использования. Накладные расходы виртуального деструктора незначительны, и это просто дает возможность ошибиться, особенно для менее опытного программиста, который этого не знает. Это маленькое ключевое слово virtual может спасти вас от многих мучений.

Michal Štein 21.03.2020 15:18

Основное определение 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;
}

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