Обработка исключительных ситуаций

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

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

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

Исключения разделяют вычислительный процесс на две части - обнаружение аварийной ситуации и ее обработка.

Причины:

· Структуризация программы,

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

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

Порядок обработки исключительной ситуации:

· Появляется ошибка, и функция, где она возникла, генерирует (порождает) исключение,

· выполнение текущего блока прекращается, происходит поиск соответствующего обработчика и передача ему управления,

· Если он не найден, вызывается стандартная функция terminate, которая в свою очередь вызывает функцию abort, аварийно завершающую текущий процесс. Можно установить собственную функцию завершения процесса.

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

Стек вызовов - последовательность вызванных, но еще не завершившихся функций.

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

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

Синтаксис исключений

Синтаксис контролирующего блока (кода, в котором может генерироваться исключение):

try {

...

}

Принадлежащими try-блоку также считаются функции прямо или косвенно вызываемые из него.

Синтаксис генерации исключения:

throw [ выражение ];

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

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

Синтаксис обработчика (три формы записи):

1. Применяется, когда имя исключения используется в теле обработчика для выполнения каких-либо действий (например, вывода информации об исключении).

catch(тип имя){ ... /* тело обработчика */ }, где параметр тип - тип исключения.

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

catch(тип){ ... /* тело обработчика */ }

3. Обработчик перехватывает все исключения.

catch(...){ .. /* тело обработчика */ }

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

Обработчик должен располагаться непосредственно за try-блоком. Можно записать один или несколько обработчиков в соответствии с типами обрабатываемых исключений.

Пример:

catch (int i){

... // Обработка исключений типа int

}

catch (const char *){

... // Обработка исключений типа const char*

}

catch (Overflow){

... // Обработка исключений класса Overflow

}

catch(...){

... // Обработка всех необслуженных исключений

}

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

Перехват исключений

После генерации исключения с помощью throw функции исполнительной библиотеки C++ выполняют:

· создание копии параметра throw в виде статического объекта (существует до тех пор, пока исключение не будет обработано);

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

· передачу объекта и управления найденному обработчику.

Обработчик считается найденным, если тип объекта, указанного после throw:

· совпадает с параметром, указанным в параметре catch (может быть записан в форме Т, const Т, Т& или const Т&, где Т— тип исключения);

· является производным от указанного в параметре catch (если наследование производилось с ключом доступа public). Поэтому обработчики производных классов следует размещать до обработчиков базовых (в противном случае им никогда не будет передано управление).

· является указателем, который может быть преобразован по стандартным правилам преобразования указателей к типу указателя в параметре catch. Указатель типа void следует размещать после обработчиков указателей конкретного типа (он скрывает указатель любого другого типа).

Пример:

#include <fstream.h>

class Hello{

// Класс, информирующий о своем создании и уничтожении

public:

Hello( ){cout << "Hello!" << endl;}

~Hello( ){cout << "Bye!" << endl;}

};

void f1( ){

ifstream ifs("\\INVALID\\FILE\\NAME"); // Открываем файл

if (!ifs){

cout << "Генерируем исключение" << endl;

throw "Ошибка при открытии файла";

}

}

void f2( ){

Hello Н; // Создаем локальный объект

f1( ); // Вызываем функцию. генерирующую исключение

}

int main ( ) {

try{

cout << "Входим в try-блок" << endl;

f2();

cout << "Выходим из try-блока" << еndl;

}

catch (int i){

cout << "Вызван обработчик int, исключение - " << i << endl;

return -1;

}

catch (const char * p){

cout << "Вызван обработчик const char*, исключение - " << p << endl;

return - 1;

}

catch(...){

cout << "Вызван обработчик всех исключений" << endl;

return -1;

}

return 0; // Все обошлось благополучно

}

Результаты выполнения программы:

Входим в try-блок

Hello!

Генерируем исключение

Bye!

Вызван обработчик const char *, исключение - Ошибка при открытии файла

После порождения исключения был вызван деструктор локального объекта, хотя управление из функции f1 было передано обработчику, находящемуся в функции main. Сообщение «Выходим из try-блока» не было выведено.

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

Примером является класс для работы с файлом. Конструктор класса открывает файл, а деструктор - закрывает. При возникновении ошибки файл будет корректно закрыт, и информация не будет утеряна.

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

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

Исключения в функциях

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

void fl() throw (int, const char*){ /* Тело функции */ }

void f2() throw (Oops*){ /* Тело функции */ }

Функция fl должна генерировать исключения только типов int и const char*, f2 - только исключения типа указателя на класс Oops или производных от него классов.

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

void f( ) throw ( ){

// Тело функции, не порождающей исключений

}

Заголовок является интерфейсом функции, поэтому указание в нем списка исключений дает информацию для ее использования, а также гарантию, что при возникновении непредвиденного исключения эта ситуация будет обнаружена (функция может прямо или косвенно породить исключение, которое не описано в ее интерфейсе).

Возникновение непредвиденного исключения приводит к вызову стандартной функции unexpected, которая по умолчанию вызывает функцию terminate (рисунок 2.3). С помощью функции set_unexpected можно установить собственную функцию, которая будет вызываться вместо terminate и определять действие программы при возникновении непредвиденной исключительной ситуации.

Функция terminate по умолчанию вызывает функцию abort, которая завершает выполнение программы. С помощью функции set_terminate можно установить собственную функцию, которая будет вызываться вместо abort и определять способ завершения программы. Функции set_unexpected и set_terminate описаны в заголовочном файле <exception>.

Рисунок 2.3 - Алгоритм обработки исключения.

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

Наши рекомендации