Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих (Стенли Липпман) 3-е хххх.pdf
Скачиваний:
85
Добавлен:
30.05.2015
Размер:
5.92 Mб
Скачать

С++ для начинающих

998

class pushOnFull { public:

pushOnFull( int i ) : _value( i ) { } int value() { return _value; }

~pushOnFull(); // вновь объявленный деструктор private:

int _value;

};

catch ( pushOnFull eObj ) {

cerr << "попытка поместить значение " << eObj.value() << " в полный стек\n";

Когда они вызываются? Чтобы ответить на этот вопрос, рассмотрим catch-обработчик:

}

Поскольку в объявлении исключения eObj объявлен как локальный для catch- обработчика объект, а в классе pushOnFull есть деструктор, то eObj уничтожается при выходе из обработчика. Когда же вызывается деструктор для объекта-исключения, созданного в момент возбуждения исключения, – при входе в catch-обработчик или при выходе из него? Однако уничтожать исключение в любой из этих точек может быть слишком рано. Можете сказать, почему? Если catch-обработчик возбуждает исключение повторно, передавая его выше по цепочке вызовов, то уничтожать объект-исключение нельзя до момента выхода из последнего catch-обработчика.

19.2.4. Объекты-исключения и виртуальные функции

Если сгенерированный объект-исключение имеет тип производного класса, а обрабатывается catch-обработчиком для базового, то этот обработчик не может использовать особенности производного класса. Например, к функции-члену value(),

catch ( const Excp &eObj ) {

// ошибка: в классе Excp нет функции-члена value() cerr << "попытка поместить значение " << eObj.value()

<< " в полный стек\n";

которая объявлена в классе pushOnFull, нельзя обращаться в catch-обработчике Excp:

}

Но мы можем перепроектировать иерархию классов исключений и определить виртуальные функции, которые можно вызывать из catch-обработчика для базового класса Excp с целью получения доступа к функциям-членам более специализированного производного:

С++ для начинающих

999

// новые определения классов, включающие виртуальные функции class Excp {

public:

virtual void print( string msg ) { cerr << "Произошло исключение"

<< endl;

}

class stackExcp : public Excp { }; class pushOnFull : public stackExcp { public:

virtual void print() {

cerr << "попытка поместить значение " << _value << " в полный стек\n";

}

// ...

int main() { try {

//iStack::push() возбуждает исключение pushOnFull

}catch ( Excp eObj ) {

eObj.print();

//

хотим вызвать

виртуальную функцию,

 

//

но вызывается

экземпляр из базового класса

}

};

};

Функцию print() теперь можно использовать в catch-обработчике следующим образом:

}

Хотя возбужденное исключение имеет тип pushOnFull, а функция print() виртуальна, инструкция eObj.print() печатает такую строку:

Произошло исключение

Вызываемая print() является членом базового класса Excp, а не замещает ее в производном. Но почему?

Вспомните, что объявление исключения в catch-обработчике ведет себя почти так же, так объявление параметра. Когда управление попадает в catch-обработчик, то, поскольку в нем объявлен объект, а не ссылка, eObj инициализируется копией подобъекта Excp базового класса объекта исключения. Поэтому eObj это объект типа Excp, а не pushOnFull. Чтобы вызвать виртуальные функции из производных классов, в объявлении исключения должен быть указатель или ссылка:

С++ для начинающих

1000

int main() { try {

//iStack::push() возбуждает исключение pushOnFull

}catch ( const Excp &eObj ) {

eObj.print(); // вызывается виртуальная функция

// pushOnFull::print()

}

}

Объявление исключения в этом примере тоже относится к базовому классу Excp, но так как eObj ссылка и при этом именует объект-исключение типа pushOnFull, то для нее можно вызывать виртуальные функции, определенные в классе pushOnFull. Когда catch-обработчик обращается к виртуальной функции print(), вызывается функция из производного класса, и программа печатает следующую строку:

попытка поместить значение 879 в полный стек

Таким образом, ссылка в объявлении исключения позволяет вызывать виртуальные функции, ассоциированные с классом объекта-исключения.

19.2.5. Раскрутка стека и вызов деструкторов

Когда возбуждается исключение, поиск его catch-обработчика раскрутка стека начинается с функции, возбудившей исключение, и продолжается вверх по цепочке вложенных вызовов (см. раздел 11.3).

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

Существует прием, позволяющий решить эту проблему. Всякий раз, когда во время поиска обработчика происходит выход из составной инструкции или блока, где определен некоторый локальный объект, для этого объекта автоматически вызывается деструктор. (Локальные объекты рассматривались в разделе 8.1.)

Например, следующий класс инкапсулирует выделение памяти для массива целых в

class PTR { public:

PTR() { ptr = new int[ chunk ]; } ~PTR { delete[] ptr; }

private: int *ptr;

конструкторе и ее освобождение в деструкторе:

};

Локальный объект такого типа создается в функции manip() перед вызовом mathFunc():

С++ для начинающих

1001

void manip( int parm ) {

PTR localPtr;

// ...

mathFunc( parm ); // возбуждает исключение divideByZero

// ...

}

Если mathFunc() возбуждает исключение типа divideByZero, то начинается раскрутка стека. В процессе поиска подходящего catch-обработчика проверяется и функция manip(). Поскольку вызов mathFunc() не заключен в try-блок, то manip() нужного обработчика не содержит. Поэтому стек раскручивается дальше по цепочке вызовов. Но

перед выходом из manip() с необработанным исключением процесс раскрутки уничтожает все объекты типа классов, которые локальны в ней и были созданы до вызова mathFunc(). Таким образом, локальный объект localPtr уничтожается до того, как поиск пойдет дальше, а следовательно, память, на которую он указывает, будет освобождена и утечки не произойдет.

Поэтому говорят, что процесс обработки исключений в C++ поддерживает технику программирования, основной принцип которой можно сформулировать так: “захват ресурса это инициализация; освобождение ресурса это уничтожение”. Если ресурс реализован в виде класса и, значит, действия по его захвату сосредоточены в конструкторе, а действия по освобождению в деструкторе (как, например, в классе PTR выше), то локальный для функции объект такого класса автоматически уничтожается при выходе из функции в результате необработанного исключения. Действия, которые должны быть выполнены для освобождения ресурса, не будут пропущены при раскрутке стека, если они инкапсулированы в деструкторы, вызываемые для локальных объектов.

Класс auto_ptr, определенный в стандартной библиотеке (см. раздел 8.4), ведет себя почти так же, как наш класс PTR. Это средство для инкапсуляции выделения памяти в конструкторе и ее освобождения в деструкторе. Если для выделения одиночного объекта из хипа используется auto_ptr, то гарантируется, что при выходе из составной инструкции или функции из-за необработанного исключения память будет освобождена.

19.2.6. Спецификации исключений

С помощью спецификации исключений (см. раздел 11.4) в объявлении функции указывается множество исключений, которые она может возбуждать прямо или косвенно. Спецификация позволяет гарантировать, что функция не возбудит не перечисленные в ней исключения.

Такую спецификацию разрешается задавать для функций-членов класса так же, как и для обычных функций; она должна следовать за списком параметров функции-члена. Например, в определении класса bad_alloc из стандартной библиотеки C++ функции- члены имеют пустую спецификацию исключений throw(), т.е. гарантированно не возбуждают никаких исключений:

С++ для начинающих

1002

class bad_alloc : public exception { // ...

public:

bad_alloc() throw();

bad_alloc( const bad_alloc & ) throw();

bad_alloc & operator=( const bad_alloc & ) throw(); virtual ~bad_alloc() throw();

virtual const char* what() const throw();

};

Отметим, что если функция-член объявлена с модификатором const или volatile, как, скажем, what() в примере выше, то спецификация исключений должна идти после него.

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

#include <stdexcept>

// <stdexcept> определяет класс overflow_error

class transport {

//...

public:

double cost( double, double ) throw ( overflow_error );

//...

};

//ошибка: спецификация исключений отличается от той, что задана

//в объявлении в списке членов класса

объявлении функции должны совпадать:

double transport::cost( double rate, double distance ) { }

Виртуальная функция в базовом классе может иметь спецификацию исключений, отличающуюся от той, что задана для замещающей функции-члена в производном.

Однако в производном классе эта спецификация для виртуальной функции должна накладывать не меньше ограничений, чем в базовом:

С++ для начинающих

1003

class Base { public:

virtual double f1( double ) throw(); virtual int f2( int ) throw( int ); virtual string f3() throw( int, string ); // ...

}

class Derived : public Base { public:

//ошибка: спецификация исключений накладывает меньше ограничений,

//чем на Base::f1()

double f1( double ) throw( string );

//правильно: та же спецификация исключений, что и для Base::f2() int f2( int ) throw( int );

//правильно: спецификация исключений f3() накладывает больше

ограничений

string f3( ) throw( int ); // ...

};

Почему спецификация исключений в производном классе должна накладывать не меньше ограничений, чем в базовом? В этом случае мы можем быть уверены, что вызов

виртуальной функции из производного класса по указателю на тип базового не нарушит

// гарантируется, что исключения возбуждены не будут void compute( Base *pb ) throw()

{

try {

pb->f3( ); // может возбудить исключение типа int или string

}

// обработка исключений, возбужденных в Base::f3() catch ( const string & ) { }

catch ( int ) { }

спецификацию исключений функции-члена базового класса:

}

Объявление f3() в классе Base гарантирует, что эта функция возбуждает лишь исключения типа int или string. Следовательно, функция compute() включает catch- обработчики только для них. Поскольку спецификация исключений f3() в производном классе Derived накладывает больше ограничений, чем в базовом Base, то при

программировании в согласии с интерфейсом класса Base наши ожидания не будут обмануты.

В главе 11 мы говорили о том, что между типом возбужденного исключения и типом, заданным в спецификации исключений, не допускаются никакие преобразования. Однако если там указан тип класса, то функция может возбуждать исключения в виде объекта класса, открыто наследующего заданному. Аналогично, если имеется указатель на класс, то функции разрешено возбуждать исключения в виде указателя на объект класса, открыто наследующего заданному. Например: