Глава 20
Одним из этапов создания программы является ее тестирование и отладка. Но даже
если вы устраните все ошибки и программа начнет работать, это еще не означает,
что проблемы не возникнут в будущем. Причина в том, что вы не можете заранее предвидеть всех действий пользователей, производимых в отношении вашей программы, и всех ситуаций, с которыми может столкнуться созданный вами программный продукт. Единственный способ проверить, как будет вести себя программа в различных экстремальных условиях, — создать эти условия и посмотреть, что произойдет. Кстати, небезызвестные законы Мэрфи во многом обязаны своим происхождением всем тем проблемам, которые возникают между
пользователями и программным обеспечением.
Такие непредвиденные условия называют условиями ошибки. Существуют тысячи причин,по которым эти условия возникают. Например, на диске нет свободного места, файл с данными содержит неверную информацию, пользователь вместо радиуса круга указывает координаты его центра и т.п.
Как следствие, все перечисленные ситуации (и множество им подобных} могут привести к сбоям в работе программы. К счастью, существует такое средство, как обработка исключений, которое поможет вам с достоинством справиться со всеми непредвиденными проблемами.
Вверх
Прежде чем осваивать метод обработки исключений, вам полезно будет узнать, как подобные проблемы решались ранее.
желательно заменить такими кодами;
Теперь представьте, что подобные коды нужно набирать в каждой процедуре программы после вызова каждой отдельной функции.
И наконец, после того как вы добавите все эти коды, нужно будет создать еще функцию, которая сможет обработать информацию об ошибке и сообщить что-то вроде: "Ага, я знаю как справиться с этой ошибкой". Такая функция должна уметь различать, ошибка какого типа произошла, и знать, как на это реагировать.
Вверх
Возможность обработки исключений позволяет избавиться от необходимости добавления
Второе преимущество механизма обработки исключений состоит в том, что любые локальные объекты, т.е. те. которые создаются в процессе выполнения какой-то функции, автоматически уничтожаются. Продемонстрируем это на таком примере. Предположим, что функция А создает локальный объект ObjectA, затем вызывает функцию В, которая создает локальный объект OhjectB и вызывает функцию, которая находит ошибку. Объекты ObjectA и ObjeclB должны быть удалены. Таким образом, все используемые ими файлы, данные, фрагменты памяти и т.п. автоматически освобождаются.
Фрагмент кода, который может послужить причиной возникновения исключительной си-
Вверх
Лучше один раз увидеть, чем сто раз услышать. Поэтому перейдем к демонстрации сказанного на конкретном примере. Приведенная ниже функция предназначена для получения числа от пользователя и преобразования его к значению типа integer. (Функция взята из программы Factorial^-, вычисляющей факториал заданного числа.) Казалось бы, ничего сложного сдесь нет:
Но что произойдет, если пользователь наберет что-то неверное или бессмысленое, например слово "печенька"? Поскольку это не может быть преобразовано к значению типа integer (это вообще не число), на экране появится сообщение об ошибке и работа программы будет моментально завершена.
Что в действительности может произойти, если компилятор столкнется с исключи-
Если пользователь набирает какую-то абракадабру, это означает возникновение ошибки FermatExceptiion. Оператор catch идентифицирует эту ошибку, в результате чего на экране отображается дружественное сообщение и возвращается факториал числа, заданного по умолчанию (в данном случае это число 1).
Вверх
Сами по себе пользователи хороши тем, что они существуют. Именно благодаря тому, что они используют создаваемое вами программное обеспечение, вы стяжаете себе славу и известность или по крайней мере сохраняете за собой свое рабочее место. Самое плохое в пользователях то, что они зачастую делают с вашей программой такое, о чем вы и подумать не могли. И именно поэтому программу нужно снабжать кодами, управляющими исключительными ситуациями.
Вот несколько наиболее часто возникающих проблемных ситуаций, с которыми ваша программа должна уметь справляться.
Вверх
Механизм обработки исключений языка C++ предоставляет поистине неограниченную
Предположим, например, что в разных частях программы вызывается функция
Это может быть реализовано в таком виде:
Каждый раз вызывается функция AllocateBuf, и каждый раз проверяется наличие
Вверх
Предположим, что программа вызывает некую функцию foо и эта функция принимает
Чтобы самостоятельно идентифицировать ошибку, воспользуйтесь командой throw. Ее
Предположим, например, что вам нужно вычислить квадратный корень числа. Вы знаете,
Эта функция проверяет, не является ли число отрицительным. Если да, фиксируется исключительная ситуация. Если нет, вычисляется квадратный корень этого числа.
Для этого можно использовать даже классы. В таком случае вы можете создать процедуру, управляющую исключительной ситуацией, непосредственно как функцию-член этого
Команды catch в качестве параметра принимают типы данных. Так, можно создать
При этом также можно использовать мехонизм наследования. Например, класс MyError
Вверх
Вот как выглядит синтаксис кодов обработки исключений:
Обратите внимание, что на количество блоков catch ограничения нет. Когда фиксируется исключительная ситуация (ошибка), ищется тип ошибки, которому она соответствует, и
Все исключения .NET являются классами, производными от класса Exception.
Рассмотрим пример небольшой программы. В процессе ее выполнения пользователю
__gc class SquareRootException : public Exception
//Returns the square root
//Throws an exception if there is a range problem
//This routine prompts the user for a number.
Console::WriteLine(S"What is the number?");
// This is the entry point for this application
//Get numbers from the user, until the user
//Now we are finished
Console::WriteLine(S"Bye");
//Hang out until the user is finished
Console::WriteLine(S"Hit the enter key to stop the program");
return 0;
Начните чтение кодов этой программы с блока try, который предназначен для вычисления и отображения значения квадратного корня числа, введенного пользователем:
Если пользователь введет отрицательное число, оно будет передано функции
Коды catch набраны сразу же после блока try. В данном случае здесь присутствует
Итак, посмотрим, как работает эта программа. Все начинается с запуска цикла. Перед
Если пользователь ввел положительное число, вызываемая функция SquareRoct не за-
Если пользователь вводит отрицательное значение, функция SquarcRoot фиксирует
Вверх
Благодаря возможности передавать объекты классов при возникновении исключи-
Например, если ошибка вызвана отсутствием свободного места на диске, желательно
Но когда вы передаете объект класса с помощью команды throw, как компилятор
Затем, на случай обнаружения ошибки диска, наберите следующее:
Затем, благодаря тому что команды catch определяют совпадение типов
При желании для определения типа ошибки можно использовать ппростой тип данных,
Вверх
Уже созданные классы обработки исключений могут быть наследованы для создания новых производных классов. Это позволит повторно использовать готовые работающие коды.
При написании неуправляемых кодов вы не сможете наследовать класс
Преобразование типов и работа оператора catch
Если тип ошибки может быть легко преобразован к типу, на который реагирует команда catch, фиксируется соответствие.
Например, тип short можно легко преобразовать к типу int . Поэтому такая команда
Точно так же, если команда throw возвращает класс Derived, который является производным класса Base, указатель на производный класс может быть воспринят как указатель на базовый класс:
В данном случае эта команда catch также отреагирует на ошибку. Исходя из этого, при создании класса, описывающего исключение, функции-члены желательно сразу делать виртуальными.
Компилятор выполняет блок кодов первого оператора catch, который фиксирует соответствие с типом возникшей ошибки. Поэтому, если вы используете сразу несколько производных классов, перечислите вначале соответствующие им блоки catch и только затем наберите блок catch, соответствующий базовому классу. В таком случае программа будет работать правильно, и специфические исключения (представляемые производными классами) будут фиксироваться именно соответствующими им командами catch. В противном случае, если первым будет набран блок catch базового класса, он будет реагировать на все подряд исключительные ситуации, представляемые производными классами.
Вверх
Вот пять простых правил, которых следует придерживаться при написании кодов обработки исключений.
Назад |
Начало урока |
Вверх |
Вперед
Как это было раньше
Процесс устранения ошибок можно разбить на два этапа: определение факта наличия
ошибки, и ее нахождение и ликвидация. Первый этап — определение того, что ошибка действительно есть, — обычно не представляет никаких сложностей. Просто в программе в нужных местах необходимо добавить несложные коды, определяющие наличие ошибки и сообщающие что-то наподобие; "Ага, недостаточно данных. Есть ошибка!"
Когда установлено, что в программе присутствует ошибка, нужно найти источник ее возникновения и ликвидировать его. Локализация ошибки может оказаться более сложной задачей. Коды программы, обнаружившие существование ошибки, могли быть вызваны функцией, которая была вызвана другой функцией, которая, в свою очередь, была вызвана еще одной функцией, и т.д. Желательно, чтобы все эти функции могли сами определить наличие ошибки и решить, как поступать далее. Обычно к ним добавляются коды, проверяющие наличие ошибки и в случае ее отсутствия позволяющие функциям работать далее в обычном режиме,
а в случае обнаружения ошибки — досрочно прекращающие их работу.
Другими словами, набор кодов
//Вызов нескольких функций
ReadSongs();
ReadSizes();
ReadMyLips();
//Вызов нескольких функций. Если возвращаемый функцией результат
//меньше нуля, это расценивается как ошибка
nTemp = ReadSongsO ;
if (nTemp < 0)
{
nTemp = ReadSizes();
if (nTemp < 0)
{
nTemp = ReadMyLips();
if
(nTemp < 0)
{
Более того, если для функции допустимо возвращать отрицательные значения, вам придется придумать другую схему, определяющую возникновение ошибки после каждого вызова этой функции.
В общем, осуществить все это на практике — задача не из легких. На написание всех этих сложных кодов, обрабатывающих возможные ошибки, может уйти больше времени и усилий, чем на написание реально работающей части программы. К тому же эти дополнительные коды делают программу громоздкой и трудночитаемой.
Новый усовершенствованный способ обработки ошибок
проверяющих кодов после каждого вызова какой-либо функции.
Сам механизм обработки исключений состоит из двух частей: одна процедура определяет исключительную ситуацию (ее задача сказать: "О да, здесь есть ошибка!") и вторая процедура (которая, собственно, и обрабатывает найденное исключение) берет на себя управление, если какое-то исключение найдено.
туации, необходимо поместить внутри фигурных скобок, а перед скобками набрать ключевое слово try. Это будет командой компилятору: "Попробуй выполнить этот фрагмент кодов и посмотри, что будет". Далее нужно набрать ключевое слово catch и в фигурных скобках поместить коды, определяющие реакцию на возникновение ошибки. Компилятор это воспримет так: "Если возникнет какая-то проблема, действуй в соответствии с приведенными здесь инструкциями".
Вот как это выглядит на практике
int GetNumberO
{
Console::WriteLine(S"Укажите число");
nNumber = Int32 :: Parse(Console::ReadLine());
return nNumber;
тельной ситуацией такого рода? Если вы не оставите на этот случай никаких инструкций, как уже отмечалось, появится сообщение об ошибке и программа будет остановлена. Если же вы заранее сообщите компилятору, что нужно делать в такой ситуации, подобного развития событий можно избежать. Вот аналогичная функция, которая самостоятельно может справиться с проблемой, возникшей в результате ввода пользователем некорректного значения:
int GetNumber()
{
Console::WriteLine(S"Укажите число");
try
{
catch (FormatException *e)
{
Console::WriteLine("В следующий раз наберите, пожалуйста,
число. Сейчас введенное вами значение будет
воспринято как число I");
nNumbeг = 1;
return nNumber ;
Вспомните о пользователях!
чинами. Например, пользователь пытается открыть файл огромных размеров или у его компьютера
просто небольшой объем оперативной памяти.
файла, который должен быть открыт, пользователь набирает что-то вроде: "Какая разница, открывай любой". Разумеется, что неверное имя файла.
должно быть введено число в интервале от нуля до пяти, а пользователь вводит число 17.
аргумента недопустимое значение. Например, внутри программы используется процедура, тре-
бующая для себя значение из определенного диапазона, и в силу каких-то обстоятельств ей передается значение, этому диапазону не принадлежащее. Чтобы программа смогла адекватно отреагировать на такое событие, снабдите эту процедуру кодами обработки исключительных ситуаций.
предназначена для обработки текстовых файлов, а пользователь пытается открыть файл с расши-
рением ЕХЕ.
загрузить файл PASSWD.SCT, содержащий в себе список паролей. Но по какой-то причине
пользователь этот файл удалил или переместил в другую папку, и теперь программа не может
его найти.
щегося свободного пространства для этого не достаточно.
Гибкость — основное свойство механизма
обработки исключений
свободу действий. Заключая отдельные фрагменты кодов в фигурные скобки оператора try.
вы можете сами определять, какие из них нужно проврять на наличие ошибок, а какие нет.
Для одного и того же фрагмента кодов можно оставить инструкции на случай возникновения
самых разных ошибок. И наконец, для одной и той же ошибки, возникающей в разных частях
программы, можно оставить различные инструкции, поместив их в фигурные скобки соответ-
ствующих операторов catch.
AllocateBuf. В одной части программы эта функция вызывается для того, чтобы иыдедичь
память для файла с данными, который должен быть сохранен, а в другой части — чтобы выделить память для фотоизображения.
//Попытка сохранения файла
try
{
SaveFile() ;
catch (Errorl e)
{
//Попытка выделить память для фотографии
try
{
Proce:>sPhoto ( ) ;
catch (Errorl e]
{
одной и той же ошибки: невозможность выделения памяти. Но в каждом случае про-
грамма реагирует по-разному, и на экране отображаются соответствующие случаю со-
общения.
Определение собственных исключительных ситуаций
значения нескольких параметров. Допустим, что заранее известно: если значение, скажем,
первого параметра будет меньше (или больше) некоторого числа, то ситуация выйдет из под
контроля либо будет развиваться в совершенно неприемлемом направлении. Как в этом случае указать комилятору на наличие ошибки?
выполнение будет означать сигнал о возникновении исключительной ситуации, и компилятор
перейдет к инструкциям catch, следующим за текущим блоком try.
что эта операция некорректна для отрицательных чисел, а потому можете набрать код, подобный следующему:
double SquareRoot(int nNumber)
{
if (nNumber < 0)
{
положительным");
return Math::Sqrt(nNumber);
Вы можете определить столько собственных исключительных ситуаций, сколько будет необходимо. Каждая команда throw передает отдельный тип данных. Можно создать любое количество типов данных, определить для них конкретные значения и использовать эту информацию как вспомогательную для управления процессом обработки исключения.
класса и передать объект данного класса кодам c a t c h с помощью команды throw. Например, можно создать класс MyError и при возникновении исключительной ситуации воспользоваться командой throw, которая создаст объект класса MyError и передаст его кодам catch.
класс ChoiceError, который будет идентифицироваться одной командой catch, и класс
MyError, на который будет реагировать другая команда catch.
может быть производным от класса ChoiceError.
Поговорим о синтаксисе
try
Чтобы зафиксировать факт возникновения ошибки, наберите
{
инструкции;
catch (тип__ошибки_1)
{
catch (тип_ошибки_2]
{
throw тип_ошибки;
выполняются оставленные на этот случай инструкции.
Объект класса Exception содержит свойство Message (Сообщение). Вы може-
те присвоить значение этому свойству (определить содержание сообщения) в момент создания объекта Exception.
снова и снова предлагается ввести число, из каждого введенного числа извлекается квадрат-
ный корень, а полученное значение отображается на экране. Если пользователь вводит значе-
ние, которое не может быть преобразовано в число (например, он вводит слово), программа
реагирует на эту ошибку и вычисляет квадратный корень числа, заданного по умолчанию. Ес-
ли же пользователь вводит отрицательное число, программа также реагирует на эту ошибку,
что сопровождается отображением специального сообщения.
// SquareRoot
// Exception handling
#include "stdafx.h"
#using <mscorlib.dll>
using namespace System;
{
SquareRootException(String *s) : Exception(s)
{
};
double SquareRoot(int nNumber)
{
{
return Math::Sqrt(nNumber);
//It returns the value of the number
int GetNumber()
{
try
{
catch (FormatException *e)
{
Console::WriteLine("Next time, please enter an integer. I'm going to guess you meant 1.");
nNumber = 1;
return nNumber;
#ifdef _UNICODE
int wmain(void)
#else
int main(void)
#endif
{
//types 0
while (nNumber = GetNumber())
{
try
{
Console::ReadLine();
try
{
SquareRoot(nNumber).ToString());
SquareRoot, которая отреагирует на это фиксированием исключения с помощью команды
throw и передачей сообщения об ошибке конструктору SauareRootException:
if (nNumber < 0 )
{
только одна команда catch, которая реагирует на все исключения, фиксируемые внутри
блока try. Информация об исключениях передастся в виде объекта класса Exception, который имеет собственное имя. Процедура eaten отображает на экране значения свойства Message этого объекта:
catch (Exception *o)
{
выполнением каждой итерации пользователю предлагается ввести какое-то число. Если
пользователь вводит то. что не может быть преобразовано к значению типа integer, функ-
ция GetNunber фиксирует ошибку и выдает предупреждающее сообщение. При этом вы-
числяется квадратный корень значения, заданного но умолчанию (в данном случае это зна-
чение равно числу 1). Внутри цикла предпринимается попытка выполнить коды блока try,
цель которых вычислить квадратный корень введенного числа и отобразить полученное
значение нa экране.
фиксирует ошибку, в резельтате чего вес коды блока t r y будут благополучно выполнены и
пользователь увидит на экране вычисленный результат. Затем программа переходит к сле-
дующей итерации, и, если пользователь не вводит нулевое значение, снова происходит по-
пытка выполнить коды блока try.
возникновение исключительной ситуации и передаст управление колам блока catch. В дан-
ном случае их выполнение приводит к отображению на экране специального предупреждаю-
щего сообщения. Затем программа переходит к следующей итерации. Как только пользова-
тель вводит число 0 (нуль), программа завершает работу.
Все это хорошо, но несколько запутанно
тельных ситуаций решаются две проблемы. Во-первых, можно определить столько ис-
ключений, сколько будет необходимо, а во-вторых, с объектами можно передавать всю
необходимую информацию об ошибке и мерах, которые следует предпринять в связи с
ее возникновением.
было бы указать название этого диска. Если ошибка связана с тем, что поврежден какой-то
файл, неплохо было бы указать его имя и адрес последнего удачно считанного фрагмента.
Вы могли бы даже определить процедуру, которая попытается восстановить этот файл.
определит, какой из блоков catch должен быть использован? Это важный момент.
Компилятор проверяет тип данных, которые передаются командой throw, и ищет ту
команду catch, которая знает, как данные такого типа должны быть обработаны. Так,
вы можете создать классы FileCorrupt (повреждение файла), DiskError (ошибка
диска), MemoryError (ошибка памяти), каждый из которых будет содержать в себе ин-
формацию, описывающую данную проблему. Сами классы могут кардинально отличать-
ся друг от друга.
DiskError foo;
На случай, если будет поврежден файл, наберите такой код:
//Здесь наберите коды, наполняющие содержанием объект foo
throw foo;
CorruptFile bar;
//Здесь наберите коды, наполняющие содержанием объект bar
throw bar;
данных или классов, управление передается соответствующему блоку
catch:
Использование классов позволяет весьма элегантно достичь впечатляющей гибкости
//Инструкции на случай возникновения ошибки диска
catch (DiskError MyError)
{
}
//Инструкции на случай повреждения файла
catch (CorruptFile MyError)
{}
в управлении исключительными ситуациями, поскольку классов, описывающих ошибки,
можно создать любое количество, а в самих классах можно обозначить всю необходимую информацию.
например char *. Но использовать классы все же гораздо предпочтительнее, поскольку с
ними вы можете передать полную и четкую информацию о том, в чем именно заключается
проблема. Процедура, в которой ошибка была обнаружена, располагает всеми необходимыми
данными. Процедура, которая будет обрабатывать эту ошибку, должна получить эти данные,
чтобы помочь пользователю справиться с возникшей проблемой.
Еще одно преимущество использования классов состоит в том, что вы можете снабдить их функциями-членами. Например, можно создать внутри класса набор процедур,
отображающих сообщения на экране и помогающих корректировать ситуацию или упралять ею.
Наследование классов, описывающих исключения
Кроме того, создать собственный, описывающий исключительную ситуацию класс можно
путем наследования класса Exception. Это позволит использовать встроенные функциональные возможности, а определенные вами исключения будут выглядеть так же, как и остальные исключения .NET.
Exception. Вам в любом случае придется самостоятельно создавать собственный класс.
catch реагирует на исключение, генерируемое командой throw:
catch
(int k)
{}
...
throw (short i = 6);
catch (Base foo)
{}
...
Derived foo;
throw foo;
Пять правил исключительного благополучия
столкнуться ваша программа.
типу, который должна идентифицировать команда catch. Например, если команда throw передает тип DiskError *, команда c a t ch также должна реагировать на тип DiskError *, а не на DiskError.
catch, работа программы автоматически прерывается. Будьте готовы к этому.
К счастью, поскольку объекты, сохраненные в стеке, уничтожаются, вызываются
их деструкторы и программа благополучно освобождает выделенную для ее вы-
полнения память.
Содержание