Если инициализировать указатель родительского класса адресом объекта дочернего класса и объявить в родительском классе деструктор виртуальным, то при "разрушении" дочернего объекта через этот указатель сначала выполнится деструктор дочернего класса, а потом деструктор родителя. То, что выполняется деструктор дочернего объекта — мне понятно, потому что мы объявили деструктор родителя виртуальным. Но почему выполняется деструктор родительского класса? Есть ли этому логичное объяснение или это нужно принять как данность? Ведь при выполнении метода через указатель или ссылку родительского типа, если одноименный метод в предке объявлен виртуальным, исполняется только один метод, тот, который принадлежит дочернему объекту.
Поделитесь, пожалуйста!
Компьютер сделает всё, что вы ему скажете, но это может сильно отличаться от того, что вы имели в виду.
Невозможное мы сделаем сегодня — чудо займет немного больше времени. /Аноним/
Здравствуйте, cppNewbie, Вы писали:
Уважаемый cppNewbie,
Давайте начнем не с кода, а с примера:
У Вас 2 цилиндра, один вложен в другой, если Вы захотите
использовать первый цилиндр, от второго придется освободится (ввиду
возможных неоднозначностей, цилиндры находятся в вертикальном
положении, т.е. стоят на чем-то). В каком порядке ? В порядке
подчиняющемуся закону стека: последним пришел, первым вышел. Иначе
никак, второй придется вынуть из первого. Теперь последовательно
рассмотрим возможные варианты, вызова кострукторов и деструкторов.
=======================================================================
Пример N%1, нет виртуальности в деструкторах, как впрочем нет и
"Если инициализировать указатель родительского класса адресом объекта
дочернего класса ", просто наследование:
#include "stdafx.h"
#include <iostream.h>
class A {
public:
A() { cout << "constructor A()\n"; }
~A() { cout << "destructor ~A()\n"; }
};
class B : public A {
public:
B() { cout << "constructor B()\n"; }
~B() { cout << "destructor ~B()\n"; }
};
void main( void )
{
B* ptrB = new B();
delete ptrB;
}
Результат:
constructor A()
constructor B()
destructor ~B()
destructor ~A()
Все понятно верно ? Один конструктор вызывает другой, деструкторы
работают по правилу — стек... Все прекрасно и удивительно, но где и как
это ВИДНО !!! Какого рожна это работает именно так, а не по другому.
Давайте расставим все точки над i.
Кострукторы и множественное наследование.
class A {};
class B {};
class C : public A, B {};
Итак конструктор класса C:
C::C() : A(), B()
{
...// дополнительная инициализация.
}
последовательно вызывает конструкторы классов A(), B(). Другими
словами вызов конструкторов классов предков происходит ДО !!!
чтения первого оператора тела конструктора потомка.
Думаю, логика в вызове конструкторов именно с таким алгоритмом
прозрачна, свяжем с цилиндрами: чтобы понять, что из себя
представляает цилиндр А, необходимо рассмотреть цилиндр C, убрать его,
рассмотреть цилиндр B, убрать его, увидев наконец цилиндр А. По
другому если мы хотим достучатся до внутренности цилиндра A, мы должны
последовательно вынуть C, затем B.
Что происходит с деструкторами, обратная ситуация... Если Вы в
трубу затолкали последовательно черный мяч, затем белый, то что бы
снова лицезреть черный мяч, необходимо вынуть белый...
Видимо поэтому для графического представления наследования используют
стрелки влево(или вверх):
A
<--B
<--C
======================================================================
Пример N%2, снова нет виртуальности в деструкторах, НО "Если
инициализировать указатель родительского класса адресом объекта
дочернего класса ", есть:
class A {
public:
A() { cout << "constructor A()\n"; }
~A() { cout << "destructor ~A()\n"; }
};
class B : public A {
public:
B() { cout << "constructor B()\n"; }
~B() { cout << "destructor ~B()\n"; }
};
void main( void )
{
B* ptr = new А();
delete ptr;
}
Результат:
constructor A()
constructor B()
destructor ~A()
У нас зависла строка "destructor ~B()", другими словами деструктор
класса B не выполнился. Как пишет Джефф Элджер "... переменная
превращается в "Летучего Голландца", обреченного на вечные скитания в
памяти. Чтобы избежать беды, достаточно обьявить оба деструктора
виртуальными, в этом случае независимо от типа указателя ( кроме конечно
void* ) уничтожение будет начинаться с класса потомка...".
И все ВСЕ !!! Но почему ? Это еще один постулат не более !!!
На самом деле ответ в этой фразе есть, просто Д. Элджер не
раскрывает его в полной мере — "...независимо от типа указателя...",
другими словами, через какой класс выражен наш указатель("ptr" —
пример N%2). Он относится к классу B или А ? Конечно к классу А, скажете
Вы, потому-что... Стоп, без потому-что. Я не верю, что это класс A, как
проверить... Просто напишем в каждом классе функцию с одинаковым имемен,
посмотрим, функция какого класса сработает ! Это раз, два наш пример с
цилиндрами перестает работать на данном этапе, потому как мы подменяем
один цилиндр другим, что в природе без "волшебства" сделать невозможно.
======================================================================
Пример N%3, компилятор ругается но работает, мы же перекрываем
функцию родительского класса:
class A {
public:
foo() { cout << "Call function of class A \n"; }
};
class B : public A {
public:
foo() { cout << "Call function of class B \n"; }
};
void main( void )
{
A* ptr = new B();
ptr->foo();
delete ptr;
}
Результат:
Call function of class A,
Итак вы были правы, в примере N%2, создается указатель на класс A.
Итак мы вплотную подошли к понятию полиморфизма — двуликости
указателя на какой-то класс. Имеется ввиду следущее, если мы перед
функцией foo() в базовом классе A поставим virtual, картина
кардинальным образом изменится.
======================================================================
Пример N%4, компилятор не ругается все корректно, но ptr —
указыбает на класс B !!!:
class A {
public:
virtual foo() { cout << "Call function of class A \n"; }
};
class B : public A {
public:
foo() { cout << "Call function of class B \n"; }
};
void main( void )
{
A* ptr = new B();
ptr->foo();
delete ptr;
}
Результат:
Call function of class B.
======================================================================
Пример N%5, данный пример составлен по аналогии с примером N%4:
#include "stdafx.h"
#include <iostream.h>
class A {
public:
virtual ~A() { cout << "destructor ~A()\n"; }
};
class B : public A {
public:
~B() { cout << "destructor ~B()\n"; }
};
Результат:
destructor ~B()
destructor ~A()
Ну вот собственно говоря и все... Разговор свелся к понятию полиморфизма,
стеку и виртуальным функциям.
Уважаемый cppNewbie, C++ полон неоднозначностей, поэтому, примите
мой скромный совет, постарайтесь, в ситуациях, когда ответ не лежит на
поверхности, искать аналогии в жизни, это часто наводит на решение
проблемы, ведь "...Речь идет не о не о строении компьютерного языка, а
скорее о нашем строении. Все уродства C++ — это в основном наши
уродства..." Джефф Элджер.
Удачи...
Исправлено форматирование. Не забывайте, пожалуйста, пользоваться тегами [c]...[/c], [code]....[/code] и т.п. для выделения фрагментов кода. -- ПК
Спасибо, Шахтер. Хоть смысл ответа на мой вопрос до вас пытались донести другие "форумчане" (спасибо им за это большое!), ваш ответ был для меня наиболее ясен и, в то же время, достаточно лаконичен. Благодарю.
Легче одурачить людей, чем убедить их в том, что они одурачены. — Марк Твен
Never argue with a woman who reads. It's likely she can also think. (c)
Never argue with a woman who reads. It's likely she can also think. (c)