17.4.5. Деструкторы

17.4.5. Деструкторы

Когда заканчивается время жизни объекта производного класса, автоматически вызываются деструкторы производного и базового классов (если они определены), а также деструкторы всех объектов-членов. Например, если имеется объект класса 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 году. Как вы думаете, почему была изменена исходная спецификация?