Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Итог_Пособие C++.doc
Скачиваний:
8
Добавлен:
01.07.2025
Размер:
2 Мб
Скачать
☆

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

4.1 Основные понятия

Представим себе, что нам для каких-то целей потребовалось написать функцию, возвращающую первую строку файла с заданным именем. В простейшем варианте реализация функции может выглядеть так:

// Пример 4.1 - функция для получения первой строки файла

#include <fstream>

std::string firstLine(const std::string &fileName) {

std::ifstream inp(fileName);

std::string res;

std::getline(inp, res);

return res;

}

Однако, что делать, если в процессе работы функции что-то пошло не так, и она не может корректно выполнить свою работу? Например, файл с таким именем не существует, у пользователя недостаточно прав доступа к этому файлу, и т.п. Рассмотрим несколько способов, которыми функция может сигнализировать об этом:

1). Отсутствие какой-либо проверки возникновения ошибочных ситуаций. В промышленном программировании такой способ нежелателен, поскольку он может привести к неопределенному поведению ("undefined behavior"), при этом дальнейший результат работы программы непредсказуем. Однако, всё же к данному способу иногда прибегают в целях повышения производительности (даже некоторые функции стандартной библиотеки могут вызывать неопределённое поведение при передаче некорректных параметров).

2). Запись сообщения об ошибке в стандартный поток ошибок или в специальный файл журнала (так называемый "лог-файл") и аварийное завершение работы программы. В таком варианте функция из предыдущего примера будет выглядеть так:

// Пример 4.2 - функция для получения первой строки файла

// с аварийным завершением программы при ошибке

#include <fstream>

#include <string>

#include <cstdlib>

std::string firstLine(const std::string &fileName) {

std::ifstream inp(fileName);

if (!inp.is_open()) {

// Запись сообщения и аварийное завершение

std::cerr << "Не удалось открыть файл "

<< fileName << std::endl;

exit(1);

}

std::string res;

std::getline(inp, res);

return res;

}

Данный подход встречается, например, в программах, работающих в неинтерактивном режиме (системных службах и т.п.). Однако, во многих случаях этот вариант недостаточно гибок (особенно для интерактивных приложений). Представим, что имя файла, которое передаётся в нашу функцию, вводится пользователем. Если пользователь случайно ошибся при вводе, то было бы логично не завершать программу совсем, а сообщить пользователю об ошибке и позволить ввести имя файла ещё раз.

3). Возврат определённого значения по умолчанию. Например, самый первый вариант нашей функции firstLine именно это и делает: если файл открыть не удалось, будет возвращена пустая строка.

В некоторых случаях такой подход оправдан. Однако, его стоит использовать с осторожностью, так как он может привести к сложно обнаруживаемым ошибкам. Например, если пользователь случайно ошибётся при вводе имени файла, то из результата функции будет невозможно определить, что произошло − то ли не удалось открыть файл, то ли первая строка в файле оказалась пустой.

4). Функция может вернуть код ошибки (иногда используется термин "код возврата"). Это довольно типичный способ для языка C (заметим, что механизм исключительных ситуаций появился только в языке C++ − см. следующий пункт). Приведём вариант нашей функции, возвращающей код ошибки:

// Пример 4.3 - функция для получения первой строки файла

// с возвратом кода ошибки

std::string firstLine(const std::string &fileName, int *pErrCode) {

std::ifstream inp(fileName);

std::string res;

if (!inp.is_open()) {

*pErrCode = 1;

} else {

std::getline(inp, res);

*pErrCode = 0;

}

return res;

}

В функцию был добавлен дополнительный параметр pErrCode − указатель на переменную, куда функция поместит код ошибки. Если ошибки нет, то код будет равен нулю. Пример вызова этой функции тогда может выглядеть так:

int errCode;

std::string s = firstLine("aaa.txt", &errCode);

if (errCode == 0) {

std::cout << s;

} else {

std::cerr << "Не удалось открыть файл";

exit(1);

}

У данного подхода также есть существенные недостатки. При вызове функции можно забыть (или полениться) проверить код ошибки − как итог, получим неопределённое поведение. С другой стороны, если тщательно проверять все коды возврата, то в программе добавится большое число операторов if, её размер значительно вырастет, а читаемость ухудшится.

Дополнительно заметим, что язык C++ унаследовал от языка C большое количество стандартных библиотечных функций. Многие из них возвращают код ошибки не с помощью дополнительного параметра, а записывают его в специальную глобальную переменную errno. Следующий пример демонстрирует вычисление квадратного корня с помощью стандартной функции sqrt. Если вводимое число меньше нуля, то корень не может быть корректно вычислен − выведем сообщение об ошибке. Функция strerror позволяет получить краткое описание ошибки по его коду.

// Пример 4.4 - использование errno и функции strerror

#include <cstdio>

#include <cmath>

#include <cstring>

#include <cerrno>

int main() {

double x;

std::cin >> x;

double r = sqrt(x);

if (errno != 0) {

// выводим сообщение об ошибке

fprintf(stderr, strerror(errno));

} else {

printf("%0.3f", sqrt(x));

}

}

Примечание. Можно предположить, что использование глобальной переменной errno должно вызывать проблемы в многопоточном режиме. В некоторых старых компиляторах это так. Однако, согласно стандарту C++11, errno является уже не глобальной переменной, а специальным макросом, который гарантирует корректную работу в условиях многопоточности, и в современных компиляторах таких проблем быть не должно.

5). Генерация (возбуждение, выбрасывание) исключения. Данный способ появился в языке C++. Именно этот способ рекомендуется использовать при написании своего кода, и о нём пойдёт речь в оставшейся части главы.

Если в функции произошла аномальная ситуация, то та часть программы, которая её обнаружила, может сгенерировать исключение. Исключение может представлять собой значение почти любого типа. Чаще всего исключение является объектом какого-то класса (или структуры), хотя может быть и значением простого типа (например, int). Для возбуждения исключения используется ключевое слово throw. Перепишем нашу функцию firstLine в таком варианте:

// Пример 4.5 - функция для получения первой строки файла

// с возбуждением исключения

class FileNotOpenedException{};

std::string firstLine(const std::string &fileName) {

std::ifstream inp(fileName);

if (!inp.is_open())

throw FileNotOpenedException();

std::string res = "123";

std::getline(inp, res);

return res;

}

В случае, если файл по какой-то причине открыть не удалось, функция возбудит исключение − объект класса FileNotOpenedException. Заметим, что в нашем примере этот класс даже не содержит ни одного члена − сам тип уже даёт информацию о том, какое именно исключение произошло.

Дальнейшая работа программы зависит от того, существует ли подходящий оператор (блок) try-catch, способный обработать данное исключение. Если подходящего блока try-catch найти не удалось, то происходит аварийное завершение работы программы, перед этим этом в поток ошибок будет выдана информация о типе возникшего исключения.

Примечание. Заметим, что деструкторы объектов-локальных переменных при этом вызваны не будут. Корректное уничтожение локальных объектов в процессе раскрутки стека (см. следующий пункт) происходят лишь при наличии подходящего блока try-catch.