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

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

993

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;

}

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

};

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

994

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

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

995

Зачем нужно генерировать объект-исключение (шаг 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 перехватывается обработчиками исключений типа stackExcp или Excp.

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

996

int main() { try {

// ...

}

catch ( Excp ) {

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

}

catch ( pushOnFull ) {

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

}

Здесь порядок 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-обработчик совсем не

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

997

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

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

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

throw;

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

void calculate( int parm ) { try {

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

}

catch ( mathExcp mExcp ) {

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

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

throw;

}

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

}

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

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

Предположим, что классы в нашей иерархии исключений имеют деструкторы: