Исключения

Обработка исключений

Последнее обновление: 17.03.2023

В процессе работы программы могут возникать различные ошибки. Например, при передаче файла по сети оборвется сетевое подключение или будут введены некорректные и недопустимые данные, которые вызовут падение программы. Такие ошибки еще называются исключениями. Исключение представлякт временный объект любого типа, который используется для сигнализации об ошибке. Цель объекта-исключения состоит в том, чтобы передать информацию из точки, в которой произошла ошибка, в код, который должен ее обработать. Если исключение не обработано, то при его возникновении программа прекращает свою работу.

Например, в следующей программе происходит деление чисел:

#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-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.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850