Обработка исключений в C++
Введение
Язык С представляет программисту очень ограниченные возможности обработки исключений,
возникших при работе программы. В этом отношении С++ намного развитее С. Здесь
у программиста существенно большие возможности по непосредственной обработке
исключений. Комитет по разработке стандартов С++ предоставил очень простую,
но мощную форму обработки исключений.
Темные дни С
Типичная функция, написанная на С, выглядит примерно так:
long DoSomething()
{
long *a, c;
FILE *b;
a = malloc(sizeof(long) * 10);
if (a == NULL)
return 1;
b = fopen("something.bah", "rb");
if (b == NULL) {
free(a);
return 2;
}
fread(a, sizeof(long), 10, b);
if (a[0] != 0x10) {
free(a);
fclose(b);
return 3;
}
fclose(b);
c = a[1];
free(a);
return c;
}
Выглядит не очень, не так ли? Вы целиком и полностью зависите от значений,
которые возвращают вам функции и для каждой ошибки вам постоянно нужен код,
который ее обрабатывает. Если вы, скажем, в функции работаете хотя бы с 10 указателями
(рапределяете память, освобождаете ее и т.д.), то наверняка половину кода функции
будет занимать код обработки ошибок. Такая же ситуация будет в коде, вызывающем
эту функцию, так как здесь также нужно обработать все возвращаемые коды ошибок.
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 может "ловить" данные любого типа, но
вовсе не обязательно при это указывать переменную. Т.е. прекрасно будет работать
что-нибудь типа этого:
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.
Приложение
Приведем пример, как все вышеизложенное может быть использовано в конкретном
приложении. Преположим, у вас в программе есть класс cMain и экземпляр этого
класса Main:
class cMain { public: bool Setup(); bool Loop(); // Основной цикл программы void Close(); }; cMain Main;
А в функции main() или WinMain() вы можете использовать этот класс как-нибудь
так:
try
{
Main.Setup();
Main.Loop();
Main.Close();
}
catch (Exception &e)
{
// использование класса, ведущего лог.
log("Exception thrown: %s", e.String());
// Показываем сообщение об ошибке и закрываем приложение.
}
Основной цикл программы может выглядеть примерно так:
while (AppActive) { try { // какие-то действия } catch (Exception &e) { /* Если исключение критическое, типа ошибки памяти, посылаем исключение дальше, в main(), оператором throw e; или просто throw. Если исключение некритично, обрабатываем его и возвращаемся в основной цикл. */ } }
Заключение
Метод обработки исключений, приведенный в статье, является удобным и мощным
средством, однако только вам решать, использовать его или нет. Одно можно скачать
точно - приведенный метод облегчит вам жизнь. Если хотите узнать об исключениях
чуть больше, посмотрите публикацию Deep
C++
на сервере MSDN.
|