Деструкторы
Когда заканчивается время жизни объекта производного класса, автоматически вызываются деструкторы производного и базового классов (если они определены), а также деструкторы всех объектов-членов. Например, если имеется объект класса NameQuery:
NameQuery nq( "hyperion" );
то порядок вызова деструкторов следующий: сначала деструктор NameQuery, затем деструктор string для члена _name и наконец деструктор базового класса. В общем случае эта последовательность противоположна порядку вызова конструкторов.
Вот деструкторы нашего базового Query и производных от него (все они объявлены открытыми членами соответствующих классов):
inline Query:: ~Query(){ delete _solution; } inline NotQuery:: ~NotQuery(){ delete _op; } inline OrQuery:: ~OrQuery(){ delete _lop; delete _rop; } inline AndQuery:: |
~AndQuery(){ delete _lop; delete _rop; }
Отметим два аспекта:
· мы не предоставляем явного деструктора NameQuery, потому что никаких специальных действий по очистке его объекта предпринимать не нужно. Деструкторы базового класса и класса string для члена _name
вызываются автоматически;
· в деструкторах производных классов оператор delete
применяется к указателю типа Query*. Чтобы вызвать не деструктор Query, а деструктор класса того объекта, который фактически адресуется этим указателем, мы должны объявить деструктор базового Query
виртуальным. (Более подробно о виртуальных функциях вообще и о виртуальных деструкторах в частности мы поговорим в следующем разделе.)
В нашей реализации неявно подразумевалось, что память для операндов, указатели на которые имеются в объектах классов NotQuery, OrQuery и AndQuery, выделена из хипа. Именно поэтому в деструкторах мы применяли к этим указателям оператор delete. Но язык не позволяет обеспечить истинность такого предположения, так как в нем нет различий между адресами в хипе и вне его. С этой точки зрения наша реализация не застрахована от ошибок.
В разделе 17. 7 мы инкапсулируем выделение памяти и конструирование объектов иерархии Query в управляющий класс UserQuery. Это гарантирует выполнение нашего предположения. На уровне программы в целом следует перегрузить операторы new и delete для классов иерархии. Например, можно поступить следующим образом. Оператор new устанавливает в объекте флажок, говорящий, что память для него выделена из хипа. Перегруженный оператор delete
проверяет этот флажок: если он есть, то память освобождается с помощью стандартного оператора delete.
Упражнение 17.7
Идентифицируйте конструкторы и деструкторы базового и производных классов для той иерархии, которую вы выбрали в упражнении 17.2 (раздел 17.1).
Упражнение 17.8
Измените реализацию класса OrQuery
так, чтобы он был производным от BinaryQuery.
Упражнение 17.9
Найдите ошибку в следующем определении класса:
class Object { public: virtual ~Object(); virtual string isA(); protected: string _isA; private: Object( string s ) : _isA( s ) {} |
Упражнение 17.10
Дано определение базового класса:
class ConcreteBase { public: explicit ConcreteBase( int ); virtual ostream& print( ostream& ); virtual ~Base(); static int object_count(); protected: int _id; static int _object_count; |
Что неправильно в следующих фрагментах:
(a) class C1 : public ConcreteBase { public: C1( int val ) : _id( _object_count++ ) {} // ... |
(b) class C2 : public C1 { public: C2( int val ) : ConcreteBase( val ), C1( val ) {} // ... |
(c) class C3 : public C2 { public: C3( int val ) : C2( val ), _object_count( val ) {} // ... |
(d) class C4 : public ConcreteBase { public: C4( int val ) : ConcreteBase ( _id+val ){} // ... |
Упражнение 17.11
В первоначальном определении языка C++ порядок следования инициализаторов в списке инициализации членов определял порядок вызова конструкторов. Принцип, который действует сейчас, был принят в 1986 году. Как вы думаете, почему была изменена исходная спецификация?