Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих.pdf
Скачиваний:
183
Добавлен:
01.05.2014
Размер:
3.97 Mб
Скачать

(a)A *pa = new D;

cout << typeid( pa ).name() << endl;

(b)X *px = new D;

cout << typeid( *px ).name() << endl;

(c)C obj;

A& ra = cobj;

cout << typeid( &ra ).name() << endl;

(d)X *px = new D; A& ra = *px;

cout << typeid( ra ).name() << endl;

19.2. Исключения и наследование

Обработка исключений – это стандартное языковое средство для реакции на аномальное поведение программы во время выполнения. C++ поддерживает единообразный синтаксис и стиль обработки исключений, а также способы тонкой настройки этого механизма в специальных ситуациях. Основы его поддержки в языке C++ описаны в главе 11, где показано, как программа может возбудить исключение, передать управление его обработчику (если таковой существует) и как обработчики исключений ассоциируются с try-блоками.

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

19.2.1. Исключения, определенные как иерархии классов

В главе 11 мы использовали два типа класса для описания исключений, возбуждаемых

class popOnEmpty

{ ... };

функциями-членами нашего класса iStack: class pushOnFull { ... };

В реальных программах на C++ типы классов, представляющих исключения, чаще всего организуются в группы, или иерархии. Как могла бы выглядеть вся иерархия для этих классов?

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

class Excp { ... };

class popOnEmpty : public Excp { ... };

class pushOnFull : public Excp { ... };

Одной из операцией, которые предоставляет базовый класс, является вывод сообщения

class Excp { public:

// напечатать сообщение об ошибке

static void print( string msg )

{

cerr << msg << endl;

}

об ошибке. Эта возможность используется обоими классами, стоящими ниже в иерархии:

};

Иерархию классов исключений разрешается развивать и дальше. От Excp можно произвести другие классы для более точного описания исключений, обнаруживаемых

class Excp { ... };

class stackExcp : public Excp { ... }; class popOnEmpty : public stackExcp

{ ... };

программой:

class mathExcp : public Excp ( ... }; class zeroOp : public mathExcp

{ ... };

class pushOnFull : public stackExcp { ... }; class divideByZero : public mathExcp { ... };

Последующие уточнения позволяют более детально идентифицировать аномальные ситуации в работе программы. Дополнительные классы исключений организуются как слои. По мере углубления иерархии каждый новый слой описывает все более специфичные исключения. Например, первый, самый общий слой в приведенной выше иерархии представлен классом Excp. Второй специализирует Excp, выделяя из него два подкласса: stackExcp (для исключений при работе с нашим iStack) и mathExcp (для исключений, возбуждаемых функциями из математической библиотеки). Третий, самый специализированный слой данной иерархии уточняет классы исключений: popOnEmpty и pushOnFull определяют два вида исключений работы со стеком, а ZeroOp и divideByZero – два вида исключений математических операций.

В последующих разделах мы рассмотрим, как возбуждаются и обрабатываются исключения, представленные классами в нашей иерархии.

19.2.2. Возбуждение исключения типа класса

Теперь, познакомившись с классами, посмотрим, что происходит, когда функция-член

void iStack::push( int value )

{

if ( full() )

// value сохраняется в объектеисключении

throw pushOnFull( value ); // ...

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

}

Выполнение инструкции throw инициирует несколько последовательных действий:

1.Инструкция throw создает временный объект типа класса pushOnFull, вызывая его конструктор.

2.С помощью копирующего конструктора генерируется объект-исключение типа pushOnFull – копия временного объекта, полученного на шаге 1. Затем он

передается обработчику исключения.

3.Временный объект, созданный на шаге 1, уничтожается до начала поиска обработчика.

Зачем нужно генерировать объект-исключение (шаг 2)? Инструкция

throw pushOnFull( value );

создает временный объект, который уничтожается в конце работы throw. Но исключение должно существовать до тех пор, пока не будет найден его обработчик, а он может находиться намного выше в цепочке вызовов. Поэтому необходимо скопировать временный объект в некоторую область памяти (объект-исключение), которая гарантированно существует, пока исключение не будет обработано. Иногда компилятор создает объект-исключение сразу, минуя шаг 1. Однако стандарт этого не требует, да и не всегда такое возможно.

Поскольку объект-исключение создается путем копирования значения, переданного инструкции throw, то возбужденное исключение всегда имеет такой же тип, как и это

void iStack::push( int value ) { if ( full() ) {

pushOnFull except( value ); stackExcp *pse = &except;

throw *pse; // объект-исключение имеет тип stackExcp

}

// ...

значение:

}

Выражение *pse имеет тип stackExcp. Тип созданного объекта-исключения – stackExcp, хотя pse ссылается на объект с фактическим типом pushOnFull.

Фактический тип объекта, на который ссылается throw, при создании объектаисключения не учитывается. Поэтому исключение не будет перехвачено catchобработчиком pushOnFull.

Действия, выполняемые инструкцией throw, налагают определенные ограничения на то, какие классы можно использовать для создания объектов-исключений. Оператор throw в функции-члене push() класса iStack вызовет ошибку компиляции, если:

в классе pushOnFull нет конструктора, принимающего аргумент типа int, или этот конструктор недоступен;

в классе pushOnFull есть копирующий конструктор или деструктор, но хотя бы один из них недоступен;

pushOnFull – это абстрактный базовый класс. Напомним, что программа не может создавать объекты абстрактных классов (см. раздел 17.1).

19.2.3. Обработка исключения типа класса

Если исключения организуются в иерархии, то исключение типа некоторого класса может быть перехвачено обработчиком, соответствующим любому его открытому базовому классу. Например, исключение типа pushOnFull перехватывается

int main() { try {

// ...

}

catch ( Excp ) {

// обрабатывает исключения popOnEmpty и pushOnFull

}

catch ( pushOnFull ) {

// обрабатывает исключение pushOnFull

обработчиками исключений типа stackExcp или Excp.

}

Здесь порядок catch-обработчиков желательно изменить. Напоминаем, что они просматриваются в порядке появления после try-блока. Как только будет найден обработчик, способный обработать данное исключение, поиск прекращается. В примере выше Excp может обработать исключения типа pushOnFull, а это значит, что специализированный обработчик таких исключений задействован не будет. Правильная

catch ( pushOnFull ) {

// обрабатывает исключение pushOnFull

}

catch ( Excp ) {

// обрабатывает другие исключения

последовательность такова:

}

catch-обработчик для производного класса должен идти первым. Тогда catch-обработчик для базового класса получит управление только в том случае, если более специализированного обработчика не нашлось.

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

catch ( pushOnFull eObj ) {

//используется функция-член value() класса pushOnFull

//см. раздел 11.3

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

}

catch ( Excp ) {

// используется функция-член print() базового класса Excp::print( "произошло исключение" );

остальных исключений, то они обрабатываются единообразно:

}

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

Объявление исключения в catch-обработчике (находящееся в скобках после слова catch) очень похоже на объявление параметра функции. В приведенном примере оно напоминает параметр, передаваемый по значению. Объект eObj инициализируется копией значения объекта-исключения точно так же, как передаваемый по значению формальный параметр функции инициализируется значением фактического аргумента. Как и в случае с параметрами функции, в объявлении исключения можно использовать ссылки. Тогда catch-обработчик имеет доступ непосредственно к объекту-исключению, созданному выражением throw, а не к его локальной копии. Чтобы избежать копирования больших объектов, параметры типа класса следует объявлять как ссылки; в объявлениях исключений тоже желательно делать исключения типа класса ссылками. В зависимости от того, что находится в таком объявлении (объект или ссылка), поведение обработчика различается (мы покажем эти различия в данном разделе).

В главе 11 были введены выражения повторного возбуждения исключения, которые используются в catch-обработчике для передачи исключения какому-то другому обработчику выше в цепочке вызовов. Такое выражение имеет вид

throw;

Как ведет себя эта инструкция, если она расположена в catch-обработчике исключений базового класса? Например, каким будет тип повторно возбужденного исключения, если mathFunc() возбуждает исключение типа divideByZero?

void calculate( int parm ) { try {

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

}

catch ( mathExcp mExcp ) {

//частично обрабатывает исключение

//и генерирует объект-исключение еще раз

throw;

}

}

Будет ли повторно возбужденное исключение иметь тип divideByZero –тот же, что и исключение, возбужденное функцией mathFunc()? Или тип mathExcp, который указан в объявлении исключения в catch-обработчике?

Напомним, что выражение throw повторно генерирует исходный объект-исключение. Так как исходный объект имеет тип divideByZero, то повторно возбужденное исключение будет такого же типа. В catch-обработчике объект mExcp инициализируется копией подобъекта объекта типа divideByZero, который соответствует его базовому классу MathExcp. Доступ к ней осуществляется только внутри catch-обработчика, она не является исходным объектом-исключением, который повторно генерируется.

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-обработчика.