День 20-й. Отслеживание исключительных ситуаций и ошибок

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

• Что представляют собой исключительные ситуации

• Как перехватываются и обрабатываются исключения

• Что такое наследование исключений

• Как использовать исключения в общей структуре отслеживания и устранения ошибок

• Что представляет собой отладка программы

Ошибки, погрешности, ляпусы и "гнилой" код

К сожалению, все программисты допускают ошибки. Чем больше программа, тем выше вероятность возникновения в ней ошибок, многие из которых до поры до времени остаются незамеченными и попадают в конечный программный продукт, уже выпущенный на рынок. С этой печальной истиной трудно смириться, поэтому создание надежных, свободных от ошибок программ должно быть задачей номер один для каждого программиста, серьезно относящегося к своему делу.

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

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

Второй тип — синтаксические ошибки, т.е. использование неправильной идиомы, функции или структуры. Эти два типа ошибок самые распространенные, поэтому именно на них сосредоточено внимание программистов.

Теория и практика неопровержимо доказали, что чем позже в процессе разработки обнаруживается проблема, тем дороже стоит ее устранение. Оказывается, что проблемы или ошибки в программах дешевле всего обойдутся компании в том случае, если своевременно принять меры по предупреждению их появления. Не слишком дорого обойдутся и те ошибки, которые распознаются компилятором. Стандарты языка C++ заставляют разработчиков создавать такие компиляторы, которые способны обнаруживать как можно больше ошибок на этапе компиляции программы.

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

Еще большей проблемой, чем логические или синтаксические ошибки, является недостаточная устойчивость программ, т.е. программа сносно работает в том случае, если пользователь вводит данные, которые предусматривались, но дает сбой, если по ошибке, например, вместо числа будет введена буква. Другие программы внезапно зависают из-за переполнении памяти, или при извлечении из дисковода гибкого диска, или при потере линии модемом.

Чтобы повысить устойчивость программ, программисты стремятся предупредить все варианты непредвиденных ситуаций. Устойчивой считают программу, которая может справляться во время работы с любыми неожиданностями: от получения нестандартных данных, введенных пользователем, до переполнения памяти компьютера.

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

Исключительные ситуации

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

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

• привести программу к аварийному останову;

• информировать пользователя о случившемся и корректно выйти из программы;

• информировать пользователя и позволить ему сделать попытку восстановить рабочее состояние программы и продолжить работу;

• выбрать обходной путь и продолжить работу программы, не беспокоя пользователя.

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

Язык C++ предоставляет безопасные интегрированные методы отслеживания всех возможных исключительных ситуаций, возникающих во время выполнения программы.

Несколько слов о "гнилом" коде

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

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

Примечание: "Гнилой" код — это шутливый термин, придуманный программистами для объяснения того, как хорошо отлаженные программы вдруг становятся ненадежными и неэффективными. Об этом явлении не стоит забывать, ведь программы часто бывают чрезвычайно сложными, из-за чего многие ошибки, погрешности и ляпсусы могут долгое время оставаться в тени, пока не проявят себя во всей красе. Для защиты от подобной "плесени" нужно писать код таким образом, чтобы самим было несложно поддерживать его работоспособность.

Это означает, что ваш код должен быть подробно прокомментирован, даже если вы и не предполагаете, что кто-то другой, кроме вас, может заглянуть в него. Когда пройдет месяцев шесть после того, как вы передадите свой код заказчику, вы сами будете смотреть на свою программу глазами постороннего человека и удивляться тому, как можно было написать такой непонятный и извилистый код, надеясь при этом на успешную работу.

Исключения

В C++ исключение — это объект, который передается из области кода, где возникла проблема, в ту часть кода, где эта проблема обрабатывается. Тип исключения определяет, какая область кода будет обрабатывать проблему и как содержимое переданного объекта, если он существует, может использоваться для обратной связи с пользователем. Основная идея использования исключений довольно проста.

• Фактическое распределение ресурсов (например, распределение памяти или захват файла) обычно осуществляется в программе на низком уровне.

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

• Исключения обеспечивают переход от кода распределения ресурсов к коду обработки исключительной ситуации. Желательно, чтобы код обработки исключительной ситуации не только отслеживал ее появление, но и мог обеспечить элегантный выход из исключительной ситуации, например отмену выделения памяти в случае ее нехватки.

Как используются исключения

Создаются блоки try для помещения в них фрагментов кода, которые могут вызвать проблему, например:

try

{

SomeDangerousFunction();

}

Исключения, возникшие в блоках try, обрабатываются в блоках catch, например:

try

{

SomeDangerousFunction();

}

catch(OutOfMemory)

{

// предпринимаем некоторые действия

}

catch(FileNotFound)

{

// предпринимаем другие действия

}

Ниже приведены основные принципы использовании исключений.

1. Идентифицируйте те области программы, где начинается выполнение операции, которая могла бы вызвать исключительную ситуацию, и поместите их в блоки try.

2. Создайте блоки catch для перехвата исключений, если таковые возникнут, очистки выделенной памяти и информирования пользователя соответствующим образом. В листинге 20.1 иллюстрируется использование блоков try и catch.

Исключения — это объекты, которые используются для передачи информации о проблеме.

Блок try — это заключенный в фигурные скобки блок, содержащий фрагменты программы, способные вызвать исключительные ситуации.

Блок catch — это блок, который следует за блоком try и в котором выполняется обработка исключений.

При возникновении исключительной ситуации управление передается блоку catch, который следует сразу за текущим блоком try.

Примечание: Некоторые очень старые компиляторы не поддерживают обработку исключений. Однако обработка исключений является частью стандарта ANSI C++. Все современные версии компиляторов полностью поддерживают эту возможность. Если у вас устаревший компилятор, вы не сможете скомпилировать и выполнить листинги, приведенные на этом занятии. Однако все же стоит прочитать представленный материал до конца, а затем вернуться к нему после обновления своего компилятора.

Листинг 20.1. Возникновение исключительной ситуации

1: #include <iostream.h>

2:

3: const int DefaultSize = 10;

4:

5: class Array

6: {

7: public:

8: // конструкторы

9: Array(int itsSize = DefaultSize);

10: Array(const Array &rhs);

11: ~Array() { delete [] pType;}

12:

13: // операторы

14: Array& operator=(const Array&);

15: int& operator[](int offSet);

16: const int& operator[](int offSet) const;

17:

18: // методы доступа

19: int GetitsSize() const { return itsSize; }

20:

21: // функция-друг

22: friend ostream& operator<< (ostream&, const Array&);

23:

24: class xBoundary { } ; // определяем класс исключений

25: private:

26: int *pType;

27: int itsSize;

28: };

29:

30:

31: Array::Array(intsize):

32: itsSize(size)

33: {

34: рТуре = new int[size];

35: for (int i = 0; i<size; i++)

36: pType[i] = 0;

37: }

38:

39:

40: Array& Array::operator=(const Array &rhs)

41: {

42: if (this == &rhs)

43: return *thts;

44: delete [] pType;

45: itsSize = rhs.GetitsSiza();

46: pType = new int[itsSize];

47: for (int i = 0; i<itsSize; i++)

48: pType[i] = rhs[i];

49: return *this;

50: }

51:

52: Array::Array(const Array &rhs)

53: {

54: itsSize = rhs.GetitsSize();

55: pType = new int[itsSize];

56: for (int i = 0; i<itsSize; i++)

57: pType[i] = rhs[i];

58: }

59:

60:

61: int& Array::operator[](int offSet)

62: {

63: int size = GetitsSize();

64: if (offSet >= 0 && offSet < GetitsSize())

65: return pType[offSet];

66: throw xBoundary();

67: return pType[0]; // требование компилятора

68: }

69:

70:

71: const int& Array::operator[](int offSet) const

72: {

73: int mysize = GetitsSize();

74: if (offSet >= 0 && offSet < GetitsSize())

75: return pType[offSet];

76: throw xBoundary();

77: return pType[0]; // требование компилятора

78: }

79:

80: ostream& operator<< (ostream& output, const Array& theArray)

81: {

82: for (int i = 0; i<theArray,GetitsSize(); i++)

83: output << "[" << i << "] " << theArray[i] << endl;

84: return output;

85: }

86:

87: int main()

88: {

89: Array intArray(20);

90: try

91: {

92: for (int ] << 0; j< 100; j++)

93: {

94: intArray[j] = j;

95: cout << "intArray[" << j << "] okay..." << endl;

96: }

97: }

98: catch (Array::xBoundary)

99: {

100: cout << "Unable to process your input!\n";

101: }

102: cout << "Done.\n";

103: return 0;

104: }

Результат:

intArray[0] okay...

intArray[1] okay...

intArray[2] okay...

intArray[3] okay...

intArray[4] okay...

intArray[5] okay...

intArray[6] okay...

intArray[7] okay...

intArray[8] okay...

intArray[9] okay...

intArray[10] okay...

intArray[11] okay...

intArray[12] okay...

intArray[13] okay...

intArray[14] okay...

intArray[15] okay...

intArray[16] okay...

intArray[17] okay...

intArray[18] okay...

intArray[19] okay...

Unable to process your input!

Done.

Анализ: В листинге 20.1 представлен несколько усеченный класс Array, основанный на шаблоне, разработанном на занятии 19.

В строке 24 объявляется новый класс xBoundary внутри объявления внешнего класса Array.

В этом новом классе ни по каким внешним признакам нельзя узнать класс обработки исключительных ситуаций. Он чрезвычайно прост и не содержит никаких данных и методов. Тем не менее это вполне работоспособный класс.

На самом деле было бы неправильно говорить, что он не содержит никаких методов, потому что компилятор автоматически назначает ему стандартный конструктор, деструктор, конструктор-копировщик и оператор присваивания (=), поэтому у него фактически есть четыре метода, но нет данных.

Обратите внимание на то, что его объявление внутри класса Array служит только для объединения двух классов. Как описано в главе 15, класс Array не имеет никакого особого доступа к классу xBoundary, да и класс xBoundary не наделен преимущественным доступом к членам класса Array.

В строках 61—68 и 71—78 операторы индексирования ([]) замещены таким образом, чтобы предварительно анализировать введенный индекс смещения и, если оно окажется вне допустимого диапазона, обратиться к классу xBoundary для создания исключения. Назначение круглых скобок состоит в том, чтобы отделить обращение к конструктору класса xBoundary от использования константы перечисления. Обратите внимание, что некоторые компиляторы компании Microsoft требуют, чтобы определение функции в любом случае заканчивалось строкой с оператором return, согласующейся по типу с прототипом функции (в данном случае возвращение ссылки на целочисленное значение), несмотря на то что в случае возникновения исключительной ситуации в строке 66 выполнение программы никогда не достигнет строки 67. Этот пример говорит о том, что логические ошибки не чужды даже компании Microsoft!

В строке 90 ключевым словом try начинается блок отслеживания исключительных ситуаций, который оканчивается в строке 97. Внутри этого блока в массив, объявленный в строке 89, добавляется 101 целое число.

В строке 98 объявлен блок catch для перехвата исключений класса xBoundary.

В управляющей программе в строках 87—104 создается блок try, в котором инициализируется каждый член массива. Когда переменная j (строка 92) увеличится до 20, осуществляется доступ к члену, соответствующему смещению 20. Это приводит к невыполнению условия проверки в строке 64, в результате чего замещенный оператор индексирования operator[] генерирует исключение класса xBoundary (строка 66).

Управление программой передается к блоку catch в строке 98, и исключение перехватывается или обрабатывается оператором catch в той же строке, которая печатает сообщение об ошибках. Программа доходит до конца блока catch в строке 100.

Наши рекомендации