Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Итог_Пособие C++.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.03 Mб
Скачать

4.3 Поиск обработчика исключений. Раскрутка стека.

Пусть в какой-то точке программы был вызван оператор throw. Рассмотрим подробнее, что будет происходить далее. После вызова throw нормальная последовательность исполнения операторов программы прерывается, и выполняется раскрутка стека вызовов до первого обработчика исключений подходящего типа, после чего управление передаётся обработчику.

Поясним подробнее, что именно происходит при раскрутке стека. Пусть функция f1 вызвала функцию f2, функция f2 вызвала функцию f3, а в ней произошло исключение. Допустим, что подходящий обработчик исключения этого типа нашёлся только в функции f1. Перед тем, как передать управление этому обработчику, будут корректно удалены из стека все локальные переменные и параметры функции f3, затем − локальные переменные и параметры функции f2, и лишь после этого управление будет передано обработчику.

Словосочетание "корректно удалить" в том числе означает, что если локальная переменная или параметр функции является объектом, то для него будет вызван деструктор. Таким образом, гарантируется, что если локальный объект был полностью построен (его конструктор завершился успешно), то при раскрутке стека будет вызван его деструктор, то есть утечки ресурсов не произойдёт. Это очень важный момент, поясним его на примере функции firstLine (см. пример 4.5).

В этой функции создаётся локальный объект inp класса std::ifstream. Если где-то в функции произойдёт исключение, то для объекта inp будет гарантированно вызван деструктор, то есть не произойдёт утечки памяти и, возможно, других ресурсов (например, если исключение случится внутри функции getline, то открытый файл будет закрыт в деструкторе).

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

Рассмотрим пример. Представьте себе, что вы разрабатываете библиотеку для выполнения арифметических операций, например, над простыми дробями. Пусть у вас есть класс AriphmeticException (арифметическая ошибка), а от него наследуется класс DivideByZeroException (деление на ноль). Следующий код содержит ошибку:

try { тело блока try }

catch(AriphmeticException) {std::cerr <<"Арифметическая ошибка"; }

catch(DivideByZeroException) { std::cerr << "Деление на 0"; }

Здесь второй обработчик не выполнится никогда. Дело в том, что если произойдёт исключение типа DivideByZeroException, то всё равно выполнится первый обработчик, так как родительский тип совместим с дочерним. Чтобы пример стал работать правильно, необходимо поменять блоки catch местами.

4.4 Повторное возбуждение исключений

Оператор throw без параметров, находящийся внутри обработчика исключения, используется для повторного возбуждения того же самого исключения, которое было поймано в текущем блоке catch. Он применяется в том случае, когда исключение не может быть полностью обработано в данном блоке catch, и его обработка будет закончена где-то в другом месте. Чаще всего оператор try c блоком catch, содержащим повторное возбуждение исключения, используется для освобождения захваченных ресурсов перед тем, как продолжить раскрутку стека.

Рассмотрим пример. Пусть дан текстовый файл, содержащий последовательность целых чисел. Вначале в файле записано количество чисел N, а затем − сами числа, разделенные пробелами и/или переводами строк. Требуется написать функцию readIntegers, которая считывает все числа в массив и возвращает его в качестве результата. Если количество чисел окажется меньше, чем N, или в файле встретится не число, то функция должна возбудить исключение WrongFileFormatException:

// Пример 4.7 - повторное возбуждение исключения

class FileNotOpenedException{};

class WrongFileFormatException{};

int* readIntegers(const std::string &fileName) {

std::ifstream inp(fileName);

if (!inp.is_open()) throw FileNotOpenedException(fileName);

int n;

inp >> n;

if (!inp.good()) throw WrongFileFormatException();

int* a = new int [n];

try{

for (int i = 0; i < n; i++) {

inp >> a[i];

if (!inp.good()) throw WrongFileFormatException();

}

} catch(...) {

delete[] a;

throw; // повторное возбуждение исключения

}

return a;

}

Если при вводе чисел произойдёт исключение любого типа, то управление передастся блоку catch, в котором произойдёт освобождение памяти, выделенной под массив. Заметим, что автоматически эта память не освободилась бы: из стека удалилась бы лишь локальная переменная-указатель a, но захваченный в куче блок памяти так и остался бы занятым.

Мы предотвратили утечку памяти, но функция всё-таки не смогла выполнить свою работу − прочитать N чисел из файла. Поэтому повторно возбуждаем исключение, чтобы оно могло быть поймано другим обработчиком (например, находящимся в функции main).

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

// Пример 4.8 - улучшенный вариант функции readIntegers

std::vector<int> readIntegers(const std::string &fileName) {

std::ifstream inp(fileName);

if (!inp.is_open()) throw FileNotOpenedException(fileName);

int n;

inp >> n;

if (!inp.good()) throw WrongFileFormatException();

std::vector<int> a(n);

for (int i = 0; i < n; i++) {

inp >> a[i];

if (!inp.good()) throw WrongFileFormatException();

}

return a;

}

Как видим, код получился более коротким и понятным, он не содержит лишних блоков try-catch, а выделение и освобождение памяти скрыто в реализации класса std::vector.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]