Перейти к содержимому

Виртуальный деструктор c зачем нужен

  • автор:

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

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

#include using namespace std; struct Base < Base() < cout ~Base() < cout >; struct Derived: public Base < Derived() < cout ~Derived() < cout >; int main()

Что вы могли ожидать на выходе:

Base() Derived() ~Derived() ~Base() 

Что может произойти (может, потому что, в общем случае, это undefined behaviour):

Base() Derived() ~Base() 

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

Виртуальный деструктор c зачем нужен

Здравствуйте, Аноним, Вы писали:

А>Для чего нужен виртуальный деструктор. И в каких случаях он необходим.

Виртуальные функции (и деструктор в частности) в С++ нужны для того, чтобы вызывать перегруженный в классе-потомке вариант соотв. функции через указатель на базовый класс. Применительно к деструктору это означает, что объект класса-потомка D можно корректно удалить, имея указатель на базовый класс B:

 class B < public: virtual ~B() <> >; . class D : public B < public: ~D() <> > . B * pObj = new D; . delete pObj; // - если бы не virtual ~B(), ~D() не вызвался бы.

подробнее — у Страуструпа.

Re: для чего нужен виртуальный деструктор

От: Stuw
Дата: 17.04.06 12:12
Оценка:

Здравствуйте, Аноним, Вы писали:

А>Для чего нужен виртуальный деструктор. И в каких случаях он необходим.

Для того же для чего и виртуальные функции 🙂 Чтобы вызывался деструктор реального типа класса, а не типа класса на который указывает ссылка

Re[2]: для чего нужен виртуальный деструктор

От: Константин Л.
Дата: 17.04.06 13:17
Оценка: 1 (1)

Здравствуйте, Stuw, Вы писали:

S>Здравствуйте, Аноним, Вы писали:

А>>Для чего нужен виртуальный деструктор. И в каких случаях он необходим.

S>Для того же для чего и виртуальные функции Чтобы вызывался деструктор реального типа класса, а не только типа класса на который указывает указатель

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

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

Re[3]: для чего нужен виртуальный деструктор

От: halka
Дата: 17.04.06 15:22
Оценка:

Здравствуйте, Константин Л., Вы писали:

КЛ> За подробностями к Саттеру
Или к Майерсу.
Re: для чего нужен виртуальный деструктор

От: Аноним
Дата: 09.10.06 13:13
Оценка:

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

Re[2]: для чего нужен виртуальный деструктор

От: artiz
Дата: 09.10.06 13:36
Оценка:

Здравствуйте, Аноним, Вы писали:
А> сорри, а для чего практически нужны виртуальные функции? т.е. не не рассказывайте про какой-нить
А> академический пример, а в реальной практике в какой ситуации вирт. функции могут быть полезны и быть
А> более эффективны, чем использование обычных функций?

Примеров — как говорицца море — первое что приходит на ум — векторный графический редакток в котором все отображаемые граф. объекты представляются потомками одного класса — Shape например который имеет виртуальную функцию для отрисовки — draw — и реализуем для каждого из потомков (Circle, Quad. ) — только свою функцию отрисовки — о все остальное — функции перемещения, свойства для цвета фона, границы и т.д. — реализовать в базовом классе.

Преимущества:
1. уменьшее размера кода (существенное
2. повышение структурированности приложения
3. повышение скорости разработки

Re[2]: для чего нужен виртуальный деструктор

От: Roman Odaisky
Дата: 09.10.06 13:40
Оценка:

Здравствуйте, Аноним, Вы писали:

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

#include 

(эта библиотека, кроме того, иллюстрирует еще один хороший принцип — NVI)

Во многих управляемых языках (Java) все вызовы виртуальные. В COM все вызовы виртуальные. (AFAIR)

До последнего не верил в пирамиду Лебедева.
Re[3]: для чего нужны виртуальные функции

От: Аноним
Дата: 26.10.06 12:37
Оценка:

A>Примеров — как говорицца море — первое что приходит на ум — векторный графический редакток в котором все отображаемые граф. объекты представляются потомками одного класса — Shape например который имеет виртуальную функцию для отрисовки — draw — и реализуем для каждого из потомков (Circle, Quad. ) — только свою функцию отрисовки — о все остальное — функции перемещения, свойства для цвета фона, границы и т.д. — реализовать в базовом классе.

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

A>Преимущества:
A>1. уменьшее размера кода (существенное
то же самое будет

A>2. повышение структурированности приложения
то же самое будет

A>3. повышение скорости разработки
то же самое будет

Re[4]: для чего нужны виртуальные функции

От: LaptevVV
Дата: 26.10.06 13:02
Оценка: 1 (1)

Здравствуйте, Аноним, Вы писали:

А> хорошо. но то же самое можно сделать и без вирт функций. т.е. базовый класс с общими вещами, наследники с отрисовкой. просто будет функция отрисовки потомков не виртуальная. в чём тут недостаток будет?
Смотри сюда. Тут без виртуальности — ну никак!

Зачем нужны виртуальные функции
При наследовании часто бывает необходимо, чтобы поведение некоторых методов базового класса и классов-наследников отличались. Решение, на первый взгляд, очевидное: переопределить соответствующие методы в производном классе. Однако тут возникает одна проблема, которую лучше рассмотреть на простом примере (листинг 9.1).

//Листинг 9.1. Необходимость виртуальных функций #include using namespace std; class Base // базовый класс < public: int f(const int &d) // метод базового класса < return 2*d; > int CallFunction(const int &d) // предполагается < return f(d)+1; // вызов метода базового класса > >; class Derived: public Base // производный класс < public: // CallFunction наследуется int f(const int &d) // метод f переопределяется < return d*d; > >; int main() < Base a; // объект базового класса cout // получаем 11 Derived b; // объект производного власса cout // какой метод f вызывается? return 0; >

В базовом классе определены два метода — f() и CallFunction(), — причем во втором методе вызывается первый. В классе-наследнике метод f() переопределен, а метод CallFunction() унаследован. Очевидно, метод f() переопределяется для того, чтобы объекты базового класса и класса-наследника вели себя по-разному. Объявляя объект b типа Derived, программист, естественно, ожидает получить результат 5 * 5 + 1 = 26 — для этого и переопределялся метод f(). Однако на экран, как и для объекта а типа Base, выводится число 11, которое очевидно вычисляется как 2 * 5 + 1 = 11. Несмотря на переопределение метода f() в классе-наследнике, в унаследованной функции CallFunction() вызывается «родная» функция f(), определенная в базовом классе!
Аналогичная проблема возникает и в несколько другом контексте: при подстановке ссылки или указателя на объект производного класса вместо ссылки или указателя на объект базового. Рассмотрим опять пример с часами и будильником (листинг 9.2).

//Листинг 9.2. Неожиданная работа принципа подстановки class Clock // базовый класс — часы < public: void print() const < cout "Clock!" >; class Alarm: public Clock // производный класс — будильник < public: void print() const // переопределенный метод < cout "Alarm!" >; void settime(Clock &d) // функция установки времени < d.print(); >// предполагается вызов метода базового класса //. Clock W; // объект базового класса settime(W); // выводится "Clock" Alarm U; // объект производного класса settime(U); // ссылка на производный вместо базового Clock *c1 = &W; // адрес объекта базового класса c1->print(); // вызов базового метода c1 = &U; // адрес объекта производного типа вместо базового c1->print(); // какой метод вызываетя, базовый или производный?

Опять в классе-наследнике переопределен метод для того, чтобы обеспечить различное поведение объектов базового и производного классов. Однако и при передаче параметра по ссылке базового класса в функцию settime(), и при явном вызове метода print() через указатель базового класса наблюдается одна и та же картина: всегда вызывается метод базового класса, хотя намерения программиста состоят в том, чтобы вызвать метод производного.
Для того чтобы разобраться в ситуации, необходимо уяснить, что такое связывание. Связывание — это сопоставление вызова функции с телом. В приведенных ранее примерах связывание выполняется на этапе трансляции (до запуска) программы. Такое связывание обычно называют ранним, или статическим.

При трансляции класса Base (см. листинг 9.1) компилятор ничего не знает о классах-наследниках , поэтому он не может предполагать, что метод f() будет переопределен в классе Derived. Его естественное поведение — «прочно» связать вызов f() с телом метода класса Base. Аналогично при трансляции функции settime() компилятору ничего не известно о типе реально передаваемого объекта во время выполнения программы. Поэтому вызов метода print() связывается с телом метода базового класса Clock, как и определено в заголовке функции settime(). Точно так же указатель на базовый класс «прочно» связывается с методом базового класса во время трансляции.
Конечно, при вызове метода по указателю в данном конкретном случае мы можем вызвать метод производного класса, задав явное преобразование указателя:

static_cast(c1)->print();
((Alarm *)c1)->print(); // "лишние" скобки нужны!

Однако для функции settime() и метода CallFunction() это сделать невозможно — нам необходимо именно разное поведение в зависимости от типа объекта. Да и с указателем не все так просто: если такой вызов прописан внутри функции, которая принимает этот указатель как параметр (например, settime(Clock *c1)), то мы имеем те же проблемы.
Определение виртуальных функций
Получается, что в С++ должен существовать механизм, с помощью которого можно узнать тип объекта во время выполнения программы. Такой механизм в С++ есть и он, как уже отмечалось, называется динамической идентификацией типов (RTTI). Однако в ситуациях, подобных описанным, применяется другой, более «сильный» и элегантный механизм С++ — механизм виртуальных функций (см. п. 10.3 в Стандарте).
Чтобы добиться разного поведения в зависимости от типа, необходимо объявить функцию-метод виртуальной; в С++ это делается с помощью ключевого слова virtual. Таким образом, в листинге 9.1 объявление метода f() в базовом и производном классе должно быть таким:

virtual int f(const int &d) // в базовом классе < return 2*d; > virtual int f(const int &d) // в производном классе < return d*d; >

После этого для объектов базового и производного классов мы получаем разные результаты: 11 и 26.
Аналогично в листинге 9.2 объявление метода print() тоже должно начинаться со слова virtual:

irtual void print() const // в базовом классе < cout "Clock!" virtual void print() const // в производном классе < cout "Alarm!" 
  • статический полиморфизм, или полиморфизм времени компиляции (compile-time polymorphism), осуществляется за счет перегрузки и шаблонов функций;
  • динамический полиморфизм, или полиморфизм времени выполнения (run-time polymorphism), реализуется виртуальными функциями.

Хочешь быть счастливым — будь им!
Без булдырабыз.
Re[4]: для чего нужны виртуальные функции

От: Кодт
Дата: 26.10.06 15:41
Оценка:

Здравствуйте, <Аноним>, Вы писали:

А> хорошо. но то же самое можно сделать и без вирт функций. т.е. базовый класс с общими вещами, наследники с отрисовкой. просто будет функция отрисовки потомков не виртуальная. в чём тут недостаток будет?

Так или иначе придётся отсылаться к реализациям функций потомков из базового. Это можно сделать:
— pattern matching’ом — попросту, нагородить ветвлений (if, switch/case) по значению тэга, определённого в базе и установленного потомком
— указателями на функции (полями базы, опять же установленными потомком)
— перекрытыми виртуальными функциями (те же указатели, но всё сделано компилятором и положено в VMT)

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

Виртуальные функции могут быть переопределены в классах наследниках, но как деструктор может быть виртуальным?

22.04.2016 в 16:08 #2721

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

#include #include #include class Profession < public: virtual void work() < std::cout >; class Programmer: public Profession < std::string m_code; public: Programmer() < m_code = "programmer writes code"; >virtual void work() < std::cout >; class Pilot: public Profession < public: virtual void work() < std::cout >; int main() < std::vectorworkers; Pilot pilot; workers.push_back(new Pilot()); workers.push_back(new Programmer()); for (Profession* worker : workers) < worker->work(); > pilot.work(); for (Profession* worker : workers) < delete worker; >>

virtual_destructor_valgrind

В данном примере создается список профессий, в который помещается один пилот и один программист. У каждого из них вызывается виртуальная функция work() . Вызов функции у нас происходит через указатель на базовый класс, статический тип объекта — Profession , поэтому если бы функция была не виртуальной — сработала бы функция базового класса. Однако из-за того, что функция виртуальная — используется динамический тип объекта ( Programmer и Pilot соответственно). Такой эффект достигается за счет использования таблицы виртуальных функций, которая есть в каждом объекте, содержащем хоть одну виртуальную функцию. В этой таблице хранятся адреса функций, которые помещаются в таблицу конструктором и используются при вызове. Кроме того, в примере создается объект pilot (на стеке, как и vector ). Деструктор для него будет вызван по окончанию работы функции main() . Деструктор дочернего класса освобождает память из своих данных-членов и всегда вызывает деструктор базового класса. Таким образом, в данном случае будет вызван сначала ~Pilot() , а затем ~Profession() . В рассмотренном примере есть проблема, связанная с тем, что компилятор автоматически создаст в классе невиртуальный деструктор. В связи с этим, при разрушении объекта будет вызван деструктор, соответствующий статическому типу объекта — для обоих рабочих вызовется ~Profession() . Однако, в классе Programmer есть поле, в которое помещается строка — его деструктор базового класса удалять не будет. Убедиться в этом можно с помощью valgrind memcheck : Чтобы решить проблему достаточно сделать деструктор базового класса виртуальным — в этом случае при разрешении объекта правильная реализация деструктора будет выбираться из таблицы виртуальных функций, т.е. использоваться динамический тип объекта:

virtual ~Profession()

Просмотр 1 ветки ответов

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

Давайте разберемся, зачем нужны виртуальные методы. Рассмотрим следующий код:

class Foo < public: void f(); >; class Bar : public Foo < public: void f(); >Foo *p = new Bar(); p->f(); 

Вызывая p->f() , мы обращаемся к Foo::f() . Это потому, что р — указатель на Foo, a f() — невиртуальная функция.

Чтобы гарантировать, что p->f() вызовет нужную реализацию f(), необходимо объявить f() как виртуальную функцию.

Теперь вернемся к деструктору. Деструкторы предназначены для очистки памяти и ресурсов. Если деструктор Foo не является виртуальным, то при уничтожении объект Bar все равно будет вызван деструктор базового класса Foo.

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

Разбор взят из книги Гейл Л. Макдауэлл «Cracking the Coding Interview» (есть в переводе).

Следите за новыми постами по любимым темам

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

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

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