Исключения в C++: типы, синтаксис и обработка
Разберёмся, для чего нужны исключения в языке C++ и какими они бывают. Изучим синтаксис выбрасывания, обработки и рассмотрим особые случаи.
Поговорим об исключениях в C++, начиная определением и заканчивая грамотной обработкой.
- Инструмент программирования для исключительных ситуаций
- Исключения: панацея или нет
- Синтаксис исключений в C++
- Базовые исключения стандартной библиотеки
- Заключение
Георгий Осипов
Один из авторов курса «Разработчик C++» в Яндекс Практикуме, разработчик в Лаборатории компьютерной графики и мультимедиа ВМК МГУ
Исключения — важный инструмент в современном программировании. В большинстве источников тема исключений раскрывается не полностью: не описана механика их работы, производительность или особенности языка C++.
В статье я постарался раскрыть тему исключений достаточно подробно. Она будет полезна новичкам, чтобы узнать об исключениях, и программистам с опытом, чтобы углубиться в явление и достичь его полного понимания.
Статья поделена на две части. Первая перед вами и содержит базовые, но важные сведения. Вторая выйдет чуть позже. В ней — информация для более продвинутых разработчиков.
В первой части разберёмся:
- для чего нужны исключения;
- особенности C++;
- синтаксис выбрасывания и обработки исключений;
- особые случаи, связанные с исключениями.
Также рассмотрим основные стандартные типы исключений, где и для чего они применяются.
Мы опираемся на современные компиляторы и Стандарт C++20. Немного затронем C++23 и даже C++03.
Если вы только осваиваете C++, возможно, вам будет интересен курс «Разработчик C++» в Яндекс Практикуме. У курса есть бесплатная вводная часть. Именно она может стать вашим первым шагом в мир C++. Для тех, кто знаком с программированием, есть внушительная ознакомительная часть, тоже бесплатная.
Инструмент программирования для исключительных ситуаций
В жизни любой программы бывают моменты, когда всё идёт не совсем так, как задумывал разработчик. Например:
- в системе закончилась оперативная память;
- соединение с сервером внезапно прервалось;
- пользователь выдернул флешку во время чтения или записи файла;
- понадобилось получить первый элемент списка, который оказался пустым;
- формат файла не такой, как ожидалось.
Примеры объединяет одно: возникшая ситуация достаточно редка, и при нормальной работе программы, всех устройств, сети и адекватном поведении пользователя она не возникает.
Хороший программист старается предусмотреть подобные ситуации. Однако это бывает сложно: перечисленные проблемы обладают неприятным свойством — они могут возникнуть практически в любой момент.
На помощь программисту приходят исключения (exception). Так называют объекты, которые хранят данные о возникшей проблеме. Механизмы исключений в разных языках программирования очень похожи. В зависимости от терминологии языка исключения либо выбрасывают (throw), либо генерируют (raise). Это происходит в тот момент, когда программа не может продолжать выполнять запрошенную операцию.
После выбрасывания в дело вступает системный код, который ищет подходящий обработчик. Особенность в том, что тот, кто выбрасывает исключение, не знает, кто будет его обрабатывать. Может быть, что и вовсе никто — такое исключение останется сиротой и приведёт к падению программы.
Если обработчик всё же найден, то он ловит (catch) исключение и программа продолжает работать как обычно. В некоторых языках вместо catch используется глагол except (исключить).
Обработчик ловит не все исключения, а только некоторые — те, что возникли в конкретной части определённой функции. Эту часть нужно явно обозначить, для чего используют конструкцию try (попробовать). Также обработчик не поймает исключение, которое ранее попало в другой обработчик. После обработки исключения программа продолжает выполнение как ни в чём не бывало.
Исключения: панацея или нет
Перед тем как совершить операцию, нужно убедиться, что она корректна. Если да — совершить эту операцию, а если нет — выбросить исключение. Так делается в некоторых языках, но не в C++. Проверка корректности — это время, а время, как известно, деньги. В C++ считается, что программист знает, что делает, и не нуждается в дополнительных проверках. Это одна из причин, почему программы на C++ такие быстрые.
Но за всё нужно платить. Если вы не уследили и сделали недопустимую операцию, то в менее производительных языках вы получите исключение, а в C++ — неопределённое поведение. Исключение можно обработать и продолжить выполнение программы. Неопределённое поведение гарантированно обработать нельзя.
Но некоторые виды неопределённого поведения вполне понятны и даже могут быть обработаны. Это зависит от операционной системы:
- сигналы POSIX — низкоуровневые уведомления, которые отправляются программе при совершении некорректных операций и в некоторых других случаях;
- структурированные исключения Windows (SEH) — специальные исключения, которые нельзя обработать средствами языка.
Особенность C++ в том, что не любая ошибка влечёт исключение, и не любую ошибку можно обработать. Но если для операции производительность не так критична, почему бы не сделать проверку?
У ряда операций в C++ есть две реализации. Одна супербыстрая, но вы будете отвечать за корректность, а вторая делает проверку и выбрасывает исключение в случае ошибки. Например, к элементу класса std::vector можно обратиться двумя способами:
- vec[15] — ничего не проверяет. Если в векторе нет элемента с индексом 15, вы получаете неопределённое поведение. Это может быть сигнал SIGSEGV, некорректное значение или взрыв компьютера.
- vec.at(15) — то же самое, но в случае ошибки выбрасывается исключение, которое можно обработать.
В C++ вам даётся выбор: делать быстро или делать безопасно. Часто безопасность важнее, но в определённых местах программы любое промедление критично.
Синтаксис исключений в C++
Ловим исключения
Начнём с примера:
void SomeFunction() < DoSomething0(); try < SomeClass var; DoSomething1(); DoSomething2(); // ещё код cout catch(ExceptionType e) < std::cout std::cout
В примере есть один try -блок и один catch -блок. Если в блоке try возникает исключение типа ExceptionType , то выполнение блока заканчивается. При этом корректно удаляются созданные объекты — в данном случае переменная var . Затем управление переходит в конструкцию catch . Сам объект исключения передаётся в переменную e . Выводя e.what() , мы предполагаем, что у типа ExceptionType есть метод what .
Если в блоке try возникло исключение другого типа, то управление также прервётся, но поиск обработчика будет выполняться за пределами функции SomeFunction — выше по стеку вызовов. Это также касается любых исключений, возникших вне try -блока.
Во всех случаях объект var будет корректно удалён.
Исключение не обязано возникнуть непосредственно внутри DoSomething*() . Будут обработаны исключения, возникшие в функциях, вызванных из DoSomething* , или в функциях, вызванных из тех функций, да и вообще на любом уровне вложенности. Главное, чтобы исключение не было обработано ранее.
Ловим исключения нескольких типов
Можно указать несколько блоков catch , чтобы обработать исключения разных типов:
void SomeFunction() < DoSomething0(); try < DoSomething1(); DoSomething2(); // ещё код >catch(ExceptionType1 e) < std::cout catch(ExceptionType2 e) < std::cout // ещё код >
Ловим все исключения
void SomeFunction() < DoSomething0(); try < DoSomething1(); DoSomething2(); // ещё код >catch(. ) < std::cout // ещё код >
Если перед catch(. ) есть другие блоки, то он означает «поймать все остальные исключения». Ставить другие catch -блоки после catch(. ) не имеет смысла.
Перебрасываем исключение
Внутри catch(. ) нельзя напрямую обратиться к объекту-исключению. Но можно перебросить тот же объект, чтобы его поймал другой обработчик:
void SomeFunction() < DoSomething0(); try < DoSomething1(); DoSomething2(); // ещё код >catch(. ) < std::cout // ещё код >
Можно использовать throw в catch -блоках с указанным типом исключения. Но если поместить throw вне блока catch , то программа тут же аварийно завершит работу через вызов std::terminate() .
Перебросить исключение можно другим способом:
std::rethrow_exception(std::current_exception())
Этот способ обладает дополнительным преимуществом: можно сохранить исключение и перебросить его в другом месте. Однако результат std::current_exception() — это не объект исключения, поэтому его можно использовать только со специализированными функциями.
Принимаем исключение по ссылке
Чтобы избежать лишних копирований, можно ловить исключение по ссылке или константной ссылке:
void SomeFunction() < DoSomething0(); try < DoSomething1(); DoSomething2(); // ещё код >catch(ExceptionType& e) < std::cout catch(const OtherExceptionType& e) < std::cout >
Это особенно полезно, когда мы ловим исключение по базовому типу.
Выбрасываем исключения
Чтобы поймать исключение, нужно его вначале выбросить. Для этого применяется throw.
Если throw используется с параметром, то он не перебрасывает исключение, а выбрасывает новое. Параметр может быть любого типа, даже примитивного. Использовать такую конструкцию разрешается в любом месте программы:
void ThrowIfNegative(int x) < if (x < 0) < // выбрасываем исключение типа int throw x; >> int main() < try < ThrowIfNegative(10); ThrowIfNegative(-15); ThrowIfNegative(0); cout // ловим выброшенное исключение catch(int x) < cout >
Вывод: «Поймано исключение типа int, содержащее число –15».
Создаём типы для исключений
Выбрасывать int или другой примитивный тип можно, но это считается дурным тоном. Куда лучше создать специальный тип, который будет использоваться только для исключений. Причём удобно для каждого вида ошибок сделать отдельный класс. Он даже не обязан содержать какие-то данные или методы: отличать исключения друг от друга можно по названию типа.
class IsZeroException<>; struct IsNegativeException<>; void ThrowIfNegative(int x) < if (x < 0) < // Выбрасывается не тип, а объект. // Не забываем скобки, чтобы создать объект заданного типа: throw IsNegativeException(); >> void ThrowIfZero(int x) < if (x == 0) < throw IsZeroException(); >> void ThrowIfNegativeOrZero(int x) < ThrowIfNegative(x); ThrowIfZero(x); >int main() < try < ThrowIfNegativeOrZero(10); ThrowIfNegativeOrZero(-15); ThrowIfNegativeOrZero(0); >catch(IsNegativeException x) < cout catch(IsZeroException x) < cout >
В итоге будет напечатана только фраза: «Найдено отрицательное число», поскольку –15 проверено раньше нуля.
Ловим исключение по базовому типу
Чтобы поймать исключение, тип обработчика должен в точности совпадать с типом исключения. Например, нельзя поймать исключение типа int обработчиком типа unsigned int .
Но есть ситуации, в которых типы могут не совпадать. Про одну уже сказано выше: можно ловить исключение по ссылке. Есть ещё одна возможность — ловить исключение по базовому типу.
Например, чтобы не писать много catch -блоков, можно сделать все используемые типы исключений наследниками одного. В этом случае рекомендуется принимать исключение по ссылке.
class NumericException < public: virtual std::string_view what() const = 0; >// Класс — наследник NumericException. class IsZeroException : public NumericException < public: std::string_view what() const override < return "Обнаружен ноль"; >> // Ещё один наследник NumericException. class IsNegativeException : public NumericException < public: std::string_view what() const override < return "Обнаружено отрицательное число"; >> void ThrowIfNegative(int x) < if (x < 0) < // Выбрасывается не тип, а объект. // Не забываем скобки, чтобы создать объект заданного типа: throw IsNegativeException(); >> void ThrowIfZero(int x) < if (x == 0) < throw IsZeroException(); >> void ThrowIfNegativeOrZero(int x) < ThrowIfNegative(x); ThrowIfZero(x); >int main() < try < ThrowIfNegativeOrZero(10); ThrowIfNegativeOrZero(-15); ThrowIfNegativeOrZero(0); >// Принимаем исключение базового типа по константной ссылке (&): catch(const NumericException& e) < std::cout >
Выбрасываем исключение в тернарной операции ?:
Напомню, что тернарная операция ?: позволяет выбрать из двух альтернатив в зависимости от условия:
std::cout = 18 ? "Проходите" : "Извините, вход в бар с 18 лет")
Оператор throw можно использовать внутри тернарной операции в качестве одного из альтернативных значений. Например, так можно реализовать безопасное деление:
int result = y != 0 ? x / y : throw IsZeroException();
Это эквивалентно такой записи:
int result; if (y != 0) < result = x / y; >else
Согласитесь, первый вариант лаконичнее. Так можно выбрасывать несколько исключений в одном выражении:
// Вычислим корень отношения чисел: int result = y == 0 ? throw IsZeroException() : x / y < 0 ? throw IsNegativeException() : sqrt(x / y);
Вся функция — try-блок
Блок try может быть всем телом функции:
int SomeFunction(int x) try < return DoSomething(x); >catch(ExceptionType e) < std::cout
Тут мы просто опустили фигурные скобки функции. По-другому можно записать так:
int SomeFunction(int x) < try < return DoSomething(x); >catch(ExceptionType e) < std::cout >
Исключения в конструкторе
Есть как минимум два случая возникновения исключений в конструкторе объекта:
- Внутри тела конструктора.
- При конструировании данных объекта.
В первом случае исключение ещё можно поймать внутри тела конструктора и сделать вид, как будто ничего не было.
Как в С++ обрабатывать ошибки в конструкторах без исключений?
Во втором случае исключение тоже можно поймать, если использовать try-блок в качестве тела конструктора. Однако тут есть особенность: сделать вид, что ничего не было, не получится. Объект всё равно будет считаться недоконструированным:
class IsZeroException<>; // Функция выбросит исключение типа IsZeroException // если аргумент равен нулю. void ThrowIf0(int x) < if (x == 0) < throw IsZeroException(); >> // Класс содержит только одно число. // Он выбрасывает исключение в конструкторе, если число нулевое. class NotNullInt < public: NotNullInt(int x) : x_(x) < ThrowIf0(x_); >private: int x_; > class Ratio < public: // Инициализаторы пишем после try: Ratio(int x, int y) try : x_(x), y_(y) < >catch(IsZeroException e) < std::cout private: int x_; NotNullInt y_; >; int main() < Ratio(10, 15); try < Ratio(15, 0); >catch(. ) < std::cout >
Тут мы увидим оба сообщения: «Знаменатель дроби не может быть нулём» и «Дробь не построена».
Если объект недоконструирован, то его деструктор не вызывается. Это логичная, но неочевидная особенность языка. Однако все полностью построенные члены – данные объекта будут корректно удалены:
#include class A < public: A() < std::cout ~A() < std::cout private: > class B < public: B() < std::cout ~B() < // Этой надписи мы не увидим: std::cout private: A a; >; int main() < try < B b; >catch (. ) < >>
Запустим код и увидим такой вывод:
A constructed B constructed A destructed
Объект типа A создался и удалился, а объект типа B создался не до конца и поэтому не удалился.
Не все исключения в конструкторах можно обработать. Например, нельзя поймать исключения, выброшенные при конструировании глобальных и thread_local объектов, — в этом случае будет вызван std::terminate .
Исключения в деструкторе
В этом разделе примера не будет, потому что исключения в деструкторе — нежелательная практика. Бывает, что язык удаляет объекты вынужденно, например, при поиске обработчика выброшенного исключения. Если во время этого возникнет другое исключение в деструкторе какого-то объекта, то это приведёт к вызову std::terminate .
Более того, по умолчанию исключения в деструкторе запрещены и всегда приводят к вызову std::terminate . Выможете разрешить их для конкретного конструктора — об этом я расскажу в следующей части — но нужно много раз подумать, прежде чем сделать это.
Обрабатываем непойманные исключения
Поговорка «не пойман — не вор» для исключений не работает. Непойманные исключения приводят к завершению программы через std::terminate . Это нештатная ситуация, но можно предотвратить немедленное завершение, добавив обработчик для std::terminate :
int main() < // Запишем обработчик в переменную terminate_handler auto terminate_handler = []() < auto e_ptr = std::current_exception(); if (e_ptr) < try < // Перебросим исключение: std::rethrow_exception(e_ptr); >catch (const SomeType& e) < std::cerr catch (. ) < std::cerr > else < std::cerr // Всё равно завершим программу. std::abort(); >; // Установим обработчик для функции terminate std::set_terminate(terminate_handler); // ….. >
Однако не стоит надеяться, что программа после обработки такой неприятной ситуации продолжит работу как ни в чём не бывало. std::terminate — часть завершающего процесса программы. Внутри него доступен только ограниченный набор операций, зависящий от операционной системы.
Остаётся только сохранить всё, что можно, и извиниться перед пользователем за неполадку. А затем выйти из программы окончательно вызовом std::abort() .
Базовые исключения стандартной библиотеки
Далеко не всегда есть смысл создавать новый тип исключений, ведь в стандартной библиотеке их и так немало. А если вы всё же создаёте свои исключения, то сделайте их наследниками одного из базовых. Рекомендуется делать все типы исключений прямыми или косвенными наследниками std::exception .
Обратим внимание на одну важную вещь. Все описываемые далее классы не содержат никакой магии. Это обычные и очень простые классы, которые вы могли бы реализовать и самостоятельно. Использовать их можно и без throw , однако смысла в этом немного.
Их особенность в том, что разработчики договорились использовать эти классы для описания исключений, генерируемых в программе. Например, этот код абсолютно корректен, но совершенно бессмысленен:
#include #include int main() < // Используем std::runtime_error вместо std::string. // Но зачем? std::runtime_error err("Буря мглою небо кроет"); std::cout
Разберём основные типы исключений, описанные в стандартной библиотеке C++.
std::exception
Базовый класс всех исключений стандартной библиотеки. Конструктор не принимает параметров. Имеет метод what() , возвращающий описание исключения. Как правило, используются производные классы, переопределяющие метод what() .
std::logic_error : public std::exception
Исключение типа logic_error выбрасывается, когда нарушены условия, сформулированные на этапе написания программы. Например, мы передали в функцию извлечения квадратного корня отрицательное число или попытались извлечь элемент из пустого списка.
Конструктор принимает сообщение в виде std::string , которое будет возвращаться методом what() .
// класс копилка class Moneybox < public: void WithdrawCoin() < if (coins_ == 0) < throw std::logic_error("В копилке нет денег"); >--coins_; > void PutCoin() < ++coins_; >private: int coins_ = 0; >
Перечислим некоторые производные классы std::logic_error . У всех них похожий интерфейс.
- std::invalid_argument. Исключение этого типа показывает, что функции передан некорректный аргумент, не соответствующий условиям.
double GetSqrt(double x) < return x >= 0 ? sqrt(x) : throw std::invalid_argument("Попытка извлечь квадратный корень из отрицательного числа"); >
Это исключение выбрасывают функции преобразования строки в число, такие как stol , stof , stoul , а также конструктор класса std::bitset :
try < int f = std::stoi("abracadabra"); >catch (std::invalid_argument& ex)
- std::length_error. Исключение говорит о том, что превышен лимит вместимости контейнера. Может выбрасываться из методов, меняющих размер контейнеров string и vector . Например resize , reserve , push_back .
- std::out_of_range. Исключение говорит о том, что некоторое значение находится за пределами допустимого диапазона. Возникает при использовании метода at практически всех контейнеров. Также возникает при использовании функций конвертации в строки в число, таких как stol , stof , stoul . В стандартной библиотеке есть исключение с похожим смыслом — std::range_error .
std::runtime_error : public std::exception
std::runtime_error — ещё один базовый тип для нескольких видов исключений. Он говорит о том, что исключение относится скорее не к предусмотренной ошибке, а к выявленной в процессе выполнения.
При этом, если std::logic_error подразумевает конкретную причину ошибки — нарушение конкретного условия, — то std::runtime_error говорит о том, что что-то идёт не так, но первопричина может быть не вполне очевидна.
Интерфейс такой же, как и у logic_error : класс принимает описание ошибки в конструкторе и переопределяет метод what() базового класса std::exception .
class CommandLineParsingError : public std::runtime_error < public: // этой строкой импортируем конструктор из базового класса: using runtime_error::runtime_error; >; class ZeroDenominatorError : public std::runtime_error < public: // используем готовое сообщение: ZeroDenominatorError() : std::runtime_error("Знаменатель не может быть нулём") < >>
Рассмотрим некоторые важные производные классы:
- std::regex_error. Исключение, возникшее в процессе работы с регулярными выражениями. Например, при неверном синтаксисе регулярного выражения.
- std::system_error. Широкий класс исключений, связанных с потоками, вводом-выводом или файловой системой.
- std::format_error. Исключение, возникшее при работе функции std::format .
std::bad_alloc : public std::exception
У std::exception есть и другие наследники. Самый важный — std::bad_alloc . Его может выбрасывать операция new. Это исключение — слабое место многих программ и головная боль многих разработчиков, ведь оно может возникать практически везде — в любом месте, где есть динамическая аллокация. То есть при:
- вставке в любой контейнер;
- копировании любого контейнера, например, обычной строки;
- создании умного указателя unique_ptr или shared_ptr;
- копировании объекта, содержащего контейнер;
- прямом вызове new (надеемся, что вы так не делаете);
- работе с потоками ввода-вывода;
- работе алгоритмов;
- вызове корутин;
- в пользовательских классах и библиотеках — практически при любых операциях.
При обработке bad_alloc нужно соблюдать осторожность и избегать других динамических аллокаций.
#include #include #include #include int main() < std::vector vec; try < while (true) < vec.push_back(std::string(10000000, 'a')); >> catch (const std::bad_alloc& e) < std::cout >
Возможный вывод: «Место закончилось после вставки 2640 элементов».
При аллокациях возможна также ошибка std::bad_array_new_length , производная от bad_alloc . Она возникает при попытке выделить слишком большое, слишком маленькое (меньше, чем задано элементов для инициализации) либо отрицательное количество памяти.
Также при аллокации можно запретить new выбрасывать исключение. Для этого пишем (std::nothrow) после new :
int main() < int* m = new (std::nothrow) int [0xFFFFFFFFFFFFFFULL]; std::cout
В случае ошибки операция будет возвращать нулевой указатель.
bad_alloc настолько сложно учитывать, что многие даже не пытаются это делать. Мотивация такая: если память закончилась, то всё равно программе делать уже нечего. Лучше поскорей вызвать std::terminate и завершиться.
Заключение
В этой части мы разобрали, как создавать исключения C++, какие они бывают и как с ними работать. Разобрали ключевые слова try , catch и throw .
В следующей части запустим бенчмарк, разберём гарантии безопасности, спецификации исключений, а также узнаем, когда нужны исключения, а когда можно обойтись без них. И главное — узнаем, как они работают.
Исключения не так просты, как кажутся на первый взгляд. Они нарушают естественный ход программы и кратно увеличивают количество возможных путей исполнения. Но без них ещё сложнее.
C++ позволяет выразительно обрабатывать исключения, он аккуратен при удалении всех объектов и освобождении ресурсов. Будьте аккуратны и вы, и тогда всё получится. Каждому исключению — по обработчику.
Исключения — это лишь одна из многих возможностей C++. Глубже погрузиться в язык и узнать больше о нём, его экосистеме и принципах программирования поможет курс «Разработчик C++».

Следите за новыми постами по любимым темам
Подпишитесь на интересующие вас теги, чтобы следить за новыми постами и быть в курсе событий.
Обработка исключений
Обработка исключений (exception handling) позволяет упорядочить обработку ошибок времени исполнения. Используя обработку исключений С++, программа может автоматически вызвать функцию-обработчик ошибок тогда, когда такая ошибка возникает. Принципиальным достоинством обработки исключений служит то, что она позволяет автоматизировать большую часть кода для обработки ошибок, для чего раньше требовалось ручное кодирование.
Основы обработки исключений
Обработка исключений в С++ использует три ключевых слова: try, catch и throw. Те инструкции программы, где ожидается возможность появления исключительных ситуаций, содержатся в блоке try. Если в блоке try возникает исключение, т. е. ошибка, то генерируется исключение. Исключение перехватывается, используя catch, и обрабатывается. Ниже это общее описание будет рассмотрено более подробно.
Инструкция, генерирующая исключение, должна исполняться внутри блока try. Вызванные из блока try функции также могут генерировать исключения. Всякое исключение должно быть перехвачено инструкцией catch, которая непосредственно следует за инструкцией try, сгенерировавшей исключение. Общая форма блоков try и catch показана ниже:
try // блок try
catch (тип1 аргумент) // блок catch
catch (тип2 аргумент) // блок catch
catch (типЗ аргумент) // блок catch
>
.
catch (типN аргумент) // блок catch
>
Размеры блока try могут изменяться в больших пределах. Например, блок try может содержать несколько инструкций какой-либо функции, либо же, напротив, включать в себя весь код функции main(), так что вся программа будет охвачена обработкой исключений.
Когда исключение сгенерировано, оно перехватывается соответствующей инструкцией catch, обрабатывающей это исключение. Одному блоку try может отвечать несколько инструкций catch, Какая именно инструкция catch исполняется, зависит от типа исключения. Это означает, что если тип данных, указанных в инструкции catch, соответствует типу данных исключения, то только эта инструкция catch и будет исполнена. Когда исключение перехвачено, arg получает ее значение. Перехваченным может быть любой тип данных, включая созданные программистом классы. Если никакого исключения не сгенерировано, то есть никакой ошибки не возникло в блоке try, то инструкции catch выполняться не будут.
Общая форма записи инструкции throw имеет вид:
Инструкция throw должна выполняться либо внутри блока try, либо в функции, вызванной из блока try. В записанном выше выражении исключение обозначает сгенерированное значение.
Если генерируется исключение, для которого отсутствует подходящая инструкция catch, может произойти аварийное завершение программы. При генерации необработанного исключения
вызывается функция terminate(). По умолчанию terminate() вызывает функцию abort(), завершающую выполнение программы. Однако можно задать свою собственную обработку, используя функцию set_terminate(). Подробности можно найти в документации к компилятору.
Ниже представлен пример, иллюстрирующий способ обработки исключений в С++:
Программа выведет на экран следующий текст:
Start
Inside try block
Caught an exception -- value is: 100
End
Рассмотрим внимательнее эту программу. Как можно видеть, блок try содержит три инструкции. За ним следует инструкция catch(int i), обрабатывающая исключения целого типа. В блоке try будут выполняться только две инструкции: первая и вторая — throw. Как только исключение было сгенерировано, управление передается инструкции catch, а блок try прекращает свое исполнение. Таким образом, catch не вызывается. Скорее можно сказать, что к ней переходит исполнение программы. Для этого автоматически осуществляется переустановка стека. Таким образом, инструкция после инструкции throw никогда не выполняется.
Обычно код в инструкции catch пытается исправить ошибку путем выполнения подходящих действий. Если ошибку удалось исправить, то выполнение продолжается с инструкции, непосредственно следующей за catch. Однако иногда не удается справиться с ошибкой, и блок catch завершает программу путем вызова функции exit() или функции abort().
Как отмечалось, тип исключения должен соответствовать указанному в инструкции типу. Например, в предыдущем примере если изменить тип инструкции catch на double, то исключение не будет перехвачено и произойдет аварийное завершение программы. Такое изменение показано ниже:
Эта программа выдаст следующий результат, поскольку исключение целого типа не будет перехвачено инструкцией catch (double i):
Start
Inside try block
Abnormal program termination
Исключение может также быть сгенерировано из функции, вызванной изнутри блока try. В качестве примера рассмотрим следующую программу:
Эта программа выдаст следующий результат:
Start
Inside try block
Inside Xtest, test is: 0
Inside Xtest, test is: 1
Caught an exception -- value is: 1
End
Блок try может быть локализован в какой-то функции. В таком случае всякий раз при входе в функцию начинается обработка исключений. В качестве примера рассмотрим следующую программу:
#include
// try/catch могут находиться в функции вне main()
void Xhandler(int test)
try if (test) throw test;
>
catch(int i) cout >
>
int main()
cout Xhandler(1);
Xhandler(2);
Xhandler(0);
Xhandler(3);
cout return 0;
>
Эта программа выдаст на экран следующий текст:
Start
Caught Exception #: 1
Caught Exception #: 2
Caught Exception #: 3
End
Как видно, сгенерировано три исключения. После каждого исключения функция возвращает управление в функцию main. При каждом новом вызове функции возвращается обработка исключений.
Важно понимать, что код, ассоциированный с инструкцией catch, будет исполняться только тогда, когда перехвачено исключение. В противном случае выполнение программы просто обойдет инструкцию catch.
Как выбросить исключение в c
(C) Dale, 01.02.2011 — 02.02.2011.
Язык C сегодня — довольно неоднозначное явление. Ему уже около 40 лет, а он до сих пор в строю. Второго такого долгожителя вряд ли сыщешь — даже Fortran-IV сдал позиции. Этот язык стал родоначальником множества других, включая столь популярные сегодня C++, Java и C#, по при этом не торопится на пенсию — хотя потомки изрядно его потеснили, но так и не вытеснили полностью.
Конечно, язык, основы которого закладывались еще в конце 1960-х годов, не может быть безупречным по сегодняшним меркам. Хотя он и развивался все это время, о чем свидетельствует последовательность стандартов языка, но в узких рамках совместимости с предыдущими версиями (не забываем, что главный конек C — это совместимость и переносимость), то есть умеренно. Поэтому у него до сих пор есть ряд очевидных слабостей, которые приходится преодолевать программистам.
Сегодня использование C можно было бы рассматривать как архаизм, если бы не одна ниша, в которой ему просто нет равных — программирование микропроцессоров. Сочетание машинной независимости с гибкостью непосредственной работы с аппаратными ресурсами и эффективностью объектного кода делают его незаменимым инструментом разработки firmware, а недостатки преодолеваются различными трюками. Один из таких трюков мы рассмотрели подробно в статье « Сопрограммы в языке программирования C », а сегодня познакомимся поближе с другим.
Один из наиболее скользких вопросов программирования на C — это, безусловно, обработка ошибок. Вообще говоря, обработка ошибок — это показатель мастерства и профессиональной зрелости программиста. Плохие программисты пишут программы, которые работают неправильно. Посредственные — пишут программы, которые работают правильно, но в тепличных условиях (оборудование исправно, все файлы на своем месте, данные имеют корректный формат и т.д.). Настоящие мастера пишут программы, которые надежны в любых условиях — если все гладко, они выдают правильный результат, а если по ходу работы возникли проблемы, они либо пытаются бороться с ними по мере возможностей, либо выдают внятную и недвусмысленную диагностику с описанием причин, по которым работа невозможна.
Прямо скажем, «чистый» C — плохой помощник разработчику по части обработки ошибок. В самом языке для этого нет никаких средств, а в стандартных библиотеках нет единодушия по вопросу, каким образом функция должна сообщать клиенту об ошибках при выполнении. Иногда функция возвращает код завершения, он же код ошибки, если имеет ненулевое значение; в других случаях часть диапазона значений функций выделена под коды ошибки; и апофеоз — errno.
Тут впору вспомнить Дейкстру: «. когда я начинаю анализировать свои собственные мыслительные привычки и привычки моих друзей, я прихожу, нравится мне это или нет, к совершенно иному заключению, а именно: инструменты, которые мы пытаемся использовать, и язык и обозначения, которые мы используем для выражения или записи наших мыслей, являются основным фактором, который определяет, о чем мы вообще можем мыслить и что можем выразить! Анализ влияния, которое язык программирования оказывает на своих пользователей, и признание факта, что к настоящему времени мощь нашего мозга является наиболее скудным ресурсом, совместно дают нам новый набор эталонов для сравнения относительных достоинств различных языков программирования» (из статьи « Смиренный программист »). Язык C — не исключение: вызвав потенциально чреватую ошибками функцию, следует проверить код ее завершения (или errno) и запрограммировать действия в случае ошибки; поскольку эти действия сами могут вызвать ошибку, их результат также следует проверить, и. В результате корректная программа оказывается загроможденной кодом обработки ошибок. Менее прилежные программисты, утомившись, просто бросают это дело, и результат в виде вылетов программы, неожиданных результатов и прочих сюрпризов не заставляет себя ждать.
Наследники C, учитывая отрицательный опыт родителя, обзавелись средствами, позволяющими облегчить жизнь программиста при обработке ошибок. В C++ появились исключения, пусть и не слишком изящные, затем в Java и C# эта концепция была доведена до практически приемлемого уровня. Применение механизма исключений позволяет писать код, который решает свою задачу, а в случае, когда продолжение работы невозможно, выбрасывает исключение. В подходящем месте программы размещается перехватчик исключений, который предпринимает соответствующие действия. Структура программы улучшается, т.к. обработка ошибок производится не в том месте, где они возникают, а в том, где это наиболее целесообразно.
К счастью, C располагает ценным средством для компенсации своих многочисленных недостатков и слабостей — препроцессором. Инструмент этот довольно опасен, но при умелом использовании дает замечательные результаты. Сегодня мы рассмотрим одно из таких успешных применений макросов — реализацию механизма исключений на языке ANSI C. А для этого мы попробуем решить очень простую, но весьма типичную задачу.
Рассмотрим простую задачу. Треугольник задан тремя сторонами a, b, c. Нам нужно определить его площадь.
В принципе задача довольно легко решаема:
double triangleArea ( double a , double b , double c )
{
double p = ( a + b + c ) / 2.0 ;
double result = sqrt ( p * ( p - a ) * ( p - b ) * ( p - c ) ) ;
return result ;
}
Впрочем, идеальным это решение не назовешь. Функция реализует вычисление площади по формуле Герона, не заботясь о корректности значений переданных ей параметров. Во-первых, отрицательное значение длины стороны треугольника лишено физического смысла. Во-вторых, не каждые три отрезка положительной длины образуют треугольник. Помимо бессмысленного результата, можно получить и более серьезную проблему в виде отрицательного значения под корнем.
Самое простое — возложить ответственность за корректность параметров на клиента, вызывающего нашу функцию. Но такое решение сродни заметанию мусора под ковер. К тому же, если функция будет вызываться достаточно часто (а ведь именно для этого и придуманы функции), проверку параметров перед вызовом придется производить во многих местах. И даже это не дает гарантии: достаточно забыть проверить параметры в одном месте программы — и благодатная почва для потенциального сбоя подготовлена.
Лучше, конечно, проверить параметры внутри функции, до начала вычислений.
double triangleArea ( double a , double b , double c )
if ( ( a
Вот только не совсем понятно, что делать, если параметры некорректны. Конечно, можно вернуть нулевое или отрицательное значение вызывающей программе как указание на то, что при выполнении возникла нештатная ситуация:
double triangleArea ( double a , double b , double c )
( a
Как вариант, можно вообще прекратить выполнение программы при ошибке, чтобы заведомо избежать нежелательных последствий:
#include
#include
double triangleArea ( double a , double b , double c )
( a
Однако такой вариант очень ограничивает использование нашей функции. Например, если значения вводятся с клавиатуры, велика вероятность опечатки; в этом случае логично было бы вывести предупреждение и предложение повторить ввод. Останов по ошибке исключает такую возможность, вынуждая перезапустить программу с самого начала.
Это одна из причин, по которым язык C сегодня многие не воспринимают всерьез, считая, что на нем невозможно писать изящные программы. Зачастую такое мнение оказывается ошибочным: C может очень многое, гораздо больше, чем кажется на первый взгляд. Нужно лишь воспользоваться одним из многочисленных средств для расширения его возможностей. Одним из таких средств является CException.
Основы CException
Пользоваться CException очень просто. Его основу образуют три макроса: Try, Catch и Throw, а также тип CEXCEPTION_T.
CEXCEPTION_T
Для того, чтобы обработчик ошибки мог выяснить, какая именно ошибка произошла, каждый тип ошибки должен иметь свой идентификатор. Тип этого идентификатора должен быть CEXCEPTION_T. По умолчанию он соответствует unsigned int.
Этот макрос начинает защищенный блок. Синтаксис его подобен синтаксису обычного if: за ним следует либо единичный оператор, либо блок. За ним обязательно должен следовать блок Catch (если вы забудете об этом, получите ошибку компиляции).
Блоки Try могут быть вложены друг в друга. При необходимости можно повторно выбросить исключение, чтобы обработать его во внешнем блоке Try.
Этот макрос начинает блок обработки ошибок. Он выполняется лишь в том случае, если в блоке Try возникло исключение. Исключение может возникнуть как непосредственно в коде внутри блока Try, так и в функции, вызываемой в блоке Try на любом уровне вложенности.
Catch получает идентификатор исключения типа CEXCEPTION_T, который вы можете использовать при обработке ошибки. Если в процессе обработки возникнет исключение, оно будет обработано внешним блоком Try.
Используется для того, чтобы выбросить исключение. Принимает единственный аргумент — идентификатор исключения, который будет передан Catch в качестве причины ошибки. Как уже упоминалось ранее, в процессе обработки ошибки можно опять выбрасывать исключение, в том числе и повторно то же самое, которое обрабатывается в данный момент.
Паттерн обработки ошибок
Располагая описанными выше примитивами, можно написать надежную программу по следующему образцу:
.
// Защищенный блок
Try
.
потенциально_опасный код
if (возникла_ошибка)
Throw(e1); // e1 - код конкретной ошибки
.
вызов_потенциально_опасной_функции()
.
>
// Сюда мы попадаем только в случае возникновения ошибки
Catch(e) // здесь e - код возникшей ошибки
.
код_обработки_ошибки
.
>
// Конец защищенного блока
.
потенциально_опасная_функция()
.
if (возникла_ошибка)
Throw(e2); // e2 - код конкретной ошибки
.
>
Теперь мы можем сосредоточить весь код обработки ошибок в единственном месте — блоке Catch, не загромождая основной поток выполнения программы.
Окончательный вариант программы
Сначала определим коды возможных ошибок, чтобы наша программа не пестрила «магическими числами».
enum ERRORCODE_T
{
NEGATIVE_SIDE ,
BAD_TRIANGLE
} ;
Интерфейс функции вычисления треугольника остается неизменным:
extern double triangleArea ( double a , double b , double c ) ;
А вот в тело функции внесем изменения с учетом того, что узнали про обработку исключений:
#include
#include
#include "CException.h"
#include "ErrorCode.h"
#include "TriangleArea.h"
double triangleArea ( double a , double b , double c )
if ( ( a
Ну и в завершение — главная программа:
#include
#include
#include "TriangleArea.h"
#include "CException.h"
#include "ErrorCode.h"
int main ( int argc , char * argv [ ] )
{
CEXCEPTION_T e ;
double a , b , c ;
a = 10.0 ;
b = 2.0 ;
c = 2.0 ;
printf ( "a=%f b=%f c=%f \n " , a , b , c ) ;
Try
{
double area = triangleArea ( a , b , c ) ;
printf ( "area=%f \n " , area ) ;
}
Catch ( e )
{
switch ( e )
{
case NEGATIVE_SIDE :
printf ( "One of triangle sides is negative. \n " ) ;
break ;
case BAD_TRIANGLE :
printf ( "Triangle cannot be made of these sides. \n " ) ;
break ;
default :
printf ( "Unknown error: %d \n " , e ) ;
}
}
system ( "PAUSE" ) ;
return 0 ;
}
Улучшение структуры кода сразу бросается в глаза: функция сообщает о возникновении ошибки, а инфраструктура CException передает управление обработчику ошибок без участия программиста. Больше нет необходимости загромождать текст программы условными операторами (не забываем, что по-хорошему программу нужно еще тестировать, а наличие каждого ветвления усложняет эту процедуру).
Заключение
Конечно, CException — не панацея, и его применение не решит всех проблем. Например, CEXCEPTION_T — это всего лишь целочисленная константа, и с ее помощью невозможно сообщить обработчику все детали об ошибке. Кроме того, обработчик Catch в отличие от настоящих обработчиков, встроенных в современные языки программирования, перехватывает все исключения, поэтому вполне возможно, что некоторые из них, не предназначенные данному обработчику, придется выбросить повторно. Но все же плюсов от использования CException гораздо больше чем минусов, и программы на C, написанные с использованием механизма исключений, окажутся проще и надежнее своих старорежимных собратьев.
Обработка ошибок и исключения
2. Коды возврата. Основная идея — в случае ошибки возвращать специальное значение, которое не может быть корректным. Например, если в методе есть операция деления, то придется проверять делитель на равенство нулю. Также проверим корректность аргументов a и b :
Double f(Double a, Double b) < if ((a == null) || (b == null)) < return null; > //. if (Math.abs(b) < EPS) < return null; > else < return a / b; > >
При вызове метода необходимо проверить возвращаемое значение:
Double d = f(a, b); if (d != null) < //. > else < //. >
Минусом такого подхода является необходимость проверки возвращаемого значения каждый раз при вызове метода. Кроме того, не всегда возможно определить тип ошибки.
3.Использовать флаг ошибки: при возникновении ошибки устанавливать флаг в соответствующее значение:
boolean error; Double f(Double a, Double b) < if ((a == null) || (b == null)) < error = true; return null; > //. if (Math.abs(b) < EPS) < error = true; return b; > else < return a / b; > >
error = false; Double d = f(a, b); if (error) < //. > else < //. >
Минусы такого подхода аналогичны минусам использования кодов возврата.
4.Можно вызвать метод обработки ошибки и возвращать то, что вернет этот метод.
Double f(Double a, Double b) < if ((a == null) || (b == null)) < return nullPointer(); > //. if (Math.abs(b) < EPS) < return divisionByZero(); > else < return a / b; > >
Но в таком случае не всегда возможно проверить корректность результата вызова основного метода.
5.В случае ошибки просто закрыть программу.
if (Math.abs(b) < EPS) < System.exit(0); return this; >
Это приведет к потере данных, также невозможно понять, в каком месте возникла ошибка.
Исключения
В Java возможна обработка ошибок с помощью исключений:
Double f(Double a, Double b) < if ((a == null) || (b == null)) < throw new IllegalArgumentException("arguments of f() are null"); > //. return a / b; >
Проверять b на равенство нулю уже нет необходимости, так как при делении на ноль метод бросит непроверяемое исключение ArithmeticException .
- разделить обработку ошибок и сам алгоритм;
- не загромождать код проверками возвращаемых значений;
- обрабатывать ошибки на верхних уровнях, если на текущем уровне не хватает данных для обработки. Например, при написании универсального метода чтения из файла невозможно заранее предусмотреть реакцию на ошибку, так как эта реакция зависит от использующей метод программы;
- классифицировать типы ошибок, обрабатывать похожие исключения одинаково, сопоставлять специфичным исключениям определенные обработчики.
Каждый раз, когда при выполнении программы происходит ошибка, создается объект-исключение, содержащий информацию об ошибке, включая её тип и состояние программы на момент возникновения ошибки. После создания исключения среда выполнения пытается найти в стеке вызовов метод, который содержит код, обрабатывающий это исключение. Поиск начинается с метода, в котором произошла ошибка, и проходит через стек в обратном порядке вызова методов. Если не было найдено ни одного подходящего обработчика, выполнение программы завершается.
Таким образом, механизм обработки исключений содержит следующие операции:
- Создание объекта-исключения.
- Заполнение stack trace'а этого исключения.
- Stack unwinding (раскрутка стека) в поисках нужного обработчика.
Классификация исключений
Класс Java Throwable описывает все, что может быть брошено как исключение. Наследеники Throwable - Exception и Error - основные типы исключений. Также RuntimeException , унаследованный от Exception , является существенным классом.
![]()
Иерархия стандартных исключений
Проверяемые исключения
Наследники класса Exception (кроме наслеников RuntimeException ) являются проверяемыми исключениями(checked exception). Как правило, это ошибки, возникшие по вине внешних обстоятельств или пользователя приложения – неправильно указали имя файла, например. Эти исключения должны обрабатываться в ходе работы программы, поэтому компилятор проверяет наличие обработчика или явного описания тех типов исключений, которые могут быть сгенерированы некоторым методом.
Все исключения, кроме классов Error и RuntimeException и их наследников, являются проверяемыми.
Error
Класс Error и его подклассы предназначены для системных ошибок. Свои собственные классы-наследники для Error писать (за очень редкими исключениями) не нужно. Как правило, это действительно фатальные ошибки, пытаться обработать которые довольно бессмысленно (например OutOfMemoryError ).
RuntimeException
Эти исключения обычно возникают в результате ошибок программирования, такие как ошибки разработчика или неверное использование интерфейса приложения. Например, в случае выхода за границы массива метод бросит OutOfBoundsException . Такие ошибки могут быть в любом месте программы, поэтому компилятор не требует указывать runtime исключения в объявлении метода. Теоретически приложение может поймать это исключение, но разумнее исправить ошибку.
Обработка исключений
Чтобы сгенерировать исключение используется ключевое слово throw . Как и любой объект в Java, исключения создаются с помощью new .
if (t == null) < throw new NullPointerException("t = null"); >
Есть два стандартных конструктора для всех исключений: первый - конструктор по умолчанию, второй принимает строковый аргумент, поэтому можно поместить подходящую информацию в исключение.
Возможна ситуация, когда одно исключение становится причиной другого. Для этого существует механизм exception chaining. Практически у каждого класса исключения есть конструктор, принимающий в качестве параметра Throwable – причину исключительной ситуации. Если же такого конструктора нет, то у Throwable есть метод initCause(Throwable) , который можно вызвать один раз, и передать ему исключение-причину.
Как и было сказано раньше, определение метода должно содержать список всех проверяемых исключений, которые метод может бросить. Также можно написать более общий класс, среди наследников которого есть эти исключения.
void f() throws InterruptedException, IOException < //.
try-catch-finally
Код, который может бросить исключения оборачивается в try -блок, после которого идут блоки catch и finally (Один из них может быть опущен).
try < // Код, который может сгенерировать исключение >
Сразу после блока проверки следуют обработчики исключений, которые объявляются ключевым словом catch.
try < // Код, который может сгенерировать исключение > catch(Type1 id1) < // Обработка исключения Type1 > catch(Type2 id2) < // Обработка исключения Type2 >
Сatch -блоки обрабатывают исключения, указанные в качестве аргумента. Тип аргумента должен быть классом, унаследованного от Throwable , или самим Throwable . Блок catch выполняется, если тип брошенного исключения является наследником типа аргумента и если это исключение не было обработано предыдущими блоками.
Код из блока finally выполнится в любом случае: при нормальном выходе из try , после обработки исключения или при выходе по команде return .
NB: Если JVM выйдет во время выполнения кода из try или catch , то finally -блок может не выполниться. Также, например, если поток выполняющий try или catch код остановлен, то блок finally может не выполниться, даже если приложение продолжает работать.
Блок finally удобен для закрытия файлов и освобождения любых других ресурсов. Код в блоке finally должен быть максимально простым. Если внутри блока finally будет брошено какое-либо исключение или просто встретится оператор return , брошенное в блоке try исключение (если таковое было брошено) будет забыто.
import java.io.IOException; public class ExceptionTest < public static void main(String[] args) < try < try < throw new Exception("a"); > finally < throw new IOException("b"); > > catch (IOException ex) < System.err.println(ex.getMessage()); > catch (Exception ex) < System.err.println(ex.getMessage()); > > >
После того, как было брошено первое исключение - new Exception("a") - будет выполнен блок finally , в котором будет брошено исключение new IOException("b") , именно оно будет поймано и обработано. Результатом его выполнения будет вывод в консоль b . Исходное исключение теряется.
Обработка исключений, вызвавших завершение потока
При использовании нескольких потоков бывают ситуации, когда поток завершается из-за исключения. Для того, чтобы определить с каким именно, начиная с версии Java 5 существует интерфейс Thread.UncaughtExceptionHandler . Его реализацию можно установить нужному потоку с помощью метода setUncaughtExceptionHandler . Можно также установить обработчик по умолчанию с помощью статического метода Thread.setDefaultUncaughtExceptionHandler .
Интерфейс Thread.UncaughtExceptionHandler имеет единственный метод uncaughtException(Thread t, Throwable e) , в который передается экземпляр потока, завершившегося исключением, и экземпляр самого исключения. Когда поток завершается из-за непойманного исключения, JVM запрашивает у потока UncaughtExceptionHandler , используя метод Thread.getUncaughtExceptionHandler() , и вызвает метод обработчика – uncaughtException(Thread t, Throwable e) . Все исключения, брошенные этим методом, игнорируются JVM.
Информация об исключениях
- getMessage() . Этот метод возвращает строку, которая была первым параметром при создании исключения;
- getCause() возвращает исключение, которое стало причиной текущего исключения;
- printStackTrace() печатает stack trace, который содержит информацию, с помощью которой можно определить причину исключения и место, где оно было брошено.
Exception in thread "main" java.lang.IllegalStateException: A book has a null property at com.example.myproject.Author.getBookIds(Author.java:38) at com.example.myproject.Bootstrap.main(Bootstrap.java:14) Caused by: java.lang.NullPointerException at com.example.myproject.Book.getId(Book.java:22) at com.example.myproject.Author.getBookIds(Author.java:35)
Все методы выводятся в обратном порядке вызовов. В примере исключение IllegalStateException было брошено в методе getBookIds , который был вызван в main . "Caused by" означает, что исключение NullPointerException является причиной IllegalStateException .
Разработка исключений
Чтобы определить собственное проверяемое исключение, необходимо создать наследника класса java.lang.Exception . Желательно, чтобы у исключения был конструкор, которому можно передать сообщение:
public class FooException extends Exception < public FooException() < super(); > public FooException(String message) < super(message); > public FooException(String message, Throwable cause) < super(message, cause); > public FooException(Throwable cause) < super(cause); > >
Исключения в Java7
- обработка нескольких типов исключений в одном catch -блоке:
catch (IOException | SQLException ex)
В таких случаях параметры неявно являются final , поэтому нельзя присвоить им другое значение в блоке catch .
Байт-код, сгенерированный компиляцией такого catch -блока будет короче, чем код нескольких catch -блоков.
- Try с ресурсами позволяет прямо в try -блоке объявлять необходимые ресурсы, которые по завершению блока будут корректно закрыты (с помощью метода close() ). Любой объект реализующий java.lang.AutoCloseable может быть использован как ресурс.
static String readFirstLineFromFile(String path) throws IOException < try (BufferedReader br = new BufferedReader(new FileReader(path))) < return br.readLine(); > >
В приведенном примере в качестве ресурса использутся объект класса BufferedReader , который будет закрыт вне зависимосити от того, как выполнится try -блок.
Можно объявлять несколько ресурсов, разделяя их точкой с запятой:
public static void viewTable(Connection con) throws SQLException < String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES"; try (Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(query)) < //Work with Statement and ResultSet > catch (SQLException e) < e.printStackTrace; >>
Во время закрытия ресурсов тоже может быть брошено исключение. В try-with-resources добавленна возможность хранения "подавленных" исключений, и брошенное try -блоком исключение имеет больший приоритет, чем исключения получившиеся во время закрытия. Получить последние можно вызовом метода getSuppressed() от исключения брошенного try -блоком.
- Перебрасывание исключений с улучшенной проверкой соответствия типов.
Компилятор Java SE 7 тщательнее анализирует перебрасываемые исключения. Рассмотрим следующий пример:
static class FirstException extends Exception < >static class SecondException extends Exception < >public void rethrowException(String exceptionName) throws Exception < try < if ("First".equals(exceptionName)) < throw new FirstException(); > else < throw new SecondException(); > > catch (Exception ex) < throw e; > >
В примере try -блок может бросить либо FirstException , либо SecondException . В версиях до Java SE 7 невозможно указать эти исключения в декларации метода, потому что catch -блок перебрасывает исключение ex , тип которого - Exception .
В Java SE 7 вы можете указать, что метод rethrowException бросает только FirstException и SecondException . Компилятор определит, что исключение Exception ex могло возникнуть только в try -блоке, в котором может быть брошено FirstException или SecondException . Даже если тип параметра catch - Exception , компилятор определит, что это экземпляр либо FirstException , либо SecondException :
public void rethrowException(String exceptionName) throws FirstException, SecondException < try < // . > catch (Exception e) < throw e; > >
Если FirstException и SecondException не являются наследниками Exception , то необходимо указать и Exception в объявлении метода.
Примеры исключений
- любая операция может бросить VirtualMachineError . Как правило это происходит в результате системных сбоев.
- OutOfMemoryError . Приложение может бросить это исключение, если, например, не хватает места в куче, или не хватает памяти для того, чтобы создать стек нового потока.
- IllegalArgumentException используется для того, чтобы избежать передачи некорректных значений аргументов. Например:
public void f(Object a) < if (a == null) < throw new IllegalArgumentException("a must not be null"); > >
- IllegalStateException возникает в результате некорректного состояния объекта. Например, использование объекта перед тем как он будет инициализирован.
Гарантии безопасности
При возникновении исключительной ситуации, состояния объектов и программы могут удовлетворять некоторым условиям, которые определяются различными типами гарантий безопасности:
- Отсутствие гарантий (no exceptional safety). Если было брошено исключение, то не гарантируется, что все ресурсы будут корректно закрыты и что объекты, методы которых бросили исключения, могут в дальнейшем использоваться. Пользователю придется пересоздавать все необходимые объекты и он не может быть уверен в том, что может переиспозовать те же самые ресурсы.
- Отсутствие утечек (no-leak guarantee). Объект, даже если какой-нибудь его метод бросает исключение, освобождает все ресурсы или предоставляет способ сделать это.
- Слабые гарантии (weak exceptional safety). Если объект бросил исключение, то он находится в корректном состоянии, и все инварианты сохранены. Рассмотрим пример:
class Interval < //invariant: left double left; double right; //. >
Если будет брошено исключение в этом классе, то тогда гарантируется, что ивариант "левая граница интервала меньше правой" сохранится, но значения left и right могли измениться.
- Сильные гарантии (strong exceptional safety). Если при выполнении операции возникает исключение, то это не должно оказать какого-либо влияния на состояние приложения. Состояние объектов должно быть таким же как и до вызовов методов.
- Гарантия отсутствия исключений (no throw guarantee). Ни при каких обстоятельствах метод не должен генерировать исключения. В Java это невозможно, например, из-за того, что VirtualMachineError может произойти в любом месте, и это никак не зависит от кода. Кроме того, эту гарантию практически невозможно обеспечить в общем случае.
Источники
- Обработка ошибок и исключения — Сайт Георгия Корнеева
- Лекция Георгия Корнеева — Лекториум
- The Java Tutorials. Lesson: Exceptions
- Обработка исключений — Википедия
- Throwable (Java Platform SE 7 ) — Oracle Documentation
- try/catch/finally и исключения — www.skipy.ru