На этом шаге мы рассмотрим правила записи и использования обработки исключений.
Если проанализировать приведенные на предыдущем шаге программы, то окажется, что в большинстве из них механизм генерации и обработки исключений можно имитировать "старыми" средствами. В этом случае, определив некоторое состояние программы как особое, ее автор предусматривает анализ результатов выполнения оператора, в котором это состояние может быть достигнуто, либо проверяет исходные данные, использование которых в операторе может привести к возникновению указанного состояния. Далее выявленное состояние обрабатывается. Чаще всего при обработке выводится сообщение о достигнутом состоянии и либо завершается выполнение программы, либо выполняются заранее предусмотренные коррекции. Описанная схема имитации механизма обработки особых ситуаций неудобна в тех случаях, когда существует "временной разрыв" между написанием частей программы, где возникает (выявляется) ситуация и где она обрабатывается. Например, это типично при разработке библиотечных функций, когда реакции на необычные состояния в функциях должен определять не автор функций, а программист, применяющий их в своих программах. При возникновении аварийной (особой) ситуации в библиотечной (или просто заранее написанной ) функции желательно передать управление и информацию о характере ситуации вызывающей программе, где программист может по своему предусмотреть обработку возникшего состояния. Именно такую возможность в языке С++ обеспечивает механизм обработки исключений.
Итак, исключения введены в язык в основном для того, чтобы дать возможность программисту динамически (Run-Time) проводить обработку возникающих ситуаций, с которыми не может справиться исполняемая функция. Основная идея состоит в том, что функция, сталкивающаяся с определенной проблемой, формирует исключение, надеясь на то, что вызывающая ее прямо или косвенно функция сможет обработать возникшую проблему.
Механизм исключений позволяет переносить анализ и обработку ситуации из точки ее возникновения (Throw Point), в другое место программы, специально предназначенное для ее обработки. Кроме того, из точки возникновения ситуации в место ее обработки (в список обработчиков исключений) может быть передано любое количество необходимой информации, например, сведения о том, какие данные и действия привели к возникновению такой ситуации.
Таким образом механизм обработки исключений позволяет регистрировать исключительные ситуации и определять процедуры их обработки, которые будут выполняться перед дальнейшим продолжением или завершением программы.
Необходимо лишь помнить, что механизм исключений предназначен только для синхронных событий, то есть таких, которые порождаются в результате работы самой программы (к примеру, попытка прерывания программы нажатием Ctrl+C во время ее выполнения не является синхронным событием).
Как уже объяснялось, применение механизма обработки исключений предусматривает выделение в тексте программы двух размещенных последовательно обязательных участков:
Контролируемый блок идентифицируется ключевым словом try. Каждый обработчик исключений начинается со служебного слова catch. Общая схема размещения указанных блоков:
try { операторы_контролируемого_блока } catch (спецификация исключения) { операторы_обработчика_исключений } . . . . catch (спецификация_исключения) { операторы_обработчика_исключений }
В приведенных на предыдущем шаге программах использовалось по одному обработчику исключений. Это объясняется "однотипностью" формируемых исключений (только типа const char * или только типа DATA). В общем случае в контролируемом блоке могут формироваться исключения разных типов и обработчиков может быть несколько. Размещаются они подряд, последовательно друг за другом и каждый обработчик "настроен" на исключение конкретного типа. Спецификация исключения, размещенная в скобках после служебного слова catch, имеет три формы:
catch (тип имя) { ... } catch (тип) { ... } catch (...) { ... }
Первый вариант подобен спецификации формального параметра в определении функции. Имя этого параметра используется в операторах обработки исключения. С его помощью к ним передается информация из обрабатываемого исключения.
Второй вариант не предполагает использования значения исключения. Для обработчика важен только его тип и факт его получения.
В третьем случае (многоточие) обработчик реагирует на любое исключение независимо от его типа. Так как сравнение "посланного" исключения со спецификациями обработчиков выполняется последовательно, то обработчик с многоточием в качестве спецификации следует помещать только в конце списка обработчиков. В противном случае все возникающие исключения "перехватит" обработчик с многоточием в качестве спецификации. В случае, если описать его не последним обработчиком, компилятор Borland C++, к примеру, выдаст сообщение об ошибке:
The ' . . . ' handler must be last in function ...() (Обработчик '...' должен идти последним в функции ...()).
Продемонстрируем некоторые из перечисленных особенностей обработки исключений еще одной программой:
//EXC3_1.СРР - исключения без передачи информации. #include <iostream.h> class ZeroDivide {}; // Класс без компонентов. class Overflow {}; // Класс без компонентов. // Определение функции с генерацией исключений: float div (float n, float d) { if (d == 0.0) throw ZeroDivide(); // Вызов конструктора. double b = n/d; if (b > 1e+30) throw Overflow(); // Вызов конструктора. return b; } float x = 1e-20, y = 5.5, z = 1e+20, w = 0.0; // Вызывающая функция с выявлением и обработкой исключений: void RR(void) { // Контролируемый блок: try { y = div(4.4,w); z = div(z,x); } // Последовательность обработчиков исключений: catch (Overflow) { cerr <<"\nПереполнение "; z = 1e30; } catch (ZeroDivide) { cerr << "\nДеление на нуль "; w = 1.0; } } void main() { // Вызов функции div() с нулевым делителем w: RR() ; // Вызов функции div() с арифметическим переполнением: RR() ; cout << "\nРезультат: y = " << y; cout << "\nРезультат: z = " << z; }
Результат выполнения программы:
Деление на нуль Переполнение Результат: у = 4.4 Результат: z = 1e+30
В программе в качестве типов для исключений используются классы без явно определенных компонентов. Конструктор ZeroDivide() вызывается и формирует безымянный объект (исключение) при попытке деления на нуль. Конструктор Overflow() используется для создания исключений, когда значение результата деления превысит величину 1е+30. Исключения указанных типов не передают содержательной информации. Эта информация не предусмотрена и в соответствующих обработчиках исключений. При первом обращении к функции RR() значение глобальной переменной y не изменяется, так как управление передается обработчику исключений
catch (ZeroDivide)
При его выполнении выводится сообщение, и делитель w (глобальная переменная) устанавливается равным 1.0. После обработчика исключения завершается функция RR(), и вновь в основной программе вызывается функция RR(), но уже с измененным значением w. При этом обращение div(4.4,w) обрабатывается безошибочно, а вызов div(z,x) приводит к формированию исключения типа Overflow. Его обработка в RR() предусматривает печать предупреждающего сообщения и изменение значения глобальной переменной z. Обработчик catch (ZeroDivide) в этом случае пропускается. После выхода из RR() основная программа выполняется обычным образом и печатаются значения результатов "деления", осуществленного с помощью функции div().
На следующем шаге мы закончим изложение вопросов, связанных с синтаксисом и семантикой обработки исключений.