Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
OOP_otvety_k_ekzamenu.doc
Скачиваний:
55
Добавлен:
13.04.2015
Размер:
786.94 Кб
Скачать

42. Обработка исключений. Цели, синтаксис выброса и обработчиков. Выбор обработчика по типу. Передача данных исключения по значению, указателю и ссылке. Исключения языка и стандартной библиотеки.

Обработка исключений – поиск и устранение ошибок.

Основные принципы механизма исключений:

- разделение кода нормального функционирования и кода обработки ошибок;

- функция, обнаруживающая ошибку, транспортирует информацию о возникшей проблеме в место обработки;

- возможность выбора между тонкой обработкой конкретного типа ошибки и более широкой группы ситуаций одним или несколькими обработчиками общего назначения;

- малый расход при нормальном выполнении программы .

Синтаксис выброса и обработчиков. Блок try-catch :

try {“выброс” -- что-либо, генерирующее неподходящую инфу, которая призведёт к ошибке выполнения осн. функции,т.е. создание исключения}

catch (тип исключения) {“обработчик”}

После выхода из блока catch работа проги возобновляеться, если на выброшеное исключение не нашелся подходящий блок обработки catch, среда обработки вызовет ф-ю terminate, которая вызовет ф-ю abort, это неловко остановит прогу.

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

Разные типы ошибок = много блоков catch. Это выглядит так:

try { // код } catch (int param) { cout << "int exception"; } catch (char param) { cout << "char exception"; } catch (...) { cout << "default exception"; }

Обработчики исключений выбираются в порядке, в котором они записаны в коде по типу, указанному в () после catch, как при вызове ф-ии. Управление передается в тот блок, тип исключения которого признается подходящим первым. Типы сопоставляются по следующему принципу:

  • фактический тип исключения точно совпадает с типом в обработчике;

  • тип в обработчике является однозначным базовым классом для фактического типа;

  • если фактический тип исключения является указателем, а тип в обработчике является точно таким же указателем либо указателем на однозначный базовый класс;

  • фактический тип исключения является простым копируемым объектом либо ссылкой, а тип в обработчике является ссылкой на это же тип либо ссылкой на однозначный базовый класс;

  • на сопоставление не влияет дополнительный модификатор const в типе, указываемом в обработчике исключения

Так же возможен перехват всех типов исключений:

catch ( ... ) троеточие вместо типа исключения = все типы

{

std::cerr << “Fatal error. Please contact support@company.com” << std::endl; }

Oбработчики всегда нужно писать от наиболее конкретных классов к базовым.

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

Оператор throw используеться для генерации исключительных ситуаций

try { throw 20;}, к примеру.

Выбрасывать исключения логично по значению, а перехватывать по обычной либо константной ссылке.

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

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

void runProgram ()

{

try

{

ManagedIntegerArray a( 10 );

a[ rand() ] = 25;

}

catch ( ManagedIntegerArray::IndexOutOfRange & e )

{

std::cout << “Program has a problem with array index” << std::endl;

} }

int main ()

{

try

{

runProgram();

}

catch ( ... )

{

std::cout << “Program has some unknown problem” << std::endl;

} }

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

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

void runProgram ()

{

try

{

ManagedIntegerArray a( 10 );

a[ rand() ] = 25;

}

catch ( ManagedIntegerArray::IndexOutOfRange & e )

{

// Локальная обработка

std::cout << “Program has a problem with array index” << std::endl;

// Повторная генерация того же исключения

throw;

} }

Если повторную генерацию инициировать в момент, когда обработки исключения на самом деле не происходит (синтаксис языка позволяет написать инструкцию throw без аргументов в теле любой функции), то это приведет к немедленному вызову функции termin

Передача и перехват указателя:

try

{

throw new DerivedException( 0, 0 );

}

catch ( DerivedException * _pE )

{

delete _pE;

}

Если не удалять объекты-исключения, перехватываемые по указателю, то это может привести к утечкам памяти, когда на стороне выброса использовалось динамическое выделение памяти. Если удалять все объекты-исключения, можно получить фатальный сбой при попытке удаления объекта, не хранящегося в динамической памяти, такого как globalException. Соответственно, вариант с указателями большинство программистов отбросит из рассмотрения.

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

Исключения стандартной библиотеки. . Все исключения, выбрасываемые операторами языка и средствами стандартной библиотеки, образуют иерархию наследования классов, вершиной которой является класс std::exception.

Определение класса std::exception и большинства производных от него классов становится доступным после включения заголовочного файла <exception>. std::exception является конкретным классом, объекты которого могут быть непосредственно созданы. Такой объект содержит минимальную функциональность, а именно:

  • конструктор по умолчанию;

  • конструктор копий и оператор присвоения;

  • виртуальный деструктор;

  • виртуальный метод what(), возвращающий строку в стиле C, описывающую исключение.

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

Часто используемые исключения:

  • исключение std::bad_allocвыбрасывается операторомnew, когда он не может обеспечить выделение запрошенного блока динамической памяти;

  • исключение std::bad_cast выбрасывается операторомdynamic_cast, когда запрошенного преобразования не существует, а работа ведется со ссылками, а не с укаталеями;

  • исключение std::bad_typeidвыбрасывается операторомtypeid, если его вызвать на нулевом указателе полиморфного типа;

  • исключение std::out_of_rangeвыбрасывается контейнерами std::vector и std::deque при попытке обращения к ячейке по некорректному индексу;

  • исключение std::ios_base::failureвыбрасывается библиотекой ввода/вывода в ряде ошибочных ситуаций - это поведение отключено по умолчанию, чтобы его включить, необходимо вызвать метод exceptions, и тогда потоки в ошибочных ситуациях будут генерировать исключения:

std::ifstream file; file.exceptions ( std::ifstream::failbit | std::ifstream::badbit );

Стандартные классы исключений std::logic_error и std::runtime_error принимают в качестве аргумента при создании строку std::string, описывающую возникшую проблему. По умолчанию, метод what() возвращает именно эту строку, что делает такие классы довольно удобными для обработки исключений в простых программах.

Класс std::logic_error предполагается использовать для ошибок внутренней логики программы, таких как нарушение логических предусловий и инвариантов. Такие ошибки являются, как правило, исправимыми в коде программы до ее запуска. Класс std::runtime_error, напротив, предназначен для ошибок, которые можно обнаружить только во время выполнения, например ошибки переполнения АТД “стек” фиксированного размера. Предполагается, что эти классы будут базовой отправной точкой для пользовательских классов-исключений. В прочем, часто пользовательские классы-исключения наследуют непосредственно от вершины иерархии - класса std::exception.

43. Процесс stack unwinding при выбросе исключений. Уничтожение полностью сконструированных локальных объектов. Утечки памяти при выбросе исключений. Исключения в конструкторах и деструкторах. Повторная генерация исключений.

Процесс поиска обработчика сгенерированного исключения по стеку вызовов функций называется stack unwinding(дословно, “разматывание” или “раскрутка” стека).

Собственно, в рамках этого процесса решаются 2 основные задачи:

  1. Поиск подходящего обработчика исключения по стеку вызовов.

  2. Вызов деструкторов локальных объектов.

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

Поскольку выброс исключения прерывает нормальный поток выполнения программы, к моменту выброса некоторые объекты могут быть еще не сконструированными.

Рассмотрим следующий простейший пример:

void f ()

{

Date d1;

if ( d1.GetDay() == 1 )

throw std::runtime_error( “Cannot call f() on the 1st day of the month” );

Date d2( 2013, 12, 23 );

// ...

}

В зависимости от текущего числа, может быть сгенерировано исключение. К этому моменту локальный объект d1 уже сконструирован, в то время как объект d2 еще не сконструирован. Механизм stack unwinding должен гарантировать, что в таком случае будет вызван деструктор объекта d1, но не будет вызван деструктор объекта d2.

Для реализации такого поведения весьма полезным свойством языка является вызов деструкторов локальных объектов в обратном конструированию порядке. В частности, в обычной ситуации, когда исключения не происходит, при выходе потока управления из функции f, компилятор будет вызывать деструктор сначала на объекте d2, а затем на объекте d1:

void f ()

{

Date d1;

if ( d1.GetDay() == 1 )

throw std::runtime_error( “Cannot call f() on the 1st day of the month” );

Date d2( 2013, 12, 23 );

// ...

// Date::~Date( & d2 );

// Date::~Date( & d1 );

}

Эта особенность чрезвычайна удобна для организации в компиляторе очистки при выбросе исключения. В случае выброса исключения в части функции, когда объект d1 уже сконструирован, а объект d2 еще нет, необходимо каким-либо образом “перескочить” вызовы деструкторов объектов, которые еще не сконструированы, и продолжить начиная с деструкторов, которые нужно вызвать.

Любую функцию условно можно условно разбить на зоны. В данном конкретном примере 3 зоны:

  • зона 0 - не сконструировано ни одного объекта;

  • зона 1 - сконструирован объект d1, но не d2;

  • зона 2 - сконструированы объекты d1 и d2.

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

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

void f ()

{

Date * pDate = new Date();

try

{

if ( pDate->GetDay() == 1 )

throw std::runtime_error( “...” );

// ...

// Нормальное освобождение ресурса

delete pDate;

}

catch ( ... )

{

// Аварийное освобождение ресурса

delete pDate;

// Повторная генерация исключения

throw;

}

}

void g ()

{

FILE * file = fopen( “test.txt”, “rt” ); try

{

if ( some_condition )

throw std::runtime_error( “Condition failed” );

// …

// Нормальное освобождение ресурса

fclose( file );

}

catch ( ... )

{

// Аварийное освобождение ресурса

fclose( file );

// Повторная генерация исключения

throw;

}

}

Исключения в конструкторах и деструкторах

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

Если исключение генерируется при конструировании объекта, он считается неполностью сконструированным, и его деструктор не вызывается.Например, класс Date генерирует исключения, если передать неправильные компоненты даты:

try

{

Date d1( 2013, 14, 1 ); // 14 месяц не существует

// До вызова деструктора выполнение не дойдет:

// Date::~Date( & d1 );

}

catch ( ... )

{

}

В случае динамического выделения памяти для нового объекта и появления исключения в конструкторе, во избежание явной утечки памяти, будет автоматически вызван оператор delete:

try

{

Date * pDate = new Date( 2013, 14, 1 ); // 14 месяц не существует

// До вызова деструктора выполнение не дойдет, но будет вызван оператор delete

}

catch ( ... )

{

}

Если ресурс выделяется до выброса исключения в конструкторе, он не будет освобожден, поскольку деструктор не полностью сконструированного объекта вызываться не будет. Это не относится к полностью сконструированным дочерним объектам, их деструкторы будут вызваны сразу при выбросе исключения.

Также следует быть чрезвычайно аккуратным при выбросе исключений в деструкторах. Нежелательно выбрасывать исключения в деструкторах в принципе. Риск состоит в том, что деструктор объекта, выбрасывающий исключение, может быть вызван как при нормальном выполнении программы, так и в ходе процесса stack unwinding, когда уже обрабатывается другое исключение. Правила языка запрещают одному и тому же потоку управления выбрасывать второе исключение во время обработки первого, и такое нарушение приведет к прерыванию выполнения программы.

Чтобы смягчить такой риск, можно использовать функцию std::uncaught_exception, вызвав которую можно определить, что система в данный момент находится в процессе stack unwinding и обрабатывает исключение.

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

void runProgram ()

{

try

{

ManagedIntegerArray a( 10 );

a[ rand() ] = 25;

}

catch ( ManagedIntegerArray::IndexOutOfRange & e )

{

// Локальная обработка

std::cout << “Program has a problem with array index” << std::endl;

// Повторная генерация того же исключения

throw;

} }

Если повторную генерацию инициировать в момент, когда обработки исключения на самом деле не происходит, то это приведет к немедленному вызову функции terminate (по умолчанию вызовет функцию abort, аварийно завершив программу).

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