Форум программистов «Весельчак У»
  *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

  • Рекомендуем проверить настройки временной зоны в вашем профиле (страница "Внешний вид форума", пункт "Часовой пояс:").
  • У нас больше нет рассылок. Если вам приходят письма от наших бывших рассылок mail.ru и subscribe.ru, то знайте, что это не мы рассылаем.
   Начало  
Наши сайты
Помощь Поиск Календарь Почта Войти Регистрация  
 
Страниц: [1]   Вниз
  Печать  
Автор Тема: Виртуальные деструкторы  (Прочитано 13879 раз)
0 Пользователей и 1 Гость смотрят эту тему.
acc15
Гость
« : 14-06-2006 08:52 » 

Вариант 1:
Код:
class A
{
public:
  A() {}
  ~A() {}
};

class B
{
public:
  B() {}
  ~B() {}
};

Вариант 2:
Код:
class A
{
public:
  A() {}
  virtual ~A() {}
};

class B
{
public:
  B() {}
  virtual ~B() {}
};

Два варианта рабочии, т.к. при создании B вызывается сначала конструктор A, потом конструктор B, а при уничтожении наоборот сначала ~B() а потом ~A

Какая разница между двумя вариантами??

Записан
nikedeforest
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #1 : 14-06-2006 09:42 » 

acc15,
может все таки
Код:
class B: public A
?
Записан

ещё один вопрос ...
npak
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #2 : 14-06-2006 12:58 » 

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

Пример.  Создадим пару классов.  В первой паре, ANonVirt и BNonVirt - деструктор не виртуальный. Во второй паре AVirt и BVirt используется виртуальный деструктор.

Код:
#include <iostream>

using namespace std;

class ANonVirt {
public:
    ~ANonVirt() { cerr << "ANonVirt" << endl; }
};

class BNonVirt : public ANonVirt {
public:
    ~BNonVirt() { cerr << "BNonVirt" << endl; }
};

class AVirt {
public:
    virtual ~AVirt() { cerr << "AVirt" << endl; }
};

class BVirt : public AVirt {
public:
    virtual ~BVirt() { cerr << "BVirt" << endl; }
};

Теперь подготовим два тестовых примера. Первый пример создаёт автоматические переменные и удаляет их.  Второй пример создаёт указатели на класс-потомок и присваивает его указателю на базовый класс (полиморфизм).

Код:
void test_local_objects() {
    {
        cerr << "+++ BNonVirt bnv;" << endl;
        BNonVirt bnv;
        cerr << "+++ Destroy bnv" << endl;
    }
    {
        cerr << "+++ BVirt bv;" << endl;
        BVirt bv;
        cerr << "+++ Destroy bv" << endl;
    }
}

void test_pointers() {
    cerr << "+++ ANonVirt * p_bnv = new BNonVirt()" << endl;
    ANonVirt * p_bnv = new BNonVirt();
    cerr << "+++ Delete p_bnv" << endl;
    delete p_bnv;

    cerr << "+++ AVirt * p_bv = new BVirt()" << endl;
    AVirt * p_bv = new BVirt();
    cerr << "+++ Delete p_bv" << endl;
    delete p_bv;
}

Если примеры выполнить, например так
Код:
int main() {
    cerr << "Testing local objects" << endl << endl;
    test_local_objects();
    cerr << endl << "Testing pointers" << endl << endl;
    test_pointers();
    return 0;
}
то получится вот что.

Для автоматических объектов трасса событий такая:
Цитата
+++ BNonVirt bnv;
+++ Destroy bnv
BNonVirt
ANonVirt
+++ BVirt bv;
+++ Destroy bv
BVirt
AVirt

Действительно, в обоих случаях вызываются как деструктор потомка, так и деструктор предка.
Теперь посмотрим, что получилось для виртуальных деструкторов.
Цитата
+++ ANonVirt * p_bnv = new BNonVirt()
+++ Delete p_bnv
ANonVirt
+++ AVirt * p_bv = new BVirt()
+++ Delete p_bv
BVirt
AVirt
Нетрудно заметить, что для указателя p_bnv на класс с невиртуальным деструктором вызывается только деструктор базового класса, так как формальный тип указателя ANonVirt*.  Так как деструктор не виртуальный, то генерируется код удаления объекта, который просто вызывает деструктор из таблицы методов типа ANonVirt.
Для указателя p_bv на тип с виртуальным деструктором вызываются оба деструктора, так как компилятор "помнит", что указатель привязан к объекту.  Для удаления такого объекта генерируется код, который берёт указатель на метод деструктора из таблицы виртуальных функций самого объекта, а не типа AVirt.  Тот, в свою очередь, "знает" о деструкторе предка и, соответственно, вызывает предка, и так далее до самого первого предка.

Виртуальные деструкторы необходимы в тех случаях, когда используются полиморфные указатели.
« Последнее редактирование: 14-06-2006 13:02 от npak » Записан

UniTesK -- индустриальная технология надежного тестирования.

http://www.unitesk.com/ru/
sss
Специалист

ru
Offline Offline

« Ответ #3 : 15-06-2006 00:56 » 

Есть еще один замес...

Код:
class A
{
  virtual void Clear() {...};
  virtual ~A() { Clear();};
};
class B : public A
{
  virtual void Clear() {...};
  virtual ~B() {};
};

При удалении экземпляра класса B, будет вызван B:~B() затем A::~A(), но вызов Clear() внутри деструктора вызовет не метод класса B::Clear (хотя он и виртуальный),  a метод A::Clear.
Записан

while (8==8)
npak
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #4 : 15-06-2006 11:26 » 

Это разумно.  К тому моменту, как в ~A будет вызван Clear, содержимое объекта B будет зачищено и метод B::Clear может фатально навернуться.
Записан

UniTesK -- индустриальная технология надежного тестирования.

http://www.unitesk.com/ru/
LP
Помогающий

ru
Offline Offline

« Ответ #5 : 17-06-2006 10:19 » 

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

При полиморфном использовании первой иерархии, возникает несколько проблем:
1) При удалении объекта производного класса через указатель на базовый не вызовется деструктор производного класса.
(То о чем говорил npak).

2) При множественном наследовании, при удалении объекта производного класса через указатель на второй базовый класс,
функции operator delete(void*) будет передан неправильный адрес, в результате чего программа упадет.
Код:
struct B1
{
int i;
//virtual ~B1(){}
};

struct B2
{
int j;
//virtual ~B2(){}
};

struct D : public B1, public B2
{
int k;
};

int main()
{
B2* pb = new D; // 1
delete pb; // 2
}
Дело в том, что в строке (1) при приведении D* к B2* к исходному указателю прибавляется 4 байта,
чтобы он указывал на подобъект 2-го базового класса. При отсутствии вирт. деструктора в строке (2)
адрес не будет скорректирован обратно на эти 4 байта, в результате чего получается что мы удаляем память по левому адресу,
что, естественно, приводит к падению программы.

3)
Код:
#include <iostream>

struct B
{
    void* operator new(size_t size)
    {
        return malloc(size);
    }
    void operator delete(void* p, size_t size)
    {
        std::cout << size << std::endl;
        free(p);
    }
    ~B(){}
    int i;
};

struct D: B
{
    double d;
};

int main()
{
    B* pb = new D;                     
    delete pb;    //функции operator delete передается неправильный размер sizeof(B)
                      //а с виртуальным деструктором было бы sizeof(D).
}


Хотя, строго говоря, все это особенности реализации компилятора. По стандарту инструкция delete pbase, при отсутствии вирт. деструктора просто приводит к неопределенному поведению.
« Последнее редактирование: 17-06-2006 15:02 от LP » Записан

Если эта надпись уменьшается, значит ваш монитор уносят
npak
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #6 : 19-06-2006 07:27 » 

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

LP - ты хороший человек, и ответы даёшь грамотные.  Но вот эти микрооптимизации по 4 байта на объект лично мне не понятны.  Сколько объектов одновременно живут в работающей системе, написанной на Си++?  Скажем ГУИ с запросами к базе данных?  Тысяча?  Десять тысяч?  Или даже до тысячи не дотягивает? В любом случае эта оптимизация даст выигрыш не больше 40 кБ.  Для современных десктопов это тьфу!  Не стоит даже мараться, чтобы потом не искать в дебаггере, где память утекает.

Экономия 4-х байтов - это, ИМХО, не аргумент.
Записан

UniTesK -- индустриальная технология надежного тестирования.

http://www.unitesk.com/ru/
Алексей++
глобальный и пушистый
Глобальный модератор

ru
Offline Offline
Сообщений: 13


WWW
« Ответ #7 : 21-06-2006 15:29 » 

( npak, ок, взял тему на заметку )
Записан

LP
Помогающий

ru
Offline Offline

« Ответ #8 : 22-06-2006 07:44 » 

npak, Улыбаюсь acc15 спросил в чем разница, - я просто проконстатировал факт, что эти 4 байта никому не нужны. Я не говорил хорошо это или плохо. Хотя раз уж зашел разговор, то я считаю это серьезным аргументом. Да-да Делать виртуальный деструктор для класса у которого нет других виртуальных функций, и следовательно он не предназначался для полиморфного использования, - просто-напросто противоестественно.
Кроме того, к примеру, если большие массивы объектов(не указателей) некоторого класса интенсивно используются в критически важных для эффективности программы алгоритмах, то добавление 4-х байт вполне может повлиять на эффективность. Без этих 4-х байт, допустим, объекты могли бы целиком помещаться в регистр. (класс Point, COORD и т. п.).
Цитата
Не стоит даже мараться, чтобы потом не искать в дебаггере, где память утекает.
Если память течет, то можно, конечно, добавить вирт. деструктор в "реальной жизни" Улыбаюсь. Но это не есть хороший стиль, ИМХО. Здесь, виноват сам программист, что кастит неполиморфные указатели на производный класс к указателям на базовый а затем их удаляет.
P.S. Кстати, у меня давно живет мысль о том, что неплохо было бы, если бы язык запрещал неявно кастить указатели на производный к указателям на базовый класс, если у базового нет виртуальных функций (или по крайней мере виртуального деструктора) - ведь это 100%-ая ошибка, а вынуждал, в этом случае, делать явное приведение, чтобы гарантировать, что программист знает что делает.
« Последнее редактирование: 22-06-2006 08:09 от LP » Записан

Если эта надпись уменьшается, значит ваш монитор уносят
npak
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #9 : 22-06-2006 07:53 » 

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

UniTesK -- индустриальная технология надежного тестирования.

http://www.unitesk.com/ru/
acc15
Гость
« Ответ #10 : 22-06-2006 08:27 » 

лично я всё понял после первого же сообщения прака... =)
А по поводу 4 байтов (а если учесть переносимость программы/библиотеки для будущего, где будут использоваться 64 битные адреса, то это уже 8 байт) то LP прав, т.к. просто неэффективно добавлять виртуальные деструкторы, если в программе/библиотеке вообще не будут использоваться полиморфные указатели.


Тема закрыта.
« Последнее редактирование: 22-06-2006 08:38 от acc15 » Записан
Страниц: [1]   Вверх
  Печать  
 

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines