Разное

Виртуальный деструктор c: Виртуальный деструктор в C++

Виртуальный деструктор в C++




Статьи »



Виртуальный деструктор в C++

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

Рассмотрим следующий пример.

#include <iostream>

using namespace std;

// Вспомогательный класс
class Object  
{
  public:
    Object() { cout << "Object::ctor()" << endl; }
   ~Object() { cout << "Object::dtor()" << endl; }
};

// Базовый класс
class Base 
{
  public:
    Base() { cout << "Base::ctor()" << endl; }
    virtual ~Base() { cout << "Base::dtor()" << endl; }	
    virtual void print() = 0;
};

// Производный класс
class Derived: public Base 
{
  public:
    Derived() { cout << "Derived::ctor()" << endl; }
   ~Derived() { cout << "Derived::dtor()" << endl; }	
    void print() {}   
    Object  obj;
};

int main ()
{
    Base * p = new Derived;
    delete p;
    return 0;
}

В функции main указателю на базовый класс присваивается адрес динамически создаваемого объекта производного класса Derived. Затем через этот указатель объект разрушается. При этом наличие виртуального деструктора базового класса обеспечивает вызовы деструкторов всех классов в ожидаемом порядке, а именно, в порядке, обратном вызовам конструкторов соответствующих классов.

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

Base::ctor()
Object::ctor()
Derived::ctor()
Derived::dtor()
Object::dtor()
Base::dtor()

Уничтожение объекта производного класса через указатель на базовый класс с невиртуальным деструктором дает неопределенный результат. На практике это выражается в том, что будет разрушена только часть объекта, соответствующая базовому классу. Если в коде выше убрать ключевое слово virtual перед деструктором базового класса, то вывод программы будет уже иным. Обратите внимание, что член данных obj класса Derived также не разрушается.

Base::ctor()
Object::ctor()
Derived::ctor()
Base::dtor()

Когда же следует объявлять деструктор виртуальным? Cуществует правило — если базовый класс предназначен для полиморфного использования, то его деструктор должен объявляться виртуальным. Для реализации механизма виртуальных функций каждый объект класса хранит указатель на таблицу виртуальных функций vptr, что увеличивает его общий размер. Обычно, при объявлении виртуального деструктора такой класс уже имеет виртуальные функции, и увеличения размера соответствующего объекта не происходит.

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

Виртуальные функции и деструктор / Хабр

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

Сразу же, как обычно, оговорюсь, что: 1) статья моя не претендует на полноту изложения материала; 2) мегапрограммеры ничего нового здесь не узнают; 3) материал не новый и давно описан во многих книгах, но если явно об этом не прочитать и самому специально не задумываться, то можно о некоторых моментах даже не подозревать (до поры, до времени). Также прошу прощения за надуманные примеры 🙂

Виртуальные деструкторы

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

Основное правило: если у вас в классе присутствует хотя бы одна виртуальная функция, деструктор также следует сделать виртуальным. При этом не следует забывать, что деструктор по умолчанию виртуальным не будует, поэтому следует объявить его явно. Если этого не сделать, у вас в программе почти наверняка будут утечки памяти (memory leaks). Чтобы понять почему, опять же много ума не надо. Рассмотрим несколько примеров.

В первом случае создадим объект производного класса в стеке:

#include <cstdlib>
#include <iostream>

using std::cout;
using std::endl;

class A {
public:

    A() { cout << "A()" << endl; }

    ~A() { cout << "~A()" << endl; }
};

class B : public A {
public:

    B() { cout << "B()" << endl; }

    ~B() { cout << "~B()" << endl; }
};

int main()
{

    B b;

    return EXIT_SUCCESS;
}

Всем ясно, что вывод программы будет следующим:

A()

B()

~B()

~A()

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

Попробуем теперь создать тот же объект в динамической памяти, используя при этом указатель на объект базового класса (код классов не изменился, поэтому привожу только код функции main()):

int main()
{

    A * pA = new B;

    delete pA;

    return EXIT_SUCCESS;
}

На сей раз конструируется объект так, как и надо, а при разрушении происходит утечка памяти, потому как деструктор производного класса не вызывается:

A()

B()

~A()

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

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

#include <cstdlib>
#include <iostream>

using std::cout;
using std::endl;

class A {
public:

    A() { cout << "A()" << endl; }

    virtual ~A() { cout << "~A()" << endl; }
};

class B : public A {
public:

    B() { cout << "B()" << endl; }

    ~B() { cout << "~B()" << endl; }
};

int main()
{

    A * pA = new B;

    delete pA;

    return EXIT_SUCCESS;
}

Теперь-то мы получим желаемый порядок вызовов:

A()

B()

~B()

~A()

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

Виртуальные функции в деструкторах

Давайте для начала рассмотрим ситуацию с вызовом виртуальных функций внутри класса. Предположим, что у нас есть Кот, который просит покушать мяуканьем, а затем приступает к процессу 🙂 Так поступают многие коты, но не Чеширский! Чеширский, как известно, мало того что вечно улыбается, так еще и довольно разговорчив, поэтому мы научим его говорить, переопределив метод speak():

#include <cstdlib>
#include <iostream>

using std::cout;
using std::endl;

class Cat
{
public:

    void askForFood() const

    {

        speak();

        eat();

    }

    virtual void speak() const { cout << "Meow! "; }

    virtual void eat() const { cout << "*champing*" << endl; }
};

class CheshireCat : public Cat
{
public:

    virtual void speak() const { cout << "WTF?! Where\'s my milk? =) "; }
};

int main()
{

    Cat * cats[] = { new Cat, new CheshireCat };

    cout << "Ordinary Cat: "; cats[0]->askForFood();

    cout << "Cheshire Cat: "; cats[1]->askForFood();

    delete cats[0]; delete cats[1];

    return EXIT_SUCCESS;
}

Вывод этой программы будет следующим:

Ordinary Cat: Meow! *champing*

Cheshire Cat: WTF?! Where’s my milk? =) *champing*

Рассмотрим код более подробно. Есть класс Cat с парой виртуальных методов, один из которых переопределен в производном CheshireCat. Но всё самое интересное происходит в методе askForFood() класса Cat.

Как видно, метод всего лишь содержит вызовы двух других методов, однако конструкция speak() в данном контексте эквивалента this->speak(), то есть вызов происходит через указатель, а значит — будет использовано позднее связывание. Вот почему при вызове метода askForFood() через указатель на CheshireCat мы видим то, что и хотели: механизм виртуальных функций работает исправно даже несмотря на то, что вызов непосредственно виртуального метода происходит внутри другого метода класса.

А теперь самое интересное: что будет, если попытаться воспользоваться этим в деструкторе? Модернизируем код так, чтобы при деструкции наши питомцы прощались, кто как умеет:

#include <cstdlib>
#include <iostream>

using std::cout;
using std::endl;

class Cat
{
public:

    virtual ~Cat() { sayGoodbye(); }

    void askForFood() const

    {

        speak();

        eat();

    }

    virtual void speak() const { cout << "Meow! "; }

    virtual void eat() const { cout << "*champing*" << endl; }

    virtual void sayGoodbye() const { cout << "Meow-meow!" << endl; }
};

class CheshireCat : public Cat
{
public:

    virtual void speak() const { cout << "WTF?! Where\'s my milk? =) "; }

    virtual void sayGoodbye() const { cout << "Bye-bye! (:" << endl; }
};

int main()
{

    Cat * cats[] = { new Cat, new CheshireCat };

    cout << "Ordinary Cat: "; cats[0]->askForFood();

    cout << "Cheshire Cat: "; cats[1]->askForFood();

    delete cats[0]; delete cats[1];

    return EXIT_SUCCESS;
}

Можно ожидать, что, как и в случае с вызовом метода speak(), будет выполнено позднее связывание, однако это не так:

Ordinary Cat: Meow! *champing*

Cheshire Cat: WTF?! Where’s my milk? =) *champing*

Meow-meow!

Meow-meow!

Почему? Да потому что при вызове виртуальных методов из деструктора компилятор использует не позднее, а раннее связывание. Если подумать, зачем он делает именно так, всё становится очевидным: нужно просто рассмотреть порядок конструирования и разрушения объектов. Все помнят, что конструирование объекта происходит, начиная с базового класса, а разрушение идет в строго обратном порядке. Таким образом, когда мы создаем объект типа CheshireCat, порядок вызовов конструкторов/деструкторов будет таким:

Cat()

CheshireCat()

~CheshireCat()

~Cat()

Если же мы захотим внутри деструктора ~Cat() совершить виртуальный вызов метода sayGoodbye(), то фактически попытаемся обратиться к той части объекта, которая уже была разрушена.

Мораль: если в вашей голове витают помыслы выделить какой-то алгоритм «зачистки» в отдельный метод, переопределяемый в производных классах, а затем виртуально вызывать его в деструкторе, у вас ничего не выйдет.

Есть ли деструктор в C#? / Хабр

Итак, начнем с того, что существует два типа ресурсов — управляемые и неуправляемые. Насчет первых можно совсем не беспокоиться — ими займется сборщик мусора. А вот с неуправляемыми ресурсами дела обстоят куда сложнее. Наш мусорщик не знает, как их освободить, поэтому нам самим приходится заниматься этим вопросом.

Чем отличается деструктор от финализатора

Деструктор — это метод для деинициализации объекта. Здесь важно упомянуть о такой штуке, как deterministic destruction. То есть мы точно знаем, когда объект будет удален. Чаще всего это происходит, когда заканчивается область видимости объекта, или программист явно освобождает память(в с/с++).

А вот определение финализатора из Википедии.

Финализатор — это метод класса, который автоматически вызывается средой исполнения в промежутке времени между моментом, когда объект этого класса опознаётся сборщиком мусора как неиспользуемый, и моментом удаления объекта (освобождения занимаемой им памяти). Это уже обратная штука — nondeterministic destruction.

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

Но деструктор и финализатор в .NET это не то же самое, что просто деструктор и финализатор в обычном мире.

В Visual C# есть финализатор, который создается с помощью синтаксиса создания деструктора в С++, и который некоторыми даже называется как деструктор, хотя таковым не является. Выполнен он через метод Finalize, который нельзя переопределить(в C# нет, но в VB можно), поэтому и приходится использовать синтаксис деструктора через тильду(~ClassName). И только при компиляции в IL, компилятор называет его Finalize. При выполнении этот метод также вызывает финализатор родительского класса.

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

Если заглянуть в спецификацию языка программирования C#(4.0 на данный момент), то там слово «finalizer» ни разу не встречается. Ну это еще можно объяснить. Финализатор тесно связан со сборщиком мусора, который в свою очередь является частью среды выполнения(CLR в нашем случае), но не самого языка программирования.

Теперь пойдем еще дальше и залезем в спецификацию CLI(ECMA-335). Здесь вот что написано.

A class definition that creates an object type can supply an instance method (called a finalizer) to be called

when an instance of the class is no longer reachable.

Это, несомненно, описание финализатора, хотя на мой взгляд, немного неточное.

Далее, идем на msdn. Ни в одной статье не встречается слово finalizer в чистом виде — зато почти всегда используется слово деструктор. Возникает закономерный вопрос — почему люди называют деструктором то, что им не является. Получается, что майкрософтовские разработчики сознательно поменяли значение этого слова. И вот почему.

Мы знаем, что в Visual C# nondeterministic destruction. Это значит, что даже если область видимости объекта закончилась, и сборщик мусора понял, что можно освобождать занимаемую им память, не факт, что это произойдет незамедлительно. То есть это чистой воды финализатор. Так как он использует синтаксис, который во всех языках используется для деструктора, можно предположить, что в Visual C# нет способа определить деструктор(в общем понимании). Это значит, что его просто-напросто нет. Да, необходимости в нем тоже особой нет, но нужно согласиться с тем, что и самого деструктора в Visual C# быть не может.

Вывод — либо я идиот, и совсем все неправильно понял(а вероятность этого довольно высока), либо нужно смириться с этим, и называть деструктором то, что внешне на него похоже(тильда, привет), но по сути является финализатором. Надо же как-то жить в этом мире.

Зачем нам нужен чистый виртуальный деструктор на C++?

основные отношения объектно-ориентированного дизайна два:
Есть-А и есть-А. Я их не выдумывал. Вот как они называются.

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

Has-a указывает, что объект является частью составного класса и что существует отношение собственности. Это означает, что в C++ это объект-член, и как таковой onus находится на классе-владельце, чтобы избавиться от него или передать право собственности перед разрушением себя.

эти два понятия легче реализовать в языках с одним наследованием, чем в модели множественного наследования, такой как C++, но правила по существу одинаковы. Осложнение возникает, когда идентификатор класса неоднозначен, например, передача указателя класса Banana в функцию, которая принимает указатель класса Fruit.

виртуальные функции, во-первых, вещь времени выполнения. Это часть полиморфизма в том, что он используется для решения, какую функцию запускать в то время, когда он вызывается запущенная программа.

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

фруктовый класс может иметь виртуальную функцию color (), которая возвращает » NONE» по умолчанию.
Функция Banana class color() возвращает «желтый» или «коричневый».

но если функция, принимающая фруктовый указатель, вызывает color () в отправленном ей классе Banana-какая функция color () вызывается?
Функция обычно вызывает Fruit:: color() для объекта Fruit.

это было бы 99% времени не то, что предполагалось.
Но если Fruit:: color () был объявлен виртуальным, то Banana:color () будет вызван для объекта, потому что правильная функция color () будет будьте привязаны к указателю Fruit во время вызова.
Среда выполнения проверяет, на какой объект указывает указатель, поскольку он был помечен как виртуальный в определении класса Fruit.

это отличается от переопределения функции в подклассе. В таком случае
фруктовый указатель вызовет Fruit:: color() если все, что он знает, это то, что он-указатель на фрукты.

Итак, теперь возникает идея «чистой виртуальной функции».
Это довольно неудачная фраза, поскольку чистота не имеет ничего общего с ним. Это означает, что предполагается, что метод базового класса никогда не называли.
Действительно чисто виртуальная функция не может быть вызвана. Однако его все еще необходимо определить. Должна существовать сигнатура функции. Многие кодеры делают пустую реализацию {} для полноты, но компилятор будет генерировать ее внутри, если нет. В том случае, когда функция вызывается, даже если указатель на фрукты, Banana:: color() будет вызываться, поскольку это единственная реализация color () там есть.

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

чисто виртуальные конструкторы являются незаконными, полностью. Это просто.

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

 virtual ~Fruit() = 0;  // pure virtual 
 Fruit::~Fruit(){}      // destructor implementation

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

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

вызов для удаления фруктового указателя, указывающего на экземпляр Банан
сначала вызовет Banana::~Banana(), а затем вызовет Fuit::~Fruit (), всегда.
Потому что, несмотря ни на что, когда вы вызываете деструктор подкласса, деструктор базового класса должен следовать.

это плохая модель? Это сложнее на этапе проектирования, да, но это может гарантировать, что правильная компоновка выполняется во время выполнения и что функция подкласса выполняется там, где есть неопределенность относительно того, к какому подклассу осуществляется доступ.

Если вы пишете C++ так, что вы только передайте точные указатели класса без общих или неоднозначных указателей, тогда виртуальные функции действительно не нужны.
Но если вам требуется гибкость во время выполнения типов (как в Apple Banana Orange ==> Fruit), функции становятся проще и универсальнее с меньшим количеством избыточного кода.
Вам больше не нужно писать функцию для каждого типа фруктов, и вы знаете, что каждый фрукт будет реагировать на color() со своей правильной функцией.

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

C++ MythBusters. Миф о виртуальных функциях (дополнение) / Хабр

Преамбула

Добрый вечер (ну, или кому чего).

Не так давно наткнулся на хабре на статью о виртуальных функциях в С++ (она находится сейчас тут). Был бы рад добавить комментарий, но, как оказалось, тут надо иметь регистрацию. Собственно поэтому я и написал этот пост-дополнение к вышеуказанной статье.

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

Статья расчитана на программистов средней и высокой квалификации. Приятного чтения.

Виртуальные конструкторы в C++

Итак, пожалуй начнем с конструкторов. Тут все очень просто — виртуальных конструкторов (а также похожих на них конструкторов) в C++ не существует. Просто потому что не бывает и всё тут (конкретно: это запрещено стандартом языка).

Вы, наверно, спросите: «а зачем такое вообще может понадобится?». На самом деле разговор про «виртуальный конструктор» немного некорректны. Конструктор не может быть виртуальным в смысле виртуальных функций, т.к. чтобы была виртуальность необходимо в конструкторе (а больше и негде особо) настроить указатель на ТВМ (таблицу виртуальных функций) для создаваемого объекта.

Замечание: обычно виртуальность реализуется через ТВМ и указатель на нее в объекте. Подробнее вы можете прочесть об этом тут

Так вот, иногда «виртуальным конструктором» называют механизм создания объект любого заранее неизвестного класса. Это может пригодится, например, при копировании массива объектов, унаследованных от общего предка (при этом нам бы очень хотелось чтобы вызывался конструктор копирования именно нужного нам класса, конечно же). В C++ для подобного, обычно, используют виртуальную функцию вроде virtual void assign (const object &o), или подобную, однако, это не является повсеместным, и возможны другие реализации.

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

А вот деструктор, напротив может быть виртуальным. И даже более того — это часто встречается.
Обычным является использование вирт деструктора в классах, имеющих вирт функции. Более того, gcc, например, выдаст вам предупреждение, если вы не сделаете виртуальным деструктор, объявив виртуальную функцию.

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

Другой миф: чисто виртуальных деструкторов не бывает. Ещё как бывают.

class Sample {
public:
virtual ~Sample()=0;

};

* This source code was highlighted with Source Code Highlighter.

Существует миф, что данный класс является абстрактным. И это верно.
Также распространено заблуждение, что налсденики этого класса будут полиморфными. Это неверно — деструкторы не наследуются.
Существут миф, что наследкника этого класса создать нельзя. Можно, вот пример:

class Sample {
public:
virtual ~Sample()=0{} //обратите особое внимание сюда, так писать по стандарту нельзя, но MS VC проглотит

};

class DSample: public Sample {

};

* This source code was highlighted with Source Code Highlighter.

Для вменяемых компиляторов класс Sample нужно писать так:

class Sample {
public:
virtual ~Sample()=0;

};

Sample::~Sample() {

}

* This source code was highlighted with Source Code Highlighter.

Сразу же замечание про нормальность компилятора, и заодно миф: по стандарту определять чисто вирт. функцию внутри определения класса нельзя. Но определенные корпорации говорят «если мы видим возможность улучшить стандарт, то мы незадумываясь делаем это».

Вы ниразу не видели чисто виртуальных деструкторов с определенными «телами»? Так вот, миф что они несуществуют, также неверен. Также определять можно и другие чисто виртуальные функции.

Почему надо писать именно с определением деструктора? Ответ на самом деле прост: из налсденика DSample в его деструкторе ~DSample будет вызываться деструктор ~Sample, и поэтому его необходимо определить, иначе у вас это даже не будет компилироваться.

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

Замечания об устройстве указаталей на функцию-член

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

Если вы дейтвительно хотите знать как это устроено внутри (заодно сломать мозги), то советую обратиться по адресу www.rsdn.ru/article/cpp/fastdelegate.xml, там все очень подробно описано. Только не говорите что я вас не предупреждал.

Заключение

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

Удачи в программировании.

P.S. перенести бы в CPP блог надо бы… думаю там оно будет более востребовано.

Виртуальный деструктор в C ++

Статьи »

Виртуальный деструктор в C ++

На языке программирования C ++ деструктор полиморфного базового класса должен объявляться виртуальным. Только так безопасное разрушение объекта производного класса через указатель на соответствующий базовый класс.

Рассмотрим следующий пример.

 #include 

используя пространство имен std;

// Вспомогательный класс
объект класса
{
  общественность:
    Object () {cout << "Object :: ctor ()" << endl; }
   ~ Object () {cout << "Object :: dtor ()" << endl; }
};

// Базовый класс
класс Base
{
  общественность:
    Base () {cout << "Base :: ctor ()" << endl; }
    виртуальный ~ Base () {cout << "Base :: dtor ()" << endl; }
    виртуальная пустота print () = 0;
};

// Производный класс
class Derived: public Base
{
  общественность:
    Derived () {cout << "Derived :: ctor ()" << endl; }
   ~ Derived () {cout << "Derived :: dtor ()" << endl; }
    void print () {}
    Object obj;
};

int main ()
{
    База * p = новый производный;
    удалить p;
    возврат 0;
}
 

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

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

 База :: ctor ()
Объект :: ctor ()
Производный :: ctor ()
Производный :: dtor ()
Объект :: dtor ()
База :: dtor ()
 

Уничтожение объекта производного класса через указатель на базовый класс с невиртуальным деструктором дает неопределенный результат.Это выражается в том, что будет разрушена часть объекта, соответствующая базовому классу. Если в коде выше убрать слово виртуальный перед деструктором базового класса, то вывод программы будет уже иным. Обратите внимание, что член данных obj класса Derived также не разрушается.

 База :: ctor ()
Объект :: ctor ()
Производный :: ctor ()
База :: dtor ()
 

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

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

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

.

Виртуальные функции и деструктор / Хабр

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

Сразу же, как обычно, что: 1) статья моя не претендует на полноту изложения материала; 2) мегапрограммеры ничего нового здесь не узнают; 3) материал не новый и давно описан во многих книгах, но если явно об этом не прочитать и самому специально не задумываться, то можно о некоторых моментах даже не подозревать (до поры, до времени). Также прошу прощения за надуманные примеры 🙂

Виртуальные деструкторы

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

Основное правило: если у вас в классе присутствует хотя бы одна виртуальная функция, деструктор также следует сделать виртуальным . При этом не следует забывать, что деструктор по умолчанию виртуальным не будует, поэтому следует объявить его явно. Если этого не сделать, у вас в программе почти наверняка будут утечки памяти (утечки памяти). Чтобы понять почему, опять же много ума не надо. Рассмотрим несколько примеров.

В первом случае создадим объект производного класса в стеке:

#include
#include

using std :: cout;
с использованием std :: endl;

класс A {
общедоступный:
A () {cout << "A ()" << endl; }
~ A () {cout << "~ A ()" << endl; }
};

класс B: общественный A {
общественный:
B () {cout << "B ()" << endl; }
~ B () {cout << "~ B ()" << endl; }
};

int main ()
{
B b;
вернуть EXIT_SUCCESS;
}

Всем ясно, что вывод программы будет следующим:

A ()
В ()
~ B ()
~ А ()

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

Попробуем создать тот же объект в динамической памяти, используя при этом указатель на базового класса (код классов не изменился, поэтому привожу только код функции main ()):

int main ()
{
A * pA = новый B;
удалить pA;
вернуть EXIT_SUCCESS;
}

На сей раз конструируется объект так, как и надо, а при разрушении происходит утечка памяти, потому что как деструктор производного класса не вызывается:

A ()
В ()
~ A ()

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

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

#include
#include

с использованием std :: cout;
с использованием std :: endl;

класс A {
общедоступный:
A () {cout << "A ()" << endl; }
виртуальный ~ A () {cout << "~ A ()" << endl; }
};

класс B: общественный A {
общественный:
B () {cout << "B ()" << endl; }
~ B () {cout << "~ B ()" << endl; }
};

int main ()
{
A * pA = новый B;
удалить pA;
вернуть EXIT_SUCCESS;
}

Теперь-то мы получим желаемый порядок перевозки:

A ()
В ()
~ B ()
~ A ()

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

Виртуальные функции в деструкторах

Давайте для начала рассмотрим ситуацию с вызовом виртуальных функций внутри класса. Предположим, что у нас есть Кот, который просит покушать мяуканьем, а затем приступает к процессу 🙂 Так поступают многие коты, но не Чеширский! Чеширский, как известно, мало что вечно улыбается, поэтому мы научим его говорить, переопределив метод speak ():

#include
#include

using std :: cout;
с использованием std :: endl;

класс Cat
{
общедоступный:
недействительным askForFood () const
{
говорить();
есть();
}
виртуальная пустота Speak () const {cout << "Мяу!"; }
виртуальная пустота eat () const {cout << "* чампинг *" << endl; }
};

класс CheshireCat: общедоступный Cat
{
общедоступный:
virtual void speak () const {cout << "Черт возьми ?! Где мое молоко? =)"; }
};

int main ()
{
Cat * cats [] = {новый Кот, новый Чеширский кот};

cout << "Обычный кот:"; коты [0] -> askForFood ();
cout << "Чеширский кот:"; коты [1] -> askForFood ();

удалить кошек [0]; удалить кошек [1];
вернуть EXIT_SUCCESS;
}

Вывод этой программы будет следующим:

Обычный кот: Мяу! * чавканье *
Чеширский Кот: Что за ?! Где мое молоко? =) * чавкает *

Рассмотрим код более подробно.Есть класс Cat с парой виртуальных методов, один из которых переопределен в производном CheshireCat. Но всё самое интересное происходит в методе askForFood () класса Cat.

Как видно, метод всего лишь содержит вызовы двух других методов, однако конструкция говорит () в данном контексте эквивалента this-> speak (), то есть вызов происходит через указатель, а значит - будет использовано позднее связывание. Вот почему при вызове механизма метода askForFood () через указатель на CheshireCat мы видим то, что и хотели: виртуальные функции работают исправно даже несмотря на то, что сам вызов виртуального метода происходит внутри другого класса.

А теперь самое интересное: что будет, если попытаетесь воспользоваться этим в деструкторе? Модернизируем код так, чтобы при деструкции наши питомцы прощались, кто как умеет:

#include
#include

using std :: cout;
с использованием std :: endl;

класс Cat
{
общедоступный:
виртуальный ~ Cat () {sayGoodbye (); }
недействительным askForFood () const
{
говорить();
есть();
}
виртуальная пустота Speak () const {cout << "Мяу!"; }
виртуальная пустота eat () const {cout << "* чампинг *" << endl; }
virtual void sayGoodbye () const {cout << "Мяу-мяу!" << endl; }
};

класс CheshireCat: общедоступный Cat
{
общедоступный:
virtual void speak () const {cout << "Черт возьми ?! Где мое молоко? =)"; }
virtual void sayGoodbye () const {cout << "До свидания! (:" << endl;}
};

int main ()
{
Cat * cats [] = {новый Кот, новый Чеширский кот};

cout << "Обычный кот:"; коты [0] -> askForFood ();
cout << "Чеширский кот:"; коты [1] -> askForFood ();

удалить кошек [0]; удалить кошек [1];
вернуть EXIT_SUCCESS;
}

Можно ожидать, что, как и в случае с вызовом метода Speak (), однако будет выполнено позднее связывание, это не так:

Обычная кошка: Мяу! * чавканье *
Чеширский Кот: Что за ?! Где мое молоко? =) * чавканье *
Мяу-мяу!
Мяу-мяу!

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

Cat ()
Чеширский кот ()
~ CheshireCat ()
~ Cat ()

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

Мораль: если в вашей голове витают помыслы, какой-то алгоритм «зачистки» в отдельном методе, переопределяемый в производных классах, а виртуально вызвать его в деструкторе, у вас ничего не выйдет.

.

Добавить комментарий

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