В процессе работы программы могут возникать различные ошибки. Например, при передаче файла по сети оборвется сетевое подключение или будут введены некорректные и недопустимые данные, которые вызовут падение программы. Такие ошибки еще называются исключениями. Исключение представляет временный объект любого типа, который используется для сигнализации об ошибке. Цель объекта-исключения состоит в том, чтобы передать информацию из точки, в которой произошла ошибка, в код, который должен ее обработать. Если исключение не обработано, то при его возникновении программа прекращает свою работу.
Например, в следующей программе происходит деление чисел:
#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>
#include <string>
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>
#include <string>
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.