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

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

Ответ 1

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

Например

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

Ответ 2

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

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

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

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

Ответ 3

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

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

Надеюсь, это поможет.

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

Литература:

Ответ 4

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

Так, например:

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

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

Ответ 5

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

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

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

Ответ 6

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

  • Основной класс иерархии в С++ должен иметь виртуальный деструктор

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

Animal* pAnimal = GetAnimal();
delete pAnimal;

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

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

Ответ 7

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

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

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

Ответ 8

Не всегда:

class Interface
{
   virtual void doSomething(void) = 0;
   // uncomment this if derived classes allocate memory...
   // ...or have members with destructors.
   //CHECK: virtual ~Interface() {}
};

class Derived : public Interface
{
   virtual void doSomething(void) { variable = 100; }
   int variable;
};

class DerivedOther : public Interface
{
   DerivedOther(const Something &ref) : ref(ref) {}
   virtual void doSomething(void) { ref.member = 100; }
   Something &ref;
};

void myFunc(void)
{
   Interface* p = new Derived();
   delete p; // calls Interface::~Interface, not Derived::~Derived (not a problem)
}