Обработка исключений в 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.
|