В процессе работы программы могут возникать различные ошибки. Например, при передаче файла по сети оборвется сетевое подключение или будут введены некорректные и недопустимые данные, которые вызовут падение программы. Такие ошибки еще называются исключениями. Исключение представлякт временный объект любого типа, который используется для сигнализации об ошибке. Цель объекта-исключения состоит в том, чтобы передать информацию из точки, в которой произошла ошибка, в код, который должен ее обработать. Если исключение не обработано, то при его возникновении программа прекращает свою работу.
Например, в следующей программе происходит деление чисел:
#include <iostream> double divide(int a, int b) { return a / b; } int main() { int x{500}; int y{}; double z {divide(x, y)}; std::cout << z << std::endl; std::cout << "The End..." << std::endl; }
Эта программа успешно скомпилируется, но при ее выполнении возникнет ошибка, поскольку в коде производится деление на ноль, после чего программа аварийно завершится.
С одной стороны, мы можем в функции divide определить проверку и выполнять деление, если параметр b не равен 0. Однако нам в любом случае надо возвращать из функции divide некоторый результат - некоторое число. То есть мы не можем просто написать:
double divide(int a, int b) { if (b) return a / b; else std::cout << "Error! b must not be equal to 0" << std::endl; }
И в этом случае нам надо известить систему о возникшей ошибке. Для этого используется оператор throw.
Оператор throw генерирует исключение. Через оператор throw можно передать информацию об ошибке. Например, функция divide могла бы выглядеть следующим образом:
double divide(int a, int b) { if (b) return a / b; throw "Division by zero!"; }
То есть если параметр b равен 0, то генерируем исключение.
Но это исключение еще надо обработать в коде, где будет вызываться функция divide. Для обработки исключений применяется конструкция try...catch. Она имеет следующую форму:
try { инструкции, которые могут вызвать исключение } catch(объявление_исключения) { обработка исключения }
В блок кода после ключевого слова try помещается код, который потенциально может сгенерировать исключение.
После ключевого слова catch в скобках идет параметр, который передает информацию об исключении. Затем в блоке производится собственно обработка исключения.
Так изменим весь код следующим образом:
#include <iostream> double divide(int a, int b) { if (b) return a / b; throw "Division by zero!"; } int main() { int x{500}; int y{}; try { double z {divide(x, y)}; std::cout << z << std::endl; } catch (...) { std::cout << "Error!" << std::endl; } std::cout << "The End..." << std::endl; }
Код, который потенциально может сгенерировать исключение - вызов функции divide помещается в блок try.
В блоке catch идет обработка исключения. Причем многоточие в скобках после оператора catch (catch(...)
) позволяет обработать любое исключение.
В итоге когда выполнение программы дойдет до строки
double z {divide(x, y)};
При выполнении этой строки будет сгенерировано исключение, поэтому последующие инструкции из блока try выполняться не будут, а управление перейдет в блок catch, в котором на консоль просто выводится сообщение об ошибке. После выполнения блока catch программа аварийно не завершится, а продолжит свою работу, выполняя операторы после блока catch:
Error! The End...
Однако в данном случае мы только знаем, что произошла какая-то ошибка, а какая именно, неизвестно. Поэтому через параметр в блоке catch
мы можем получить
то сообщение, которое передается оператору throw
:
#include >iostream< double divide(int a, int b) { if (b) return a / b; throw "Division by zero!"; } int main() { int x{500}; int y{}; try { double z {divide(x, y)}; std::cout << z << std::endl; } catch (const char* error_message) { std::cout << error_message << std::endl; } std::cout << "The End..." << std::endl; }
С помощью параметра const char* error_message
получаем сообщение, которое предано оператору throw, и выводим это сообщение на консоль.
Почему здесь мы получаем сообщение об ошибке в виде типа const char*
? Потому что после оператора throw
идет строковый литерал, который представляет как
раз тип const char*
. И в этом случае консольный вывод будет выглядеть следующим образом:
Division by zero! The End...
Таким образом, мы можем узнать суть возникшего исключения. Подобным образом мы можем передавать информацию об исключении через любые типы, например, std::string:
throw std::string{"Division by zero!!"};
Тогда в блоке catch мы можем получить эту информацию в виде объекта std::string
:
catch (std::string error_message) { std::cout << error_message << std::endl; }
Если же исключение не обработано, то вызывается функция std::terminate() (из модуля <exception>
стандартной библиотеки C++),
которая, в свою очередь, по умолчанию вызывает другую функцию - std::abort() (из <cstdlib>
), которая собственно и завершает программу.
Существует очень много функций и в стандартной библиотеке С++, и в каких-то сторонних библиотеках. И может возникнуть вопрос, какие из них вызывать в конструкции try-catch, чтобы не столкнуться с необработанным исключением и аварийным завершением программы. В этом случае может помочь прежде всего документация по функции (при ее наличии). Другой сигнал - ключевое слово noexcept, которое при использовании в заголовке функции указывает, что эта функция никогда не будет генерировать исключения. Например:
void print(int argument) noexcept;
Здесь указываем, что функция print() никогда не вызовет исключение. Таким образом, встретив функцию с подобным ключевым словом, можно ожидать, что она не вызовет исключения. И соответственно нет необходимости помещать ее вызов в конструкцию try-catch.
При обработке исключения стоит помнить, что при передаче объекта оператору throw
блок catch получает копию этого объекта. И эта копия существует только в пределах блока catch.
Для значений примитивных типов, например, int
, копирование значения может не влиять на производительность программы. Однако при передаче объектов классов издержки могут выше.
Поэтому в этом случае объекты обычно передаются по ссылке, например:
#include <iostream> double divide(int a, int b) { if (b) return a / b; throw std::string{"Division by zero!"}; } int main() { int x{500}; int y{}; try { double z {divide(x, y)}; std::cout << z << std::endl; } catch (const std::string& error_message) // строка передается по ссылке { std::cout << error_message << std::endl; } std::cout << "The End..." << std::endl; }
Мы можем генерировать и обрабатывать несколько разных исключительных ситуаций. Допустим, нам надо, чтобы при делении делитель (второе число) был не больше, чем делимое (первое число):
#include <iostream> double divide(int a, int b) { if(!b) // если b == 0 { throw 0; } if(b > a) { throw "The second number is greater than the first one"; } return a / b; } void test(int a, int b) { try { double result {divide(a, b)}; std::cout << result << std::endl; } catch (int code) { std::cout << "Error code: " << code << std::endl; } catch (const char* error_message) { std::cout << error_message << std::endl; } } int main() { test(100, 20); // 5 test(100, 0); // Error code: 0 test(100, 1000); // The second number is greater than the first one }
В функции divide
в зависимости от значения числа b оператору throw
передаем либо число:
throw 0;
либо строковый литерал:
throw "The second number is greater than the first one";
Для тестирования функции divide определена другая функция - test, где вызов функции divide()
помещен в конструкцию try..catch
.
Поскольку при генерации исключения мы можем получать ошибку в виде двух типов - int (если b равно 0) и const char*
(если b больше a), то для обработки каждого типа исключений
определены два разных блока catch:
catch (int code) { std::cout << "Error code: " << code << std::endl; } catch (const char* error_message) { std::cout << error_message << std::endl; }
В функции main вызываем функцию test, передавая в нее различные числа. При вызове:
test(100, 20); // 5
число b не равно 0 и меньше a, поэтому никаких исключений не возникает, блок try срабатывает до конца, и функция завершает свое выполнение.
При втором вызове
test(100, 0); // Error code: 0
число b равно 0, поэтому генерируется исключение, а в качестве объекта исключения передается число 0. Поэтому при возникновении исключения программа выберет тот блок catch, где обрабатывается исключения типа int:
catch (int code) { std::cout << "Error code: " << code << std::endl; }
При третьем вызове
test(100, 1000); // The second number is greater than the first one
число b больше a, поэтому объект исключения будет представлять строковый литерал или const char*
. Поэтому при возникновении исключения программа выберет
блок catch, где обрабатывается исключения типа const char*:
catch (const char* error_message) { std::cout << error_message << std::endl; }
Таким образом, в данном случае мы получим следующий консольный вывод:
5 Error code: 0 The second number is greater than the first one
Может быть ситуация, когда генерируется исключение внутри конструкции try-catch
, и даже есть блок catch
для обработки исключений, однако он обрабатывает
другие типы исключений:
void test(int a, int b) { try { double result {divide(a, b)}; std::cout << result << std::endl; } catch (const char* error_message) { std::cout << error_message << std::endl; } }
Здесь нет блока catch для обработки исключения типа int. Поэтому при генерации исключения:
throw 0;
Программа не найдет нужный блок catch для обработки исключения, и программа аварийно завершит свое выполнение.
Стоит отметить, что, если в блоке try создаются некоторые объекты, то при возникновении исключения у них вызываются деструкторы. Например:
#include <iostream> class Person { public: Person(std::string name) :name{ name } { std::cout << "Person " << name << " created" << std::endl; } ~Person() { std::cout << "Person " << name << " deleted" << std::endl; } void print() { throw "Print Error"; } private: std::string name; }; int main() { try { Person tom{ "Tom" }; tom.print(); // Здесь генерируется ошибка } catch (const char* error) { std::cerr << error << std::endl; } }
В классе Person определяет деструктор, который выводит сообщение на консоль. В функции print просто генерируем исключение.
В функции main в блоке try создаем один объект Person и вызываем у него функцию print, что естественно приведет к генерарции исключения и переходу управления программы в блок catch. И если мы посмотрим на консольный вывод
Person Tom created Person Tom deleted Print Error
то мы увидим, что прежде чем начнется обработка исключения в блоке catch, будет вызван деструктор объекта Person.