Основы обработки исключительных ситуаций
Обработка исключительных ситуаций лишена недостатков вышеназванных методов реагирования на ошибки. Этот механизм позволяет использовать для представления информации об ошибке объект любого типа. Поэтому можно, например, создать иерархию классов, которая будет предназначена для обработки аварийных событий. Это упростит, структурирует и сделает более понятной программу.
Рассмотрим пример обработки исключительных ситуаций. Функция div() возвращает частное от деления чисел, принимаемых в качестве аргументов. Если делитель равен нулю, то генерируется исключительная ситуация.
#include<iostream.h>
double div(double dividend, double divisor)
{ if(divisor==0) throw 1;
return dividend/divisor;
}
void main()
{ double result;
try {
result=div(77.,0.);
cout<<"Answer is "<<result<<endl;
}
catch(int){
cout<<"Division by zero"<<endl;
}
}
Результат выполнения программы:
Division by zero
В данном примере необходимо выделить три ключевых элемента. Во-первых, вызов функции div() заключен внутрь блока, который начинается с ключевого слова try. Этот блок указывает, что внутри него могут происходить исключительные ситуации. По этой причине код, заключенный внутри блока try, иногда называют охранным.
Далее за блоком try следует блок catch, называемый обычно обработчиком исключительной ситуации. Если возникает исключительная ситуация, выполнение программы переходит к этому catch-блоку. Хотя в этом примере имеется один-единственный обработчик, их в программах может присутствовать множество и они способны обрабатывать множество различных типов исключительных ситуаций.
Еще одним элементом процесса обработки исключительных ситуаций является оператор throw (в данном случае он находится внутри функции div()). Оператор throw сигнализирует об исключительном событии и генерирует объект исключительной ситуации, который перехватывается обработчиком catch. Этот процесс называется вызовом исключительной ситуации. В рассматриваемом примере исключительная ситуация имеет форму обычного целого числа, однако программы могут генерировать практически любой тип исключительной ситуации.
Если в инструкции if(divisor==0) throw 1 значение 1 заменить на 1. , то при выполнении будет выдана ошибка об отсутствии соответствующего обработчика catch (так как возбуждается исключительная ситуация типа double).
Одним из главных достоинств использования механизма обработки исключительных ситуаций является обеспечение развертывания стека. Развертывание стека – это процесс вызова деструкторов локальных объектов, когда исключительные ситуации выводят их из области видимости.
Сказанное рассмотрим на примере функции add() класса add_class, выполняющей сложение компонентов-данных объектов add_class и возвращающей суммарный объект. В случае, если сумма превышает максимальное значение для типа unsigned short, генерируется исключительная ситуация.
#include<limits.h>
#include<iostream.h>
class add_class
{ private:
unsigned short num;
public:
add_class(unsigned short a)
{ num=a;
cout<<"Constructor "<<num<<endl;
}
~add_class() { cout<<"Destructor of add_class "<<num<<endl; }
void show_num() { cout<<" "<<num<<" "; }
void input_num(unsigned short a) { num=a; }
unsigned short output_num(){return num;}
};
add_class add(add_class a,add_class b) // суммирование компонент num
{ add_class sum(0); // объектов a и b
unsigned long s=(unsigned long)a.output_num()+
(unsigned long)b.output_num();
if(s>USHRT_MAX) throw 1; // генерация исключения типа int
sum.input_num((unsigned short) s);
return sum;
}
void main()
{ add_class a(USHRT_MAX),b(1),s(0);
try{ // охранный блок
s=add(a,b);
cout<<"Result";
s.show_num();
cout<<endl;
}
catch(int){ // обработчик исключеения типа int
cout<<"Overflow error"<<endl;
}
}
Результат выполнения программы:
Constructor 65535
Constructor 1
Constructor 0
Constructor 0
Destructor of add_class 0
Destructor of add_class 65535
Destructor of add_class 1
Overflow error
Destructor of add_class 0
Destructor of add_class 1
Destructor of add_class 65535
Сначала вызываются конструкторы объектов a, b и s, далее происходит передача параметров по значению в функцию. В этом случае происходит вызов конструктора копий (сначала для объекта b затем для a), созданного по умолчанию, именно поэтому вызовов деструктора больше, чем конструктора, затем, используя конструктор, создается объект sum. После этого генерируется исключение и срабатывает механизм развертывания стека, то есть вызываются деструкторы локальных объектов sum, a и b. И, наконец, вызываются деструкторы s, b и a.
Рассмотрим более подробно элементы try, catch и throw механизма обработки исключений.
Блок try. Синтаксис блока:
try{
Охранный код
}
Список обработчиков
Необходимо помнить, что после ключевого слова try всегда должен следовать составной оператор, т.е. после try всегда следует {…}. Блоки try не имеют однострочной формы, как, например, операторы if, while, for.
Еще один важный момент заключается в том, что после блока try должен следовать, по крайней мере, хотя бы один обработчик. Недопустимо нахождение между блоками try и catch какого-либо кода. Например:
int i;
try{
throw исключение;
}
i=0; // 'try' block starting on line ' номер ' has no catch handlers
catch(тип аргумент){
блок обработки исключения
}
В блоке try можно размещать любой код, вызовы локальных функций, функции-компоненты объектов, и любой код любой степени вложенности может генерировать исключительные ситуации. Блоки try сами могут быть вложенными.
Обработчики исключительных ситуаций catch.Обработчики исключительных ситуаций являются важнейшей частью всего механизма обработки исключений, так как именно они определяют поведение программы после генерации и перехвата исключительной ситуации. Синтаксис блока catch имеет следующий вид:
catch(тип 1 <аргумент>)
{
Тело обработчика
}
catch(тип 2 <аргумент>))
{
Тело обработчика
}
.
.
.
catch(тип N <аргумент>))
{
Тело обработчика
}
Таким образом, так же как и в случае блока try, после ключевого слова catch должен следовать составной оператор, заключенный в фигурные скобки. В аргументах обработчика можно указать только тип исключительной ситуации, необязательно объявлять имя объекта, если этого не требуется.
У каждого блока try может быть множество обработчиков, каждый из которых должен иметь свой уникальный тип исключительной ситуации. Неправильной будет следующая запись:
typedef int type_int;
try{ . . . }
catch(type_int error1){
. . .
}
catch(int error2){
. . .
}
Так, в этом случае type_int и int - это одно и то же.
Однако следующий пример верен.
Class cls
{ public:
int i;
};
try{
. . .
}
catch(cls i1){
...
}
catch(int i2){
}
В этом случае cls – это отдельный тип исключительной ситуации. Существует также абсолютный обработчик, который совместим с любым типом исключительной ситуации. Для написания такого обработчика надо вместо аргументов написать многоточие (эллипсис).
catch (…){
Блок обработки исключения
}
Использование абсолютного обработчика исключительных ситуаций рассмотрим на примере программы, в которой происходит генерация исключительной ситуации типа char *, но обработчик такого типа отсутствует. В этом случае управление передается абсолютному обработчику.
#include <iostream.h>
void int_exception(int i)
{ if(i>100) throw 1; // генерация исключения типа int
}
void string_exception()
{ throw "Error"; // генерация исключения типа char *
}
void main()
{ try{ // в блоке возможна обработка одного из 2 исключений
int_exception(99); // возможно исключение типа int
string_exception(); // возможно исключение типа char *
}
catch(int){
cout<<"Обработчик для типа int"<<endl;
}
catch(...){
cout<<"Абсолютный обработчик "<<endl;
}
}
Результат выполнения программы:
Абсолютный обработчик
Так как абсолютный обработчик перехватывает исключительные ситуации всех типов, то он должен стоять в списке обработчиков последним. Нарушение этого правила вызовет ошибку при компиляции программы.
Для того чтобы эффективно использовать механизм обработки исключительных ситуаций, необходимо грамотно построить списки обработчиков, а для этого, в свою очередь, нужно четко знать следующие правила, по которым осуществляется поиск соответствующего обработчика:
- исключительная ситуация обрабатывается первым найденным обработчиком, т. е. если есть несколько обработчиков, способных обработать данный тип исключительной ситуации, то она будет обработана первым стоящим в списке обработчиком;
- абсолютный обработчик может обработать любую исключительную ситуацию;
- исключительная ситуация может быть обработана обработчиком соответствующего типа либо обработчиком ссылки на этот тип;
- исключительная ситуация может быть обработана обработчиком базового для нее класса. Например, если класс В является производным от класса А, то обработчик класса А может обработать исключительную ситуацию класса В;
- исключительная ситуация может быть обработана обработчиком, принимающим указатель, если тип исключительной ситуации может быть приведен к типу обработчика, путем использования стандартных правил преобразования типов указателей.
Если при возникновении исключительной ситуации подходящего обработчика нет среди обработчиков данного уровня вложенности блоков try, то обработчик ищется на следующем охватывающем уровне. Если обработчик не найден вплоть до самого верхнего уровня, то программа аварийно завершается.
Следствием из правил 3 и 4 является еще одно утверждение: исключительная ситуация может быть направлена обработчику, который может принимать ссылку на объект базового для данной исключительной ситуации класса. Это значит, что если, например, класс В – производный от класса А, то обработчик ссылки на объект класса А может обрабатывать исключительную ситуацию класса В (или ссылку на объект класса В).
Рассмотрим особенности выбора соответствующего обработчика на следующем примере. Пусть имеется класс С, являющийся производным от классов А и В; показано, какими обработчиками может быть перехвачена исключительная ситуация типа С и типа указателя на С.
#include<iostream.h>
class A{};
class B{};
class C : public A, public B {};
void f(int i)
{ if(i) throw C(); // возбуждение исключительной ситуации
// типа объект класса С
else throw new C; // возбуждение исключительной ситуации
// типа указатель на объект класса С
}
void main()
{ int i;
try{
cin>>i;
f(i);
}
catch(A) {
cout<<"A handler";
}
catch(B&) {
cout<<"B& handler";
}
catch(C) {
cout<<"C handler";
}
catch(C*) {
cout<<"C* handler";
}
catch(A*) {
cout<<"A* handler";
}
catch(void*) {
cout<<"void* handler";
}
}
В данном примере исключительная ситуация класса С может быть направлена любому из обработчиков A, B& или C, поэтому выбирается обработчик, стоящий первым в списке. Аналогично для исключительной ситуации, имеющей тип указателя на объект класса С, выбирается первый подходящий обработчик A* или C*. Эта ситуация также может быть обработана обработчиками void*. Так как к типу void* может быть приведен любой указатель, то обработчик этого типа будет перехватывать любые исключительные ситуации типа указателя. Рассмотрим еще один пример:
#include <iostream.h>
class base {};
class derived : public base{};
void fun()
{ derived obj;
base &a = obj;
throw a;
}
main()
{ try{ fun();
}
catch(derived){ cout<<”derived”<<endl;
}
catch(base) { cout<<”base”<<endl;
}
}
Результат выполнения программы:
base
В примере генерируется исключение, имеющее тип base, хотя ссылка a имеет тип base, хотя является ссылкой на класс derived. Так происходит потому, что статический тип a равен base, а не derived.
В случае, когда генерация исключения выполняется по значению объекта или по ссылке на объект то происходит копирование значения объекта (локального) во временный (в catch).
Генерация исключительных ситуаций throw.Исключительные ситуации передаются обработчикам с помощью ключевого слова throw. Как ранее отмечалось, обеспечивается вызов деструкторов локальных объектов при выходе из области видимости, то есть развертывание стека. Однако развертывание стека не обеспечивает уничтожение объектов, созданных динамически. Таким образом, перед генерацией исключительной ситуации необходимо явно освободить динамически выделенные блоки памяти.
Следует отметить также, что если исключительная ситуация генерируется по значению или по ссылке, то создается скрытая временная переменная, в которой хранится копия генерируемого объекта. Когда после throw указывается локальный объект, то к моменту вызова соответствующего обработчика этот объект будет уже вне области видимости и, естественно, прекратит существование. Обработчик же получит в качестве аргумента именно эту скрытую копию. Из этого следует, что если генерируется исключительная ситуация сложного класса, то возникает необходимость снабжения этого класса конструктором копий, который бы обеспечил корректное создание копии объекта.
Если же исключительная ситуация генерируется с использованием указателя, то копия объекта не создается. В этом случае могут возникнуть проблемы. Например, если генерируется указатель на локальный объект, к моменту вызова обработчика объект уже перестанет существовать и использование указателя в обработчике может привести к ошибкам.
Class A
{ int i;
public:
A():i(0){}
void ff(int i){this->i=i;}
};
void f()
{ A obj;
. . .
throw &obj;
}
main()
{ try{
f();
}
catch(A *a){ // код обработчика;
a->ff(1);
}
}
Эту проблему можно легко устранить, если поместить указатель на новый динамический объект:
void f()
{ A obj;
. . .
throw new A;
}
При этом необходимо решить вопрос о необходимости удаления или нет динамически созданного объекта исключения (например, в блоке catch). В противном возможны утечки памяти.