Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология
Образование Политология Производство Психология Стандартизация Технологии


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



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

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

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

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

class PTR {

public:

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

~PTR { delete[] ptr; }

private:

int *ptr;

};

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

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

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

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

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

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 ) { }

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

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 мы говорили о том, что между типом возбужденного исключения и типом, заданным в спецификации исключений, не допускаются никакие преобразования. Однако если там указан тип класса, то функция может возбуждать исключения в виде объекта класса, открыто наследующего заданному. Аналогично, если имеется указатель на класс, то функции разрешено возбуждать исключения в виде указателя на объект класса, открыто наследующего заданному. Например:

class stackExcp: public Excp { };

class popObEmpty: public stackExcp { };

 

class pushOnFull: public stackExcp { };

 

void stackManip() throw( stackExcp )

{

//...

}

Спецификация исключений указывает, что stackManip() может возбуждать исключения не только типа stackExcp, но также popOnEmpty и pushOnFull. Напомним, что класс, открыто наследующий базовому, представляет собой пример отношения ЯВЛЯЕТСЯ, т.е. является частным случае более общего базового класса. Поскольку popOnEmpty и pushOnFull – частные случаи stackExcp, они не нарушают спецификации исключений функции stackManip().


Поделиться:



Последнее изменение этой страницы: 2019-04-09; Просмотров: 296; Нарушение авторского права страницы


lektsia.com 2007 - 2024 год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! (0.019 с.)
Главная | Случайная страница | Обратная связь