Почему я должен объявлять виртуальный деструктор для абстрактного класса В C++?


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

7 146

7 ответов:

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

class Interface
{
   virtual void doSomething() = 0;
};

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

void myFunc(void)
{
   Interface* p = new Derived();
   // The behaviour of the next line is undefined. It probably 
   // calls Interface::~Interface, not Derived::~Derived
   delete p; 
}

ответ на ваш вопрос часто, но не всегда. Если ваш абстрактный класс запрещает клиентам вызывать delete по указателю на него (или если он говорит об этом в своей документации), вы можете не объявлять виртуальный деструктор.

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

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

[см. пункт 4 в этой статье:http://www.gotw.ca/publications/mill18.htm]

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

  1. ваш класс предназначен для использования в качестве базового класса?
    • нет: объявите публичный невиртуальный деструктор, чтобы избежать V-указателя на каждый объект класса *.
    • Да: прочитайте следующий вопрос.
  2. является ли ваш базовый класс абстрактным? (т. е. любые виртуальные чистые методы?)
    • нет: попробуйте сделать ваш базовый класс абстрактным, изменив иерархию классов
    • Да: прочитайте следующий вопрос.
  3. вы хотите, чтобы полиморфное удаление через базовый указатель?
    • нет: объявить защищенный виртуальный деструктор, чтобы предотвратить нежелательное использование.
    • Да: объявить общедоступный виртуальный деструктор (никаких накладных расходов в этом случай.)

Я надеюсь, что это помогает.

* важно отметить, что в C++ нет способа пометить класс как окончательный (т. е. неклассируемый), поэтому в случае, если вы решите объявить свой деструктор невиртуальным и общедоступным, не забудьте явно предупредить своих коллег-программистов о том, что они не являются производными от вашего класса.

ссылки:

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

следовательно, вы открываете потенциал для утечек памяти

class IFoo
{
  public:
    virtual void DoFoo() = 0;
};

class Bar : public IFoo
{
  char* dooby = NULL;
  public:
    virtual void DoFoo() { dooby = new char[10]; }
    void ~Bar() { delete [] dooby; }
};

IFoo* baz = new Bar();
baz->DoFoo();
delete baz; // memory leak - dooby isn't deleted

Это не только хорошая практика. Это правило № 1 для любой иерархии классов.

  1. самый базовый класс иерархии в C++ должен иметь виртуальный деструктор

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

Animal* pAnimal = GetAnimal();
delete pAnimal;

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

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

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

например:

Base *p = new Derived;
// use p as you see fit
delete p;

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

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

    Base *ptr = new Derived();
    delete ptr; // Here the call order of destructors: first Derived then Base.

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