Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Язык программирования C++. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
07.09.2026
Размер:
1 Мб
Скачать
☆
обрабатывает.
Третий вид ошибок тоже в какой-то мере предусмотрен. Это исключительные ситуации, которые могут иметь место, даже если в программе нет ошибок . Например, нехватка памяти для создания нового объекта. Или сбой диска при извлечении информации из базы данных.
Именно обработка двух последних видов ошибок и рассматривается в последующих разделах. Граница между ними довольно условна. Например, для большинства программ сбой диска – исключительная ситуация, но для операционной системы сбой диска должен быть предусмотрен и должен обрабатываться. Скорее два типа можно разграничить по тому, какая реакция программы должна быть предусмотрена. Если после плановых ошибок программа должна продолжать работать, то после исключительных ситуаций надо лишь сохранить уже вычисленные данные и завершить программу.
Возвращаемое значение как признак ошибки
Простейший способ сообщения об ошибках предполагает использование возвращаемого значения функции или метода. Функция сохранения объекта в базе данных может возвращать логическое значение: true в случае успешного сохранения, false – в случае ошибки.
class Database { public: bool SaveObject(const Object& obj); };
Соответственно, вызов метода должен выглядеть так:
if (database.SaveObject(my_obj) == false ){ //обработка ошибки }
Обработка ошибки, разумеется, зависит от конкретной программы.
Язык программирования C++Фридман А.Л.
182
181
Типична ситуация, когда при многократно вложенных вызовах функций обработка происходит на несколько уровней выше, чем уровень, где ошибка произошла. В таком случае результат, сигнализирующий об ошибке, придется передавать во всех вложенных вызовах.
int main() { if (fun1()==false ) //обработка ошибки return 1; } bool fun1() { if (fun2()==false ) return false ; return true ; } bool fun2() { if (database.SaveObject(obj)==false ) return false ; return true ; }
Если функция или метод должны возвращать какую-то величину в качестве результата, то особое, недопустимое, значение этой величины используется в качестве признака ошибки. Если метод возвращает указатель, выдача нулевого указателя применяется в качестве признака ошибки. Если функция вычисляет положительное число, возврат - 1 можно использовать в качестве признака ошибки.
Иногда невозможно вернуть признак ошибки в качестве возвращаемого значения. Примером является конструктор объекта, который не может вернуть значение. Как же сообщить о том, что во время инициализации объекта что-то было не так?
Распространенным решением является дополнительный атрибут объекта – флаг, отражающий состояние объекта. Предположим,
Язык программирования C++Фридман А.Л.
183
182
конструктор класса Database должен соединиться с сервером базы данных.
class Database { public : Database(const char *serverName); ... bool Ok(void) const {return okFlag;}; private : bool okFlag; }; Database::Database(const char*serverName) { if (connect(serverName)==true ) okFlag =true ; else okFlag =false ; } int main() { Database database("db-server"); if (!database.Ok()){ cerr <<"Ошибка соединения с базой данных"<<endl; return 0; } return 1; }
Лучше вместо метода Ok, возвращающего значение флага okFlag, переопределить операцию ! (отрицание).
class Database { public : bool operator !()const {return !okFlag;}; };
Тогда проверка успешности соединения с базой данных будет выглядеть
Язык программирования C++Фридман А.Л.
184
183
так:
if (!database){ cerr <<"Ошибка соединения с базой данных"<<endl; }
Следует отметить, что лучше избегать такого построения классов, при котором возможны ошибки в конструкторе. Из конструктора можно выделить соединение с сервером базы данных в отдельный метод
Open:
class Database { public : Database(); bool Open(const char*serverName); }
и тогда отпадает необходимость в операции ! или методе Ok().
Использование возвращаемого значения в качестве признака ошибки – метод почти универсальный. Он применяется, прежде всего, для обработки запланированных ошибочных ситуаций. Этот метод имеет ряд недостатков. Во-первых, приходится передавать признак ошибки через вложенные вызовы функций. Во-вторых, возникают неудобства, если метод или функция уже возвращают значение, и приходится либо модифицировать интерфейс, либо придумывать специальное " ошибочное " значение. В-третьих, логика программы оказывается запутанной из-за сплошных условных операторов if с проверкой на ошибочное значение.
Исключительные ситуации
В языке Си++ реализован специальный механизм для сообщения об ошибках – механизм исключительных ситуаций. Название, конечно же, наводит на мысль, что данный механизм предназначен, прежде всего, для оповещения об исключительных ситуациях, о которых мы говорили чуть ранее. Однако механизм исключительных ситуаций может
Язык программирования C++Фридман А.Л.
185
184
применяться и для обработки плановых ошибок.
Исключительная ситуация возникает при выполнении оператора
throw. В качестве аргумента throw задается любое значение. Это
может быть значение одного из встроенных типов (число, строка символов и т.п.) или объект любого определенного в программе класса.
При возникновении исключительной ситуации выполнение текущей функции или метода немедленно прекращается, созданные к этому моменту автоматические переменные уничтожаются, и управление передается в точку, откуда была вызвана текущая функция или метод. В точке возврата создается та же самая исключительная ситуация, прекращается выполнение текущей функции или метода, уничтожаются автоматические переменные, и управление передается в точку, откуда была вызвана эта функция или метод. Происходит своего рода откат всех вызовов до тех пор, пока не завершится функция main и, соответственно, вся программа.
Предположим, из main была вызвана функция foo, которая вызвала метод Open, а он в свою очередь возбудил исключительную ситуацию:
class Database { public : void Open(const char*serverName); }; void Database::Open(const char*serverName) { if (connect(serverName)==false ) throw 2; } foo() { Database database; database.Open("db-server"); String y; ... }
Язык программирования C++Фридман А.Л.
186
185
int main() { String x; foo(); return 1; }
В этом случае управление вернется в функцию foo, будет вызван деструктор объекта database, управление вернется в main, где будет вызван деструктор объекта x, и выполнение программы завершится. Таким образом, исключительные ситуации позволяют аварийно завершать программы с некоторыми возможностями очистки переменных.
В таком виде оператор throw используется для действительно исключительных ситуаций, которые практически никак не обрабатываются. Гораздо чаще даже исключительные ситуации требуется обрабатывать.
Обработка исключительных ситуаций
В программе можно объявить блок, в котором мы будем отслеживать исключительные ситуации с помощью операторов try и catch:
try { ... }catch (тип_исключительной_операции){ ... }
Если внутри блока try возникла исключительная ситуация, то она первым делом передается в оператор catch. Тип исключительной ситуации – это тип аргумента throw. Если тип исключительной ситуации совместим с типом аргумента catch, выполняется блок
catch. Тип аргумента catch совместим, если он либо совпадает с
типом ситуации, либо является одним из ее базовых типов. Если тип несовместим, то происходит описанный выше откат вызовов, до тех пор, пока либо не завершится программа, либо не встретится блок
Язык программирования C++Фридман А.Л.
187
186
catch с подходящим типом аргумента.
В блоке catch происходит обработка исключительной ситуации.
foo() { Database database; int attempCount =0; again: try { database.Open("dbserver"); } catch (int& x){ cerr <<"Ошибка соединения номер " <<x <<endl; if (++attemptCount <5) goto again; throw ; } String y; ... }
Ссылка на аргумент throw передается в блок catch. Этот блок гасит исключительную ситуацию. Во время обработки в блоке catch можно создать либо ту же самую исключительную ситуацию с помощью оператора throw без аргументов, либо другую, или же не создавать никакой. В последнем случае исключительная ситуация считается погашенной, и выполнение программы продолжается после блока
catch.
С одним блоком try может быть связано несколько блоков catch с разными аргументами. В этом случае исключительная ситуация последовательно "примеряется" к каждому catch до тех пор, пока аргумент не окажется совместимым. Этот блок и выполняется. Специальный вид catch
catch (...)
совместим с любым типом исключительной ситуации. Правда, в него
Язык программирования C++Фридман А.Л.
188
187
нельзя передать аргумент.
Примеры обработки исключительных ситуаций
Механизм исключительных ситуаций предоставляет гибкие возможности для обработки ошибок, однако им надо уметь правильно пользоваться. В этом параграфе мы рассмотрим некоторые приемы обработки исключительных ситуаций.
Прежде всего, имеет смысл определить для них специальный класс. Простейшим вариантом является класс, который может хранить код ошибки:
class Exception { public : enum ErrorCode { NO_MEMORY, DATABASE_ERROR, INTERNAL_ERROR, ILLEGAL_VALUE }; Exception(ErrorCode errorKind, const String& errMessage); ErrorCode GetErrorKind(void )const {return kind;}; const String& GetErrorMessage(void )const {return msg;}; private : ErrorCode kind; String msg; };
Создание исключительной ситуации будет выглядеть следующим образом:
if (connect(serverName)==false ) throw Exception(Exception::DATABASE_ERROR,
Язык программирования C++Фридман А.Л.
189
188
serverName);
А проверка на исключительную ситуацию так:
try { ... }catch (Exception e){ cerr <<"Произошла ошибка "<<e.GetErrorKind() <<"Дополнительная информация:" <<e.GetErrorMessage(); }
Преимущества класса перед просто целым числом состоят, во-первых, в том, что передается дополнительная информация и, во-вторых, в операторах catch можно реагировать только на ошибки определенного вида. Если была создана исключительная ситуация другого типа, например
throw AnotherException;
то блок catch будет пропущен: он ожидает только исключительных ситуаций типа Exception. Это особенно существенно при сопряжении нескольких различных программ и библиотек – каждый набор классов отвечает только за собственные ошибки.
В данном случае код ошибки записывается в объекте типа
Exception. Если в одном блоке catch ожидается несколько разных
исключительных ситуаций, и для них необходима разная обработка, то в программе придется анализировать код ошибки с помощью операторов
if или switch.
try { ... }catch (Exception&e){ cerr <<"Произошла ошибка "<<e.GetErrorKind() <<"Дополнительная информация:" <<e.GetErrorMessage(); if (e.GetErrorKind()==Exception::NO_MEMORY || e.GetErrorKind()== Exception::INTERNAL_ERROR)
Язык программирования C++Фридман А.Л.
190
189
throw ; else if (e.GetErrorKind()== Exception::DATABASE_ERROR) return TRY_AGAIN; else if (e.GetErrorKind()== Exception::ILLEGAL_VALUE) return NEXT_VALUE; }
Другим методом разделения различных исключительных ситуаций является создание иерархии классов – по классу на каждый тип исключительной ситуации.
Рис. 16.1. Пример иерархии классов для представления исключительных ситуаций.
В приведенной на рисунке 16.1 структуре классов все исключительные ситуации делятся на ситуации, связанные с работой базы данных (класс
DatabaseException ), и внутренние ошибки программы (класс InternalException). В свою очередь, ошибки базы данных
бывают двух типов: ошибки соединения (представленные классом
ConnectDbException) и ошибки чтения (ReadDbException).
Внутренние исключительные ситуации разделены на нехватку памяти (NoMemoryException) и недопустимые значения
Язык программирования C++Фридман А.Л.
191
190
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]