December 4, 2019

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

Исключительная ситуация, или исключение — это возникновение непредвиден­ного или аварийного события, которое может порождаться некорректным ис­пользованием аппаратуры. Например, это деление на ноль или обращение по не­существующему адресу памяти. Обычно эти события приводят к завершению программы с системным сообщением об ошибке. C++ дает программисту воз­можность восстанавливать программу и продолжать ее выполнение.

Try-catch-throw
Давайте же разберем основы обработки исключений в С++. Чтобы комфортно работать с исключениями в С++ вам нужно знать лишь три ключевых слова:
• try (пытаться) - начало блока исключений;
• catch (поймать) - начало блока, "ловящего" исключение;
• throw (бросить) - ключевое слово, "создающее" ("возбуждающее") исключение.
А теперь пример, демонстрирующий, как применить то, что вы узнали:

void func()
{
	try
	{
		throw 1;
	}
	catch (int a)
	{
		cout << "Caught exception number: " << a << endl;
		return;
	}
	cout << "No exception detected!" << endl;
	return;
}

Если выполнить этот фрагмент кода, то мы получим следующий результат:
Caught exception number: 1
Теперь закоментируйте строку throw 1; и функция выдаст такой результат:
No exception detected!
Как видите все очень просто, но если это применить с умом, такой подход покажется вам очень мощным средством обработки ошибок. Catch может "ловить" любой тип данных, так же как и throw может "кинуть" данные любого типа. Т.е. throw AnyClass(); будет правильно работать, так же как и catch
(AnyClass &d) {};.

работать что-нибудь типа этого:
catch(dumbclass) { }
так же, как и
catch(dumbclass&) { }
Так же можно "поймать" и все исключения:
catch(...) { }

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

try {
	throw 1;
	// throw 'a';
}
catch (long b) {
	cout << "пойман тип long: " << b << endl;
}
catch (char b) {
	cout << "пойман тип char: " << b << endl;
}

"Создание" исключений

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

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

Перегрузка глобальных операторов new/delete

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

char *a;
try
{
	a = new char[10];
}
catch (...) {
	// a не создан - обработать ошибку распределения памяти, 
	// выйти из программы и т.п.
}
// a успешно создан, продолжаем выполнение

Это, на первый взгляд, кажется длиннее, чем стандартная проверка в С "а равен NULL?", однако если в программе выделяется десяток динамических переменных, то такой метод оправдывает себя.

Операторы throw без параметров

Итак, мы увидели, как новый метод обработки ошибок удобен и прост. Блок try-catch может содержать вложенные блоки try-catch и если не будет определено соответствующего оператора catch на текущем уровне вложения, исключение будет поймано на более высоком уровне. Единственная вещь, о которой вы должны помнить, - это то, что операторы, следующие за throw, никогда не выполнятся.

try
{
	throw;
	// ни один оператор, следующий далее (до закрывающей скобки) 
	// выполнен не будет
}
catch (...)
{
	cout << "Исключение!" << endl;
}

Такой метод может применяться в случаях, когда не нужно передавать никаких данных в блок catch.