Главная / Программирование / Лабораторная работа № 7. Обработка исключений

Лабораторная работа № 7. Обработка исключений

1. ЦЕЛЬ РАБОТЫ

    1. Изучение способов обнаружения ошибок времени выполнения с помощью исключений. 2. Изучение основных принципов обработки исключений в языке С++. 3. Знакомство со структурным управлением исключениями.

2. ОБРАБОТКА ИСКЛЮЧИТЕЛЬНЫХ СИТУАЦИЙ

2.1. Схема обработки исключений в С++

Понятие Исключения (Exception) введено для генерации в системе сообщения об ошибке. Например:

Struct Range_error

{

int i;

Range_error(int ii):i(ii) { }

};

Char to_char(int i)

{

if(i < numeric_limits<char>::min() ||

i > numeric_limits<char>::max()) throw Range_error(i);

return i;

}

Функция To_char() либо возвращает Char по числовому значению I, либо генерирует исключение Range_error. Основная идея состоит в том, функция, обнаружившая проблему, которую она не знает как решать, генерирует (Throw) исключение в надежде, что вызывающий (прямо или косвенно) модуль знает, что делать в этой ситуации. Функция, которая собирается обрабатывать ошибку, может объявить, что она будет Перехватывать (Catch) исключения данного типа. Например, для вызова To_char() и перехвата исключений, которые она может вызвать, можно написать:

Void f(int i)

{

try

{

char c = to_char(i);

// …

}

catch(Range_error)

{

cerr << “ проблема!”;

}

}

Конструкция

Catch(/*…*/) { /* … */ }

Называется Обработчиком исключений. Она может использоваться только сразу после блока, начинающегося с ключевого слова Try, или сразу после другого обработчика; Catch также является ключевым словом. В скобках находится объявление, которое используется аналогично объявлению аргументов функции. То есть, оно указывает тип объектов, которые могут быть перехвачены этим обработчиком, и (необязательно) присваивает имена перехватываемым объектам. Например:

Void g(int i)

{

try

{

char c = to_char(i);

// …

}

catch(Range_error x)

{

cerr << “ проблема!: to_char(” << x. i << “) ”;

}

}

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

Обработка исключений в С++ в основном является методом передачи управления специальному коду в вызывающем модуле.

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

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

Class Zero_divide { };

С другой стороны, обработчик скорее всего предпочел бы получать информацию о том, какого вида встретилась синтаксическая ошибка. Например:

Class Syntax_error

{

Public:

const char* p;

Syntax_error(const char* q) { p = q; }

};

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

Try

{

// …

}

Catch(Syntax_error se)

{

// обработка синтаксической ошибки

cerr << “ Синтаксическая ошибка: ” << se. p;

// …

}

Catch()

{

// обработка деления на ноль

cerr << “ Деление на ноль”;

}

Catch(…)

{

// обработка остальных ошибок

}

Также как и в функциях, многоточие «…» означает «любой аргумент», поэтому Catch(…) означает «перехват всех исключений». Такой неспециализированный блок Catch() обозначает способность обрабатывать не обслуженные предшествующими блоками исключения, поэтому он должен размещаться последним.

2.2. Иерархическое управление исключениями

Исключение является объектом некоторого класса, являющегося представлением исключительного случая. Код, обнаруживший ошибку, генерирует объект инструкцией Throw. Фрагмент кода выражает свое желание обрабатывать исключение при помощи инструкции Catch. Результатом throw является поиск подходящего Catch (в функции, которая непосредственно или косвенно вызывала функцию, сгенерировавшую исключение).

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

Class Math_err { };

Class Overflow : public Math_err { }; // переполнение сверху

Class Underflow : public Math_err { }; // переполнение снизу

Class Zerodivide : public Math_err { };// деление на ноль

Это позволяет обрабатывать любой Math_err, не заботясь о том, какое в точности исключение возникло. Например:

Void f()

{

try { /* … */ }

catch(Overflow)

{

// обработка Overflow или всех производных от Overflow

}

catch(Math_err)

{

// обработка любой Math_err, не являющейся Overflow

}

}

Использование иерархий классов для обработки исключений естественным образом приводит к обработчикам, интересующимся лишь подмножеством информации, которую несут с собой исключения. Другими словами, исключение обычно перехватывается обработчиком его базового класса, а не обработчиком его собственного класса. Семантика перехвата и задания имен исключений идентична семантике функции с аргументом. Из этого следует, что сгенерированное исключение «срезается» до перехваченного. Например:

Class Math_err

{

// …

virtual void debug_print() const

{ cerr << “ Математическая ошибка”; }

};

Class Int_overflow : public Math_err

{

const char* op;

int a1, a2;

Public:

Int_overflow(const char* p, int a, int b)

{ op = p; a1 = a; a2 = b; }

virtual void debug_print() const

{ cerr << ‘ ’ << op << ‘(’ << a1 << ‘,’ << a2 << ‘)’; }

// …

};

Void f()

{

try

{

g();

}

catch(Math_err m)

{

// …

}

}

Когда вызывается обработчик Math_err, M является объектом Math_err – даже если вызов G() привел к генерации Int_overflow. Это означает, что дополнительная информация, имеющаяся в Int_overflow, недоступна.

Во избежание потери информации можно использовать указатели или ссылки. Например:

Int add(int x, int y)

{

if((x > 0 && y > 0 && x > INT_MAX — y)||

( x < 0 && y < 0 && x < INT_MIN — y))

throw Int_overflow(“+”, x, y);

return x + y;

}

Void f()

{

try

{

int i1 = add(1, 2);

int i2 = add(INT_MAX, -2);

int i3 = add(INT_MAX, 2); // Приехали!

}

catch(Mathe_rr& m)

{

// …

m. debug_print();

}

}

Последний вызов Add() приведет к исключению, которое вызовет Int_overflow::debug_print(). Если бы исключение перехватывалось по значению, а не по ссылке, была бы вызвана функция Math_err::debug_print().

Не каждая группа исключений является древообразной структурой. Довольно часто исключение принадлежит сразу двум группам. Например:

// ошибка, связанная с файлом в сети

Class NetFile_err : public NetWork_err, public FileSystem_err

{

// …

};

NetFile_err может перехватываться функциями, работающими с исключениями в сети:

Void f()

{

try { /* … */ }

catch(NetWork_err& nwe) { /* … */ }

}

А также функциями, работающими с исключениями файловой системы:

Void g()

{

try { /* … */ }

catch(FileSystem_err& fse) { /* … */ }

}

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

Рассмотрим пример:

Void f()

{

try

{

throw E();

}

catch(H) { /* … */ }

}

Обработчик будет вызван:

1) если Н того же типа, что и Е;

2) если Н является открытым базовым классом Е;

3) если Н и Е являются указателями, и 1 или 2 выполняется для типов, на которые они ссылаются;

4) если Н и Е являются ссылками, и 1 или 2 выполняется для типа, на который Н ссылается.

Кроме того, можно добавить модификатор Const к типу, используемому для перехвата исключения. Это воспрепятствует модификации перехваченного исключения.

2.3. Обработка исключений в конструкторах и деструкторах

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

Возвратить объект в «неправильном» состоянии и полагаться на то, что пользователь проверит его состояние;

Присвоить значение нелокальной переменной (например, Errno) для указания на неуспешное создание объекта и полагаться на то, что пользователь проверит значение переменной;

Не осуществлять никакой инициализации в конструкторе и полагаться на то, что пользователь вызовет функцию инициализации до первого использования;

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

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

Class Vector

{

Public:

class Size_err { };

enum { max = 32000 };

Vector(int sz)

{

if(sz < 0 || sz > max) throw Size();

// …

}

}

Код, создающий вектора, теперь может перехватывать ошибки Vector::Size_err, которые можно каким-либо образом обработать:

Vector* f(int s)

{

try

{

Vector* p = new Vector(s)

// …

return p;

}

catch(Vector::Size_err)

{

// обработка ошибки размера вектора

}

}

Когда код, инициализирующий элемент (непосредственно или косвенно), генерирует исключение, оно (по умолчанию) передается туда, где вызван конструктор для класса этого элемента. Однако сам конструктор может перехватывать такие исключения, помещая все тело функции, включая список инициализаторов элементов, в блок Try. Например:

Class X

{

Vector v;

// …

Public:

X(int);

// …

};

X::X(int s)

Try : v(s) // инициализация v при помощи s

{

// …

}

Catch(Vector::Size_err) // перехват исключений

{ // сгенерированных при

// … // инициализации v

}

С точки зрения обработки исключений деструктор может вызываться одним из двух способов:

Нормальный вызов: в результате нормального выхода имени из области видимости, использования оператора Delete и т. д.;

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

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

Деструктор в состоянии защитить себя, если он вызывает функции, которые могут сгенерировать исключения. Например:

X::~X() try

{

f(); // может сгенерировать исключение

}

Catch(…)

{

// некоторые действия

}

2.4. Структурное управление исключениями

В современных системах программирования на языках C/C++ существует механизм так называемого структурного управления исключениями, где исключения идентифицируются только типом Unsigned int. Структурное управление исключениями позволяет наряду с обработкой потока явно порождаемых программой исключений обрабатывать и исключения, порождаемые операционной системой в аварийных ситуациях. Этим объясняется наличие единственного типа идентификации исключений и, как следствие, единственного блока обработки исключений в контролируемом блоке программы.

Различают две разновидности структурного управления исключениями:

Кадрированное управление – блок обработки исключений активизируется только в момент порождения исключения;

Завершающее управление – любой вид выхода из контролируемого блока программы завершается активизацией предопределенного блока операторов.

Операторы контролируемого блока могут явно породить исключение, используя функцию

VOID RaiseException (

DWORD dwExceptionCode,

DWORD dwExceptionFlags,

DWORD nNumberOfArguments,

CONST DWORD *lpArguments );

Интерпретация параметров функции RaiseException:

DwExceptionCode – код исключения;

DwExceptionFlags – флаг возобновления исключения;

NNumberOfArguments – количество аргументов детализации описания исключения в массиве LpArguments.

Предопределенные коды исключений:

EXCEPTION_ACCESS_VIOLATION – чтение или запись по адресу, не имея на то соответствующих прав;

EXCEPTION_DATATYPE_MISALIGNMENT – попытка чтения или записи данных с нарушением выравнивания;

EXCEPTION_BREAKPOINT – достигнута точка прерывания;

EXCEPTION_SINGLE_STEP – сигнал о том, что одиночная команда была выполнена;

EXCEPTION_ARRAY_BOUNDS_EXCEEDED – выход за пределы массива;

EXCEPTION_FLT_DENORMAL_OPERAND – недопустимое значение операнда в операциях с плавающей точкой;

EXCEPTION_FLT_DIVIDE_BY_ZERO – попытка деления на ноль в операциях с плавающей точкой;

EXCEPTION_FLT_INEXACT_RESULT – результат операции с плавающей точкой не может быть представлен в виде десятичной дроби;

EXCEPTION_FLT_INVALID_OPERATION – любое другое исключение в операциях с плавающей точкой, не включенное в этот список;

EXCEPTION_FLT_OVERFLOW – операция с плавающей запятой вызвала переполнение;

EXCEPTION_FLT_STACK_CHECK – переполнение стека в операциях с плавающей точкой;

EXCEPTION_FLT_UNDERFLOW – операция с плавающей запятой вызвала антипереполнение;

EXCEPTION_INT_DIVIDE_BY_ZERO – попытка деления на ноль в операциях с целыми;

EXCEPTION_INT_OVERFLOW – результат целочисленной операции вызвал переполнение;

EXCEPTION_PRIV_INSTRUCTION – попытка выполнить команду недопустимую для текущего режима;

EXCEPTION_IN_PAGE_ERROR – попытка обращения к неизвестной странице;

EXCEPTION_ILLEGAL_INSTRUCTION – попытка выполнения недопустимой инструкции;

EXCEPTION_NONCONTINUABLE_EXCEPTION – попытка продолжить выполнение команд, после возникновения исключение не позволяющее этого;

EXCEPTION_STACK_OVERFLOW – переполнение стека;

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

2.5. Кадрированное управление исключениями

Синтаксис определения кадрированного управления исключениями:

__try

{

// Операторы контролируемого блока

}

__except(выражение_фильтра)

{

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

}

Блок обработки исключения можно рассматривать как условный оператор, где решение о продолжении процесса определяется вычисляемым после порождения исключения выражением фильтра.

Выражение фильтра может принимать одно из значений:

EXCEPTION_EXECUTE_HANDLER (1) – обработать исключение;

EXCEPTION_CONTINUE_SEARCH (0) – продолжение поиска обработчика исключения на предшествующем уровне вложенности оператора __try;

EXCEPTION_CONTINUE_EXECUTION (-1) – возврат управления в точку выброса исключения.

Как в выражении фильтра, так и в блоке обработки исключения можно получить детальную информацию о причине исключения, вызывая функции

DWORD GetExceptionCode(VOID);

LPEXCEPTION_POINTERS GetExceptionInformation(VOID);

Функция GetExceptionCode() возвращает код исключения, а GetExceptionInformation() – указатель на структуру EXCEPTION_POINTERS, которая представляет собой детальное описание исключения.

2.6. Завершающее управление исключениями

Синтаксис определения завершающего управления исключениями:

__try

{

// Операторы контролируемого блока

}

__finally

{

// Операторы блока обработки факта завершения

// контролируемого блока

}

Завершение контролируемого блока может быть нормальным или преждевременно прерванным. Причины досрочного выхода:

Выполнение оператора Return, Goto, Break или Continue;

Вызов функции, подобной Longjump();

Порождение исключения.

Любой исход завершения контролируемого блока, безусловно приводит к гарантированному выполнению операторов блока __finally. Очевидно, что допускается совмещение кадрированного и завершающего управления исключениями. Например:

Void main()

{

puts("Начало программы");

int *p = 0x00000000; // Пустой указатель!

__try

{

puts("Начало блока контроля исключения");

__try

{

puts("Начало блока контроля завершения");

puts("Попытка нарушения защиты памяти…");

*p = 13;

puts("Продолжение работы");

}

__finally

{

puts("Блок завершения активен");

}

puts("Конец блока контроля исключения");

}

__except(puts("Фильтр активен"), 1)

{

puts("Исключение обработано");

}

puts("Завершение программы");

}

Результаты работы программы:

Начало программы

Начало блока контроля исключения

Начало блока контроля завершения

Попытка нарушения защиты памяти…

Фильтр активен

Блок завершения активен

Исключение обработано

Завершение программы