Шаг 2.
Общие принципы механизма обработки исключений (окончание)

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

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

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

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

    На каждом шаге алгоритма выполняются сравнения:

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

//EXC2_1.СРР  -  GCM  -  Greatest Common Measure.
#include <iostream.h>
// Определение функции с генерацией, контролем и 
// обработкой исключений: 
int GCM(int x, int y) 
{ // Контролируемый блок: 
  try  
  {   
  if (x==0   || y==0)   throw  "\nОдно из значений нуль!";
  if (x < 0) throw "\nПервый параметр отрицательный.";
  if (y < 0) throw "\nВторой параметр отрицательный.";
  while   (x != y)
  {
    if (x > y) x -= y;
      else y -= x ; }
   return x;
  } // Конец контролируемого блока.
  // Обработчик исключений стандартного типа  "строка": 
  catch   (const char  *report)
  {   
    cerr << report << " x = "  << x << ", y = "  << y; 
    return 0; 
  }
} // Конец определения  функции.
void main() 
{ // Безошибочный вызов:
  cout << "\nGCM(66,44) = " <<  GCM(66,44);
  // Нулевой параметр:
  cout << "\nGCM(0,7) = " << GCM(0,7);
  // Отрицательный параметр: 
  cout << "\nGCM(-12,8) = " << GCM(-12,8); 
}
Текст этой программы можно взять здесь.

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

GCM(66,44) = 22
Одно из значений нуль! x = 0, у = 7
GCM(0,7) = 0
Первый параметр отрицательный. x = -12, у = 8
GCM(-12,8) = 0

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

    Служебное слово try определяет следующий за ним набор операторов в фигурных скобках как контролируемый блок. Среди операторов этого контролируемого блока три условных оператора анализируют значения параметров. При истинности проверяемого условия в каждом из них с помощью оператора генерации throw формируется исключение, т.е. создается объект - символьная строка, имеющая атрибуты const char *. При выполнении любого из операторов throw естественная последовательность исполнения операторов функции прерывается и управление автоматически без каких-либо дополнительных указаний программиста передается обработчику исключений, помещенному непосредственно за контролируемым блоком. (Это чуть-чуть похоже на оператор goto). Так как в данной программе обработчик исключений локализован в теле функции, то ему доступны значения ее параметров (х, у). Поэтому при возникновении каждого исключения в поток вывода сообщений об ошибках cerr выводится символьная строка с информацией о характере ошибки (нулевые параметры или отрицательные значения параметров) и значения параметров, приведшие к возникновению особой ситуации и к генерации исключения. Здесь же в составном операторе обработчика исключений выполняется оператор return 0;. Тем самым при ошибках возвращается необычное нулевое значение наибольшего общего делителя. При естественном окончании выполнения функции GCM(), когда становятся равными значения х и у, функция возвращает значение x.

    Так как по умолчанию и выходной поток cout, и поток cerr связываются с экраном дисплея, то результаты как правильного, так и ошибочного выполнения функции GCM() выводятся на один экран. Обратите внимание, что исключения (const char *) одного типа посылаются в ответ на разные ситуации, возникающие в функции.

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

    В следующей программе используется другой вариант функции для определения наибольшего общего делителя (НОД) двух целых чисел, передаваемых в качестве аргументов. В теле указанной функции GCM_NEW() нет контролируемого блока и обработчика исключений, но сохранены "генераторы" исключений. Контролируемый блок и обработчик исключений перенесены в функцию main(). Все вызовы функции (верный и с ошибками в параметрах) помещены в контролируемый блок.

//EXC2_2.СРР - функция с генерацией, но без контроля исключений.
#include <iostream.h>
int GCM_NEW(int x, int y) //  Определение  функции.
{
  if (x == 0 || y == 0)   throw   "\nОдно из значений нуль!";
  if (x <  0)   throw   "\nПервый параметр отрицательный.";
  if (y <  0)   throw   "\nВторой параметр отрицательный.";
  while (x != y)
  {  
    if (x > y)  x -= y; 
   else y -= x;
  }
  return x;
}
// Контроль  и  обработка  исключений в  вызывающей  программе.
void main ()
{  
  try  // Контролируемый блок.
  { 
    cout << "\nGCM_NEW(66,44) = " << GCM_NEW(66,44);
    cout << "\nGCM_NEW(0,7)   = " << GCM_NEW(0,7);
    cout << "\nGCM_NEW(-12,8) = " << GCM_NEW(-12,8);
  }
  catch (const char * report) // Обработчик исключений. 
    { cerr << report; }
}
Текст этой программы можно взять здесь.

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

    GCM_NEW(66,44)  =  22 
    Одно из значений нуль!

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

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

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

//EXC2_3.CPP  -  исключения глобального пользовательского типа.
#include <iostream.h>
struct DATA  // Глобальный класс объектов-исключений.
{ 
  int n, m;
  char *s;
  DATA(int x, int y, char *c) // Конструктор класса DATA.
   { n = x; m = y; s = c;}
};
int GCM_ONE (int x, int y)    // Определение функции.
{ 
  if (x == 0 || y == 0)   throw   DATA(x,y,"\nОдно из значений нуль!");
  if (x <  0)   throw   DATA(x,y,"\nПервый параметр отрицательный.");
  if (y <  0)   throw   DATA(x,y,"\nВторой параметр отрицательный.");
  while (x != y)
    { if (x > y) x -= y; else y -= x; }
  return x;
}
void main() 
{ 
  try
    { cout << "\nGCM_ONE(66,44) = " << GCM_ONE(66,44);
      cout << "\nGCM_ONE(0,7)   = " << GCM_ONE(0,7);
      cout << "\nGCM_ONE(-12,8) = " << GCM_ONE(-12,8);
    }
  catch (DATA d)
    { cerr << d.s  << " x = " << d.n << ", y = " << d.m; }
}
Текст этой программы можно взять здесь.

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

    GCM_ONE(66,44)   =  22 
    Одно из значений нуль!   x =  0,   у  =  7

    Отметим, что объект класса DATA формируется в теле функции при выполнении конструктора класса. Если бы этот объект не был исключением, он был бы локализован в теле функции и недоступен в точке ее вызова. Но по определению исключений они создаются как временные статические объекты. В данном примере исключения как безымянные объекты класса DATA формируются в теле функции, вызываемой из контролируемого блока. В блоке обработчика исключений безымянный объект типа DATA инициализирует переменную (параметр) DATA d и тем самым информация из исключения становится доступной в теле обработчика исключений, что демонстрирует результат.

    Итак, чтобы исключение было достаточно информативным, оно должно быть объектом класса, причем класс обычно определяется специально. В приведенной программе класс для исключений определен как глобальный, т.е. он доступен как в функции GCM_ONE(), где формируются исключения, так и в основной программе, где выполняется контроль за ними и, при необходимости, их обработка. Внешне исключение выглядит как локальный объект той функции, где око формируется. Однако исключение не локализуется в блоке, где использован оператор его генерации. Исключение как объект возникает в точке генерации, распознается в контоолируемом блоке и передается в обработчик исключений. Только после обработки оно может исчезнуть. Нет необходимости в глобальном определении класса объектов-исключений. Основное требование к нему - известность в точке формирования (throw) и в точке обработки (catch). Следующий пример иллюстрирует сказанное. Класс (структура) DATA определен отдельно как внутри функции GCM_TWO(), так и в основной программе. Никаких утверждений относительно адекватности этих определений не делается. Но передача исключений проходит вполне корректно, что демонстрируют результаты.

//EXC2_4.СРР - локализация определений типов (классов) исключений.
#include <iostream.h> 
int GCM_ONE(int x, int y)
{ 
  struct DATA   // Определение типа локализовано в функции.
   { int n, m; 
     char *s;
     DATA (int x, int y, char *c) // Конструктор класса DATA. 
       { n = x; m = y; s = c;}
   };
  if (x = 0 || y = 0) throw DATA (x,y,"\nОдно из значений нуль!"); 
  if (x < 0) throw DATA(x,y,"\nПервый параметр отрицательный."); 
  if (y < 0) throw DATA(x,y,"\nВторой параметр отрицательный."); 
  while (x != y)
    { if (x > y) x -= y; else y -= x;  }
  return x;
}

void main()
{ 
  struct DATA   // Определение типа локализовано в main().
   { int n, m; 
     char *s;
     DATA (int x, int y, char *c) // Конструктор класса DATA. 
       { n = x; m = y; s = c;}
   };
  try
    { cout << "\nGCM_ONE(66,44) = " << GCM_ONE(66,44); 
      cout << "\nGCM_ONE(-12,8) = " << GCM_ONE(-12,8); 
      cout << "\nGCM_ONE(0,7)   = " << GCM_ONE(0,7);
    } 
  catch (DATA d)
    { cerr << d.s  << " x = " << d.n << ", y = " << d.m; }
}
Текст этой программы можно взять здесь.

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

    GCM_TWO(66,44)   = 22 
    Первый параметр отрицательный. x =  -12,  у = 8

    На следующем шаге мы рассмотрим синтаксис и семантику генерации и обработки исключений .




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