§27 Обработка исключений

Что такое “исключение”?

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

  • Недопустимые арифметические операции, например, деление на ноль
  • Файл, к которому осуществляется доступ потоком fstream, – отсутствует в системе
  • Попытка приведения типа недопустимого литерала
  • Память, выделенная объекту, исчерпана
  • Устройство в системе не найдено
Появление исключительной ситуации производит аварийное завершение программы. Такие ситуации нужно прогнозировать и “отлавливать”, чтобы программа имела возможность продолжать работу, т. е. была устойчива к появлению исключений.
Некоторые прогнозируемые исключительные ситуации мы учитывали и ранее, на предыдущих этапах обучения. Для их “перехвата” мы использовали условные инструкции if-else. Но такая схема не будет обладать гибкостью и универсальностью, а тем более читаемостью.

Инструкция try-catch

В C++ используется встроенный механизм обработки исключительных ситуаций. Управляющая конструкция состоит из двух частей. Первая часть – это блок контролируемого кода, а вторая часть – это обработчики исключительных ситуаций: try-catch. Первую часть обработчика событий представляет блок try. А непосредственными обработчиками исключительных ситуаций являются блоки catch, которых (по отношению к данному блоку try) может быть столько, сколько это необходимо. Синтаксис этой инструкции управления выглядит следующим образом:

try { 
    // Блок контролируемого кода    
} 
catch (exception1) {
    // Будет выполняться этот блок кода,
    // если перехвачено исключение типа exception1
} 
catch (exception2) {
    // Блок обработки исключения exception2
} 
catch (exception3) {
    // Блок обработки исключения exception3
    throw;
} 
catch ( ... ) { 
    // Многоточие - это часть синтаксиса!
    // Обрабатывается исключение
    // любого другого типа,
    // которое не было обработано
    // предыдущими блоками catch
} 

В C++ можно обрабатывать любые исключения, как определенные в классах исключений STD, так и определённые разработчиком в виде своих классов и функций. Чтобы иметь возможность работать со встроенными классами исключений необходимо включать заголовки в которых эти классы определены. Например:

  • exeption – базовый класс для исключений, захват и сохранение объектов исключений, обработка сбоев при обработке исключений, обработка нарушения спецификации исключений
  • stdexcept – классы логических ошибок и ошибок времени выполнения
  • new – классы низкоуровневого управления памятью
  • type_info – классы поддержки типов
  • system_error – типы и функции для сообщений об ошибках состояния ОС
Каждый из таких заголовков включает определения нескольких классов исключений. Вот лишь небольшой перечень исключений (с которыми вы, возможно, сталкивались не раз) предоставляемых заголовком stdexcept:
  • logic_error – ошибка в логике программы
  • invalid_argument – использование неверных аргументов
  • length_error – попытка превысить максимально допустимый размер
  • out_of_range – выход за пределы диапазона
  • runtime_error – ошибки времени выполнения
  • range_error – нарушение диапазона значений во внутренних расчетах
  • overflow_error – арифметическое переполнение
Стоит упомянуть, что приведенный выше список заголовков далеко не полный. Многие библиотеки входящие в STD (например, ввода/вывода ios) имеют собственные классы исключений. Такой неупорядоченный разброс встроенных классов исключений по STD усложняет работу и требует хорошего знания стандартной библиотеки. С другой стороны, используя базовый класс исключений и знание иерархии встроенных классов исключений можно создавать свои собственные классы, включая их в единый механизм.
Приведем пример обработки исключения out_of_range. Вы знаете, что такое исключение возбуждается при использовании метода at(), когда предпринимается попытка выхода за границы массива. В результате выполнения программы 27.1a:
Программа 27.1a

#include <iostream>
#include <vector>
using namespace std;

void printmas(vector<int>&, size_t&);

int main() {
	size_t t;
	vector<int> mas(10);
	cout << "t = "; cin >> t;
	printmas(mas, t);
	return 0;
}
void printmas(vector<int> &ar, size_t &n)  {
	for (size_t i = 0; i < n; i++)
		ar.at(i) = i;
	for (size_t i = 0; i < n; i++)
		cout << ar.at(i) << " ";
}

при вводе значения t => 11 будет возбуждено исключение:

terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 10) >= this->size() (which is 10)

Создадим обработчик исключения такой, чтобы он перехватил и обработал это исключение таким образом, чтобы размер контейнера стал достаточным для корректного завершения операции при любом (положительном) значении t.
Программа 27.1b

#include <iostream>
#include <stdexcept>
#include <vector>
using namespace std;

void printmas(vector<int>&, size_t&);

int main() {
	size_t t;
	vector<int> mas(10);
	cout << "t = "; cin >> t;
	try {
		printmas(mas, t);
	}
	catch (out_of_range) {
		mas.resize(t + 1);
		printmas(mas, t);
	}
	return 0;
}

void printmas(vector<int> &ar, size_t &n)  {
	for (size_t i = 0; i < n; i++)
		ar.at(i) = i;
	for (size_t i = 0; i < n; i++)
		cout << ar.at(i) << " ";
}

Поток ошибок cerr. Функция-член what()

Выводу ошибок можно придавать форматированный вид. В библиотеке IOStream определен глобальный объект cerr (подобный cout), который представляет собой стандартный поток ошибок, предназначенный для их вывода на стандартное устройство (монитор). Отличие cerr от cout в том, что первый, по умолчанию, не проходит буферизацию.
Примечание. Вы можете использовать для вывода ошибок объект cout.
Во всех классах исключений определен один метод what() возвращающий Си-строку. Эта строка будет содержать дополнительную информацию об ошибке. Для того, чтобы воспользоваться методом what() необходимо создать объект класса исключения. Для этого заголовок блока cath должен содержать не имя класса, а конструктор:

catch (const exeption& obj)

Добавим в программе 27.1b вывод сообщения об ошибке:

catch (const out_of_range &e) {
		cerr << "\nУвеличиваем размер массива:\n"
			 << e.what() << "\n\n";
		mas.resize(t + 1);
		printmas(mas, t);
	}
t = 20
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 
Увеличиваем размер массива:
vector::_M_range_check: __n (which is 10) >= this->size() (which is 10)

Операция throw

Операция throw предназначена для возбуждения (raise) исключения. Операция throw должна находиться в блоке try или в функции, которая вызывается в этом блоке. Внутри блока try, throw действует также, как return. Эта операция принимает выражение определенного типа (в том числе абстрактного). Тип выражения определяет, какой из обработчиков catch будет обрабатывать это исключение. Выбор будет зависеть от типа определенного в заголовке блока catch. Однако жесткой привязки throw к конкретному блоку catch не устанавливается. Будет выбран ближайший (или первый) подходящий блок catch соответствующего типа. При этой передаче приведение типов (по правилам языка) не производится.
Программа 27.2

#include <iostream>
using namespace std;

class a {
public:
	a() {};
	a(int t) {
		cout << t << " - Abstract data type" << endl;
	}
};

int main() {
	int q;
	cout << "q = "; cin >> q;
	try {
		switch (q) {
			case 1: throw q; break;
			case 2: throw unsigned(q); break;
			case 3: throw double(q); break;
			case 4: throw a(q); break;
			default: throw "C-string type";
		}
	}
	catch (int) {
		cout << "1 - Type INT" << endl;
	}
	catch (unsigned) {
		cout << "2 - Type UNSIGNED" << endl;
	}
	catch (double) {
		cout << "3 - Type DOUBLE" << endl;
	}
	catch (a) {
		a(q);
	}
	catch (const char* &s) {
		cout << s;
	}

	return 0;
}
q = 4
4 - Abstract data type

Инструкции находящиеся после throw будут проигнорированы, поэтому throw всегда находится в конце функции или в условной инструкции.
Программа 27.3

#include <iostream>
#include <cmath>
using namespace std;

double hyp(int&);
double sqr(int&);

int main() {
	int n;
	cout << "n = "; cin >> n;
	cout << "1 / " << n << " = ";
	try {
		cout << hyp(n) << endl;
	}
	catch (int) {
		cout << "Ошибка: на ноль делить нельзя!" << endl;
	}
	cout << "n = "; cin >> n;
	cout << "sqrt(" << n << ") = ";
	try {
		cout << sqr(n) << endl;
	}
	catch (const char* s) {
		cout << "Ошибка: " << s << endl;
	}
	return 0;
}

double hyp(int &x) {
	if (!x)
		throw x;
	return 1 / x;
}

double sqr(int &x) {
	if (x < 0)
		throw "отрицательное число под корнем!";
	return sqrt(x);
}
n = 0
1 / 0 = Ошибка: на ноль делить нельзя!
n = -1
sqrt(-1) = Ошибка: отрицательное число под корнем!

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

#include <iostream>
using namespace std;

void testExcept(void);

int main() {
	try {
		testExcept();
	}
	catch (int) {
		cout << "Исключение 2" << endl;
	}
	return 0;
}

void testExcept() {
	try {
		throw 1;
	}
	catch (int) {
		cout << "Исключение 1" << endl;
		throw;
	}
}
Исключение 1
Исключение 2

Здесь нами рассмотрены только основы работы с исключениями. Использование исключений совместно с классами мы рассмотрим позднее.

Print Friendly, PDF & Email

Comments are closed.