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

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

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

« : 19-06-2006 07:19 » 

http://forum.shelek.ru/index.php?topic=9064.new

acc15 :
Вариант 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 :
acc15,
может все таки
Код:
class B: public A
?


npak :

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

Пример.  Создадим пару классов.  В первой паре, 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.  Тот, в свою очередь, "знает" о деструкторе предка и, соответственно, вызывает предка, и так далее до самого первого предка.

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

sss :

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

Код:
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.


npak :

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


LP :

Цитата
Какая разница между двумя вариантами??
Первая иерархия классов предназначена для неполиморфного использования, а вторая для полиморфного.
Если ты не планируешь указатели на 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, при отсутствии вирт. деструктора просто приводит к неопределенному поведению.


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

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

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


:


:

« Последнее редактирование: 11-05-2007 17:39 от Алексей1153++ » Записан

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

http://www.unitesk.com/ru/
Страниц: [1]   Вверх
  Печать  
 

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines