ОБРАБОТКА ИСКЛЮЧЕНИЙ В VISUAL C++
Ни одна серьезная программа не может обойтись без собственных обработчиков исключений,
так как во время выполнения программы рано или поздно могут случиться разные «неожиданности».
Например, пользователь введет неверные данные и компьютеру придется выполнять деление на ноль.
Или исчезнет куда-нибудь файл, необходимый для работы программы. Конечно, ничего страшного не
произойдет, так как операционная система обрабатывает подобные неприятности. Но
программа при этом завершится аварийно, а это уже грозит не только потерей несохраненных
данных, но и серией нецензурных выражений в адрес программиста со стороны пользователя.
А это как раз тот редкий случай, когда пользователь абсолютно прав. Поэтому любой
мало-мальски грамотный программист должен предусмотреть возможное появление ошибок
в ходе выполнения программы и принять соответствующие меры.
Эта статья посвящена обработке исключений. В качестве примеров используются языковые
конструкции Visual C++ 6.5.
1. Фреймовая обработка исключений
1.1. Исключения и их обработчики
Исключение – это событие, которое произошло во время выполнения программы, в
результате совершения которого дальнейшее нормальное выполнение программы становится
невозможным. Обычно такие события происходят из-за ошибок в программе или
неправильных действий пользователя (впрочем, хороший программист не может
рассчитывать на то, что пользователь всегда будет действовать правильно).
После возникновения исключения требуется привести программу в рабочее состояние
или выполнить её аварийное завершение с освобождением всех ресурсов, которые
использовались программой.
Для выполнения описанных выше действий в операционных системах Windows предназначен
механизм
структурной обработки исключений (structured exception handling, SHE).
Работает это так. В программе выделяется блок программного кода, где может произойти
исключение. Этот блок кода называется
фреймом, а сам код называется
охраняемым кодом.
После фрейма вставляется программный блок, где обрабатывается исключение. Этот блок
называется
обработчиком исключения. Когда исключение будет обработано, управление
передается первой инструкции, которая следует за обработчиком исключения.
В языке С++ для обработки исключений имеются ключевые слова
__try и
__except.
Ключевое слово
__try отмечает фрейм, а ключевое слово
__except отмечает обработчик
исключения. Таким образом, участок кода, где используется механизм структурной
обработки исключений, выглядит так:
__try
{
//Охраняемый код
}
__except (выражение-фильтр)
{
//Код обработки исключения
}
Здесь
выражение-фильтр – это выражение языка С++, которое указывает на то, как
должна выполняться программа после обработки исключения. Это выражение вычисляется
сразу после возникновения исключения и в результате даёт одно из следующих значений:
- EXCEPTION_EXECUTE_HANDLER – управление передается обработчику исключений;
- EXCEPTION_CONTINUE_SEARCH – система продолжает поиск обработчика исключения;
- EXCEPTION_CONTINUE_EXECUTION – система передает управление в точку прерывания программы.
Не допускается использование оператора
goto для передачи управления внутрь фрейма
или обработчика исключения. В выражении фильтра возможно использование функций
GetExceptionCode и
GetExeptionInformation, которые предоставляют информацию о
происшедшем исключении. Эти функции будут рассмотрены ниже.
Переменные, объявленные внутри фрейма или блока обработки исключения, являются
локальными и видны только внутри соответствующего блока, как это принято в С++.
Пример обработки исключения:
#include
#include
void main()
{
int a = 10;
int *p = NULL; //пустой указатель на целое число
__try
{
cout << “a = “ << *p << endl; //ошибка, так как p = NULL
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
cout << “There was some exception.” << endl;
p = &a;
}
cout << «a = « << *p << endl; //нормально
}
1.2. Как получить код исключения
Чтобы получить код исключения, которое произошло, можно вызвать функцию
GetExceptionCode.
Функция имеет следующий прототип:
DWORD GetExceptionCode(VOID);
Функция возвращает одно из следующих значений:
- EXCEPTION_ACCESS_VIOLATION – попытка чтения или записи в виртуальную память без соответствующих прав доступа;
- EXCEPTION_BREAKPOINT – встретилась точка останова;
- EXCEPTION_DATATYPE_MISALIGNMENT – доступ к данным, адрес которых не выровнен по границе слова или двойного слова;
- EXCEPTION_SINGLE_STEP – механизм трассировки программы сообщает, что выполнена одна инструкция;
- EXCEPTION_ARRAY_BIUNDS_EXCEEDED – выход за пределы массива, если аппаратное обеспечение поддерживает такую проверку;
- EXCEPTION_FLT_DENORMAL_OPERAND – один из операндов с плавающей точкой является ненормализованным;
- EXCEPTION_FLT_DIVIDE_BY_ZERO – попытка деления на ноль в операции с плавающей точкой;
- EXCEPTION_FLT_INEXACT_RESULT – результат операции с плавающей точкой не может быть точно представлен десятичной дробью;
- EXCEPTION_FLT_INVALID_OPERATION – ошибка в операции с плавающей точкой, для которой не предусмотрены другие коды исключения;
- EXCEPTION_FLT_OVERFLOW – при выполнении операции с плавающей точкой произошло переполнение;
- EXCEPTION_FLT_STACK_CHECK – переполнение или выход за нижнюю границу стека при выполнении операции с плавающей точкой;
- EXCEPTION_FLT_UNDERFLOW – результат операции с плавающей точкой является числом, которое меньше минимально возможного числа с плавающей точкой;
- EXCEPTION_INT_DIVIDE_BY_ZERO – попытка деления на ноль при операции с целыми числами;
- EXCEPTION_INT_OVERFLOW – при выполнении операции с целыми числами произошло переполнение;
- EXCEPTION_PRIV_INSTRUCTION – попытка выполнения привилегированной инструкции процессора, которая недопустима в текущем режиме процессора;
- EXCEPTION_NONCONTINUABLE_EXCEPTION – попытка возобновления исполнения программы после исключения, которое запрещает выполнять такое действие.
Функция
GetExceptionCode может вызываться только в выражении-фильтре или
в блоке обработки исключения. Ниже приведен пример программы, где выполняется
попытка деления на ноль целого числа:
#include
#include
void main()
{
int a = 10;
int b = 0;
__try
{
a = a / b; //ошибка, так как b = 0
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
DWORD err = GetExceptionCode(); //получаем код исключения
if (err == EXCEPTION_INT_DIVIDE_BY_ZERO)
cout << “ERROR: INT DIVIDE BY ZERO” << endl;
else
cout << “Some other exception.” << endl;
}
cout << “a = “ << a << endl;
}
1.3. Функции фильтра
Если есть необходимость более детально обработать информацию об исключении,
то в выражении-фильтре используют функцию, которая в этом случае называется
функцией фильтра. В функции фильтра нельзя вызывать функции
GetExceptionCode и
GetExceptionInformation. Однако эти функции могут вызываться для
инициализации параметров функции фильтра.
Пример программы, в которой используется функция фильтра для принятия решения
о дальнейшей обработке исключения, приведён ниже.
#include
#include
DWORD ff(DWORD ec, int &a)
{
//проверяем код исключения
if (ec == EXCEPTION_INT_DIVIDE_BY_ZERO)
{
cout << “Integer divide by zero exception.” << endl;
cout << “a = “ << a << endl;
a = 10; //восстанавливаем ошибку
//возобновляем выполнение программы
cout << “Continue execution.” << endl;
cout << “a = “ << a << endl;
return EXCEPTION_CONTINUE_EXECUTION;
}
else //прекращаем выполнение программы
return EXCEPTION_EXECUTE_HANDLER;
}
void main()
{
int a = 0;
int b = 1000;
__try
{
b /= a;
cout << “b = “ << b << endl;
}
__except(ff(GetExceptionCode(), a))
{
cout << “There was some exception.” << endl;
}
}
Здесь функция фильтра (ff) возвращает одно из двух значений EXCEPTION_CONTINUE_EXECUTION
или EXCEPTION_EXECUTE_HANDLER. Первое значение возвращается в том случае,
если исключение генерируется системой при целочисленном делении на ноль,
а второе – в остальных случаях. При попытке деления на ноль происходит исключение
и в качестве выражения-фильтра применяется результат выполнения функции
ff.
Эта функция проверяет, чем было вызвано исключение, и если это деление на ноль,
то ошибка исправляется (а = 10). Затем функция возвращает значение
EXCEPTION_CONTINUE_EXECUTION, то есть программа продолжает свою работу,
но уже с исправленным значением переменной
a. Если же это исправление
не сделать, то программа войдет в бесконечный цикл.
1.4. Необработанные исключения
Если в программе произошло исключение, для которого не существует обработчика исключений,
то в этом случае вызывается функция-фильтр системного обработчика исключений,
которая выводит на экран окно сообщений с предложением пользователю закончить
программу аварийно или выполнить отладку приложения. Системная функция-фильтр
UnhandledExceptionFilter имеет следующий прототип:
LONG UnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo);
Эта функция имеет один параметр, который указывает на структуру типа
EXCEPTION_INFO и возвращает одно из следующих значений:
- EXCEPTION_CONTINUE_SEARCH – передать управление отладчику приложения;
- EXCEPTION_EXECUTE_HANDLER – передать управление обработчику исключений.
Приложение может заменить системную функцию-фильтр с помощью функции
SetUnhandledExceptionFilter, которая имеет следующий прототип:
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter);
Эта функция возвращает адрес старой функции фильтра или NULL, если установлен
системный обработчик исключений. В качестве параметра эта функция принимает
указатель на новую функцию-фильтр, которая будет установлена вместо системной.
Эта функция-фильтр должна иметь прототип, соответствующий системной функции
фильтра
UnhandledExceptionFilter, и возвращать одно из следующих значений:
- EXCEPTION_EXECUTE_HANDLER – выполнение программы прекращается;
- EXCEPTION_CONTINUE_EXECUTION – возобновить исполнение программы с точки исключения;
- EXCEPTION_CONTINUE_SEARCH – выполняется системная функция UnhandledExceptionFilter.
Чтобы восстановить системную функцию
UnhandledExceptionFilter, нужно вызвать
функцию
UnhandledExceptionFilter с параметром NULL. Ниже приведена программа,
которая устанавливает новую функцию-фильтр для необработанных исключений,
а затем восстанавливает системную функцию
UnhandledExceptionFilter.
#include
#include
LONG new_filter(PEXCEPTION_POINTERS pExceptionInfo)
{
cout << “New filter-function is called.” << endl;
cout << “Exception code = “ << hex
<< pExceptionInfo->ExceptionRecord->ExceptionCode << endl;
return EXCEPTION_EXECUTE_HANDLER;
}
void main()
{
int *p = NULL;
LPTOP_LEVEL_EXCEPTION_FILTER old_filter;
//устанавливаем новую функцию-фильтр необработанных исключений
old_filter =
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)new_filter);
//выводим адрес старой функции-фильтра
cout << “Old filter-function address = “ << hex << old_filter << endl;
*p = 10; //создаем необработанное исключение
cout << “The END.” << endl;
}
Здесь мы устанавливаем новую функцию-фильтр, поэтому программа не завершается аварийно.
А завершается она нормально при попытке записать число 10 в несуществующий адрес,
то есть строки “The END” после завершения программы мы не увидим.
1.5. Обработка исключений при операциях с плавающей точкой
По умолчанию система отключает все исключения с плавающей точкой. Поэтому если
при выполнении операции с плавающей точкой было получено число, которое не входит
в диапазон представления чисел с плавающей точкой, то в результате система вернет
NAN или INFINITY в случае слишком малого или слишком большого числа соответственно.
Чтобы включить режим генерации исключений с плавающей точкой нужно изменить
состояние слова, управляющего обработкой операций с плавающей точкой. Это можно
сделать при помощи функции
_controlfp, которая имеет следующий прототип:
unsigned int _controlfp(unsigned int new,
unsigned int mask);
Прототип определен в заголовочном файле
float.h. Эта функция возвращает
старое слово, управляющее обработкой исключений. Параметр new задает новое
управляющее слово, а параметр mask должен принимать значение _MCW_EM.
Если значение этого параметра равно 0, то функция возвращает старое управляющее слово.
В параметре
new для управления исключениями можно сбрасывать или
устанавливать следующие значения:
- _EM_INVALID – исключение EXCEPTION_FLT_INVALID_OPERATION;
- _EM_DENORMAL – исключение EXCEPTION_FLT_DENORMAL_OPERAND;
- _EM_ZERODIVIDE – исключение EXCEPTION_FLT_DIVIDE_BY_ZERO;
- _EM_OVERFLOW – исключение EXCEPTION_FLT_OVERFLOW;
- _EM_UNDERFLOW – исключение EXCEPTION_FLT_UNDERFLOW;
- _EM_INEXACT – исключение EXCEPTION_FLT_INEXACT_RESULT.
Если бит, соответствующий одному из этих исключений, сброшен, то система
генерирует соответствующее исключение с плавающей точкой, иначе исключение не генерируется.
Ниже приведен пример программы, которая обрабатывает исключение с плавающей точкой
при делении на ноль.
#include
#include
#include
void main()
{
double a = 0;
int cw = _controlfp(0, 0); //получить управляющее слово, заданное по умолчанию
//разрешить обработку исключений с плавающей точкой
cw &=~(EM_OVERFLOW | EM_UNDERFLOW | EM_INEXACT | EM_ZERODIVIDE | EM_DENORMAL);
_controlfp(cw, _MCW_EM); //установить новое управляющее слово
//теперь можно обрабатывать исключения
__try
{
double b = 100;
b = b/a; //ошибка, деление на 0
cout << “b/a = “ << b << endl;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
DWORD ec = GetExceptionCode(); //получаем код исключения
if (ec == EXCEPTION_FLT_DIVIDE_BY_ZERO)
cout << “Exception float divide by zero.” << endl;
else
cout << “Some other exception.” << endl;
}
}
Все это прекрасно работает в консольных приложениях, а вот добиться нормальной
работы обработчика исключений с плавающей точкой в MFC-приложениях мне так и не
удалось. Можно, конечно, заменить системный обработчик исключений, как это описано
в п.1.4, но тогда программа будет завершаться при возникновении исключения
(хотя и не аварийно, то есть без надоевшего всем вопроса Windows XP об
отправке отчета в компанию Microsoft). В общем, пришлось мне для обработки
исключений с плавающей точкой воспользоваться еще одним способом. Этот способ
более трудоемок, зато работает. Хотя во многих случаях проще проверять значения
с помощью оператора
if, не прибегая к «хитромудрым» способам обработки исключений в Visual C++.
1.6. Использование блоков try и catch
Обработка исключений в Visual C++ может быть реализована тремя следующими фрагментами кода:
- Блок try отмечает участок текста программы, где возможно возникновение ошибки.
- Блок catch следует непосредственно за блоком try и содержит операторы обработки обнаруженной ошибки.
- Оператор throw используется для передачи сообщения об ошибке в вызывающую часть программы. Принято говорить, что оператор throw выбрасывает исключение.
Ниже приведен пример использования такого обработчика исключений.
#include
void main()
{
float a = 100, b = 0;
try
{
if (b == 0) throw(“ERROR!!!”);
else a /= b;
cout << “a = “ << a << endl;
}
catch(char* err)
{
cout << err << endl;
}
}
Если в блоке
try управление будет передано оператору
throw, то дальнейшее
выполнение блока
try прекратится и программа передаст управление блоку
catch.
В приведенном выше примере это случится в том случае, если значение переменной
b
будет равно нулю. В реальных программах, как правило, требуется обрабатывать
большее количество данных. Для этого можно создавать объекты исключений,
которые обрабатывают все возможные ситуации (попытки деления на ноль, выход за
пределы диапазона и т.п.).
2. Финальная обработка исключений
2.1. Финальные блоки фрейма
В операционных системах Windows существует еще один способ обработки исключений.
При этом способе код, где возможно возникновение исключения, также заключается в
блок
__try. Но теперь за этим блоком следует блок
__finally. В таком случае
блок
__finally выполняется всегда – независимо от того, произошло исключение
или нет. Такой способ обработки исключений называется
финальная обработка
исключений. Структурно финальная обработка выглядит следующим образом:
__try
{
//Охраняемый код
}
__finally
{
//Финальный код
}
Финальная обработка исключений используется для того, чтобы при любом исходе
исполнения блока
__try освободить ресурсы (память, файлы и т.п.),
которые были захвачены внутри этого блока.
Недостатком такого метода является то, что финальный код будет выполняться в
любом случае. А это не всегда хорошо. Например, если мы пытаемся освободить память,
которая распределяется в блоке
__try, то это может привести к ошибке,
если до распределения памяти дело не дошло (исключение произошло раньше).
Чтобы избежать такой ситуации, нужно проверить, как завершился блок
__try – нормально или нет.
2.2. Проверка завершения фрейма
Управление из блока
__try может быть передано одним из следующих способов:
- Нормальное завершение блока.
- Выход из блока при помощи управляющей инструкции __leave.
- Выход из блока при помощи одной из управляющих инструкций return, break, continue или goto.
- Передача управления обработчику исключения.
В первых двух случаях считается, что блок
__try завершился нормально,
в остальных – ненормально.
Чтобы определить, как завершился блок
__try, используется функция
AbnormalTermination, которая имеет следующий прототип:
BOOL AbnormalTermination(VOID);
В случае если блок
__try завершился ненормально, эта функция возвращает
ненулевое значение, иначе – значение FALSE. Используя эту функцию, ресурсы, захваченные в
блоке
__try, можно освобождать в зависимости от ситуации. Пример:
#include
#include
void main()
{
int *p = NULL; //пустой указатель на целое число
__try
{
*p = 10; //исключение, т.к. память не распределена
}
__finally
{
if (!AbnormalTermination()) //если блок __try закончился нормально
{
delete p; //то освобождаем память
cout << “The memory is free.” << endl;
}
else //иначе нечего освобождать
cout << “The memory was not allocated.” << endl;
}
}