https://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,
может все таки
?
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-х байтов - это, ИМХО, не аргумент.
:
: