Шаг 3.
Синтаксис и семантика генерации и обработки исключений

    На этом шаге мы рассмотрим правила записи и использования обработки исключений.

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

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

    Механизм исключений позволяет переносить анализ и обработку ситуации из точки ее возникновения (Throw Point), в другое место программы, специально предназначенное для ее обработки. Кроме того, из точки возникновения ситуации в место ее обработки (в список обработчиков исключений) может быть передано любое количество необходимой информации, например, сведения о том, какие данные и действия привели к возникновению такой ситуации.

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

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

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

    Контролируемый блок идентифицируется ключевым словом try. Каждый обработчик исключений начинается со служебного слова catch. Общая схема размещения указанных блоков:

    try
      { операторы_контролируемого_блока } 
    catch (спецификация исключения)
      { операторы_обработчика_исключений }
    .    .    .    .
    catch (спецификация_исключения)
      { операторы_обработчика_исключений }

    В приведенных на предыдущем шаге программах использовалось по одному обработчику исключений. Это объясняется "однотипностью" формируемых исключений (только типа const char * или только типа DATA). В общем случае в контролируемом блоке могут формироваться исключения разных типов и обработчиков может быть несколько. Размещаются они подряд, последовательно друг за другом и каждый обработчик "настроен" на исключение конкретного типа. Спецификация исключения, размещенная в скобках после служебного слова catch, имеет три формы:

    catch   (тип имя)   {   ...   } 
    catch   (тип)   {   ...   } 
    catch   (...)   {   ...   }

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

    Второй вариант не предполагает использования значения исключения. Для обработчика важен только его тип и факт его получения.

    В третьем случае (многоточие) обработчик реагирует на любое исключение независимо от его типа. Так как сравнение "посланного" исключения со спецификациями обработчиков выполняется последовательно, то обработчик с многоточием в качестве спецификации следует помещать только в конце списка обработчиков. В противном случае все возникающие исключения "перехватит" обработчик с многоточием в качестве спецификации. В случае, если описать его не последним обработчиком, компилятор Borland C++, к примеру, выдаст сообщение об ошибке:

    The   ' . . . '   handler must be  last in function   ...()
    (Обработчик '...' должен идти последним в функции ...()).

    Продемонстрируем некоторые из перечисленных особенностей обработки исключений еще одной программой:

//EXC3_1.СРР  - исключения без  передачи информации.
#include <iostream.h>
class ZeroDivide {};   // Класс без компонентов.
class Overflow {};      // Класс без компонентов.
// Определение  функции с генерацией исключений:
float div (float n,   float d)
{
  if  (d == 0.0)  throw ZeroDivide();  // Вызов конструктора.
  double b = n/d;
  if  (b > 1e+30)   throw Overflow(); //  Вызов конструктора.
  return b;
}
float x = 1e-20,  y = 5.5,   z = 1e+20,   w = 0.0;
// Вызывающая функция с выявлением и обработкой исключений:
void RR(void)
{ // Контролируемый блок:
    try  { y = div(4.4,w);
           z = div(z,x);
         }
  // Последовательность обработчиков исключений:
  catch (Overflow)
	 { cerr <<"\nПереполнение "; z = 1e30; }
  catch (ZeroDivide)
	 { cerr << "\nДеление на нуль "; w = 1.0; }
}
void main()
{
  // Вызов функции div() с нулевым делителем w:
  RR() ;
  //  Вызов  функции  div()  с арифметическим переполнением:
  RR() ;
  cout << "\nРезультат: y = " << y;
  cout << "\nРезультат: z = " << z;
}
Текст этой программы можно взять здесь.

    Результат выполнения программы:

    Деление на нуль
    Переполнение
    Результат: у = 4.4 
    Результат: z = 1e+30

    В программе в качестве типов для исключений используются классы без явно определенных компонентов. Конструктор ZeroDivide() вызывается и формирует безымянный объект (исключение) при попытке деления на нуль. Конструктор Overflow() используется для создания исключений, когда значение результата деления превысит величину 1е+30. Исключения указанных типов не передают содержательной информации. Эта информация не предусмотрена и в соответствующих обработчиках исключений. При первом обращении к функции RR() значение глобальной переменной y не изменяется, так как управление передается обработчику исключений

    catch   (ZeroDivide)

    При его выполнении выводится сообщение, и делитель w (глобальная переменная) устанавливается равным 1.0. После обработчика исключения завершается функция RR(), и вновь в основной программе вызывается функция RR(), но уже с измененным значением w. При этом обращение div(4.4,w) обрабатывается безошибочно, а вызов div(z,x) приводит к формированию исключения типа Overflow. Его обработка в RR() предусматривает печать предупреждающего сообщения и изменение значения глобальной переменной z. Обработчик catch (ZeroDivide) в этом случае пропускается. После выхода из RR() основная программа выполняется обычным образом и печатаются значения результатов "деления", осуществленного с помощью функции div().

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




Предыдущий шаг Содержание Следующий шаг