Опубликован: 07.03.2015 | Уровень: для всех | Доступ: свободно | ВУЗ: Компания ALT Linux
Лекция 10:

Объектно-ориентированное программирование

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

10.5.1 Общие понятия

Классический подход к обработке ошибок в программе, разработанной в рамках процедурного подхода, предполагает анализ значений, возвращаемых функциями. Например, многие библиотечные функции в случае возникновения ошибки или какой-либо непредвиденной ситуации возвращают нулевое значение, интерпретируемое как "ложно", а в случае успешной работы — ненулевое, т. е. "истина". При необходимости передачи более детальной информации об ошибке, код ошибки может сохраняться в некую глобальную переменную (библиотечные функции, унаследованные из языка С, для этой цели используют глобальную переменную errno).

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

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

Для решения этих проблем в C++ был включён новый механизм — обработка исключительных ситуаций.

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

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

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

Программист, желающий использовать исключения, должен поместить вызов кода, в котором исключение может возникнуть, в специальный блок try{}. Следом за этим блоком должен следовать блок catch(){}, внутрь которого помещают код, обрабатывающий исключительную ситуацию. Например, если исключение может возникнуть в некой функции f(), для его обработки нужно написать следующую конструкцию:

f ( )
{
	//генерируем исключение, если возникла соответствующая ситуация
	if (.... ) throw индикатор;
.....
}
.....
try
{
	//вызываем код, который может сгенерировать исключение:
	f ( );
}
catch (индикатор)
{
	//обрабатываем исключение
.....
}

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

В следующем примере, показывающем, как можно применить обработку исключений для организации диалогового режима работы, мы объявим два пустых класса для использования в качестве индикаторов: класс unknown_exception, означающий получение неизвестного ответа от пользователя, и класс abort_exception, сигнализирующий о необходимости немедленного выхода из программы. Сама программа задаёт пользователю вопрос о выполнении последовательно 100 неких абстрактных пронумерованных команд. Диалог реализуется функцией confirm(), спрашивающей у пользователя подтверждение на выполнение команды с заданным номером и анализирующей ответ ("y"— подтверждение, "n" — отказ, "a" — немедленный выход).

#include <iostream>
#include <math.h>
using namespace std;
class unknown_exception { };
class abort_ exception { };
bool confirm ( int i )
{
	char c;
	cout << "Подтвердите команду " << i << " ( y / n / a/да/нет/выход) : ";
	cin >> c;
	cin.ignore ( ); //очищаем буфер если введены лишние символы
	switch ( c ) {
	case " y " : return true;
	case " n " : return false;
	case " a " : throw abort_exception ( );
	default : throw unknown_exception ( );
	}
}
main ( )
{
	cout << "Демонстрация диалога подтверждения при выполнении"<<" 100 команд\n ";
	for ( int i =1; i <=100; i++) {
	try{
		if ( confirm ( i ) ) cout << "КОМАНДА "<< i <<" ВЫПОЛНЕНА\n ";
		else cout << "КОМАНДА " << i << " ОТМЕНЕНА\n ";
	}
	catch ( unknown_exception ) {
	cout << "Неизвестный ответ пользователя\n ";
	i --; // возвращаемся к предыдущей команде
	}
	catch ( abort_exception ) {
	cout << "Выполняется немедленный выход из программы\n ";
	return 0;
	}
	cout << "Продолжение демонстрации диалога\n ";
	}
}

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

Обратите внимание, что в блоке catch() мы указали в качестве параметра только тип данных — класс-индикатор. Это допустимо с учётом того, что обработчик исключения не собирается извлекать никаких данных из переданного индикатора, да и сами классы-индикаторы, созданные в программе, являются пустыми и используются только для того, чтобы различать исключительные ситуации.

10.5.2 Передача информации в обработчик

Как уже упоминалось, чтобы передать в обработчик дополнительную информацию, нужно принимать в блоке catch() не тип данных, а переменную. Перед выбрасыванием исключения можно её проинициализировать конкретными данными, а потом прочитать эти данные в обработчике.

Приведём в качестве иллюстрации ещё один пример, в котором реализован класс array, предоставляющий пользователю массив с возможностью добавления и удаления элементов в стиле работы со стеком. Для этого класса будет перегружен оператор индекса [], возвращающий значение элемента с заданным номером, а также две функции для изменения размера массива: push(), позволяющая добавить новый элемент в конец массива, и pop(), забирающая из массива последний добавленный элемент. При создании объекта для массива будет резервироваться память, для чего конструктору будет передаваться параметр capacity — ёмкость массива, т. е. максимально допустимое число элементов.

#include <iostream>
#include <math.h>
using namespace std;
class general_error
{
public :
	char *message;
	general_error ( char* message ) { this->message=message; }
};
class out_of_range
{
public :
	size_t i;
	out_of_range ( size_t i ) { this->i= i; }
};
class overflow { };
class underflow { };
class array
{
	size_t size; //реальный размер массива
	size_t capacity; //максимально-допустимый размер
	double *data;
public :
	array ( size_t capacity );
	˜ array ( );
	double operator [ ] ( size_t i ); //получить значение i-го элемента
	void push ( double v ); //добавить элемент
	double pop ( ); //убрать последний добавленный элемент
};
array::array ( size_t capacity )
{
	if ( capacity==0)
throw general_error ( "массив нулевой вместимости" );
	this->capacity=capacity;
	size =0;
	data.new double [ capacity ];
}
array::˜ array ( )
{
	delete [ ] data;
}
double array::operator [ ] ( size_t i )
{
	if ( i < size ) return data[ i ];
	else throw out_of_range ( i );
}
void array::push ( double v )
{
	if ( size < capacity ) data[size++]=v;
	else throw overflow ( );
}
double array::pop ( )
{
	if ( size > 0 ) return data [-- size ];
	else throw underflow ( );
}
main ( )
{
	char c;
	size_t i;
	double v;
	cout << "Введите ёмкость массива: ";
	cin >> v;
	array a( v );
	for (;; )
	{
		cout << "Введите \ " + \ " для добавления элемента," " \ " - \ " для извлечения, \" i \" для
		просмотра " " i-го элемента, \" a \" для выхода: ";
		cin >> c;
		try
		{
			switch ( c )
			{
			case " + " :
				cout << "Введите добавляемое число: ";
				cin >> v;
				a.push ( v );
				break;
			case " - " :
				v=a.pop ( );
				cout << "Извлечено число " << v << endl;
				break;
			case " i " :
				cout << "Введите индекс: ";
				cin >> i;
				v=a[ i ];
				cout << "Искомый элемент равен " << v << endl;
				break;
			case " a::
				return 0;
				break;
			}
		}
		catch ( const out_of_range& e )
		{
			cout << "Попытка доступа к элементу с недопустимым индексом "<< e.i << endl;
		}
		catch ( overflow )
		{
			cout << "Операция не выполнена, так как массив переполнен\n ";
		}
		catch ( underflow )
		{
			cout << "Операция не выполнена, так как массив пуст\n ";
		}
	}
}

В этом примере использованы четыре класса-индикатора исключений: general_error для ошибок неопределённого типа (класс содержит строку message, описывающую суть возникшей проблемы), out_of_range для выхода индекса за границу массива (свойство i предусмотрено для значения индекса), а также классы overflow для ошибки переполнения ёмкости массива и underflow для попыток удалить элемент из пустого массива. Обработчик out_of_range принимает объект класса-индикатора и сообщает пользователю, какое именно значение индекса оказалось недопустимым. Диалог с пользователем ведётся в бесконечном цикле, на каждой итерации которого предлагается выбрать одно из четырёх действий: добавление элемента, удаление элемента, просмотр элемента с заданным индексом или выход из программы.

Сергей Радыгин
Сергей Радыгин

Символы кириллицы выводит некорректно. Как сделать чтобы выводился читабельный текст на русском языке?

Тип приложения - не Qt,

Qt Creator 4.5.0 основан на Qt 5.10.0. Win7.

 

Юрий Герко
Юрий Герко

Кому удалось собрать пример из раздела 13.2 Компоновка (Layouts)? Если создавать проект по изложенному алгоритму, автоматически не создается  файл mainwindow.cpp. Если создавать этот файл вручную и добавлять в проект, сборка не получается - компилятор сообщает об отсутствии класса MainWindow. Как правильно выполнить пример?