С++ для начинающих

       

Использование исключений


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

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

Механизм исключений делится на две основные части:

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

исключения. Для этого в С++ предусмотрен специальный оператор throw. Возбуждение исключения в случае невозможности открыть некоторый файл выглядит следующим образом:

if ( !infile ) {

  string errMsg("Невозможно открыть файл: ");

  errMsg += fileName;

  throw errMsg;

}

Место программы, в котором исключение обрабатывается. При возбуждении исключения нормальное выполнение программы приостанавливается и управление передается обработчику исключения. Поиск нужного обработчика часто включает в себя раскрутку так называемого стека вызовов программы. После обработки исключения выполнение программы возобновляется, но не с того места, где произошло исключение, а с точки, следующей за обработчиком. Для определения обработчика исключения в С++ используется ключевое слово catch. Вот как может выглядеть обработчик для примера из предыдущего абзаца:

catch (string exceptionMsg) {

  log_message (exceptionMsg);


  return false;

}

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

int* stats (const int *ia, int size)

{

  int *pstats = new int [4];

  try {

    pstats[0] = sum_it (ia,size);

    pstats[1] = min_val (ia,size);

    pstats[2] = max_val (ia,size);

  }

  catch (string exceptionMsg) {

    // код обработчика

  }

  catch (const statsException &statsExcp) {

    // код обработчика

  }

  pstats [3] = pstats[0] / size;

  do_something (pstats);

  return pstats;

}

В данном примере в теле функции stats() три оператора заключены в try-блок, а четыре – нет. Из этих четырех операторов два способны возбудить исключения.

1) int *pstats = new int [4];

Выполнение оператора new может окончиться неудачей. Стандартная библиотека С++ предусматривает возбуждение исключения bad_alloc в случае невозможности выделить нужное количество памяти. Поскольку в примере не предусмотрен обработчик исключения bad_alloc, при его возбуждении выполнение программы закончится аварийно.

2) do_something (pstats);

Мы не знаем реализации функции do_something(). Любая инструкция этой функции, или функции, вызванной из этой функции, или функции, вызванной из функции, вызванной этой функцией, и так далее, потенциально может возбудить исключение. Если в реализации функции do_something и вызываемых из нее предусмотрен обработчик такого исключения, то выполнение stats() продолжится обычным образом. Если же такого обработчика нет, выполнение программы аварийно завершится.

Необходимо заметить, что, хотя оператор

pstats [3] = pstats[0] / size;

может привести к делению на ноль, в стандартной библиотеке не предусмотрен такой тип исключения.

Обратимся теперь к инструкциям, объединенным в try-блок. Если в одной из вызываемых в этом блоке функций – sum_it(), min_val() или max_val() –произойдет исключение, управление будет передано на обработчик, следующий за try-блоком и перехватывающий именно это исключение. Ни инструкция, возбудившая исключение, ни следующие за ней инструкции в try-блоке выполнены не будут. Представим себе, что при вызове функции sum_it() возбуждено исключение:



throw string ("Ошибка: adump27832");

Выполнение функции sum_it() прервется, операторы, следующие в try- блоке за вызовом этой функции, также не будут выполнены, и pstats[0] не будет инициализирована. Вместо этого возбуждается исключительное состояние и исследуются два catch-обработчика. В нашем случае выполняется catch с параметром типа string:

catch (string exceptionMsg) {

  // код обработчика

}

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

pstats [3] = pstats[0] / size;

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

Вот пример:

catch (string exceptionMsg) {

  // код обработчика

  cerr << "stats(): исключение: "

       << exceptionMsg

       << endl;

  delete [] pstats;

  return 0;

}

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

Функция stats() умеет реагировать на два типа исключений: string и statsException. Исключение любого другого типа игнорируется, и управление передается в вызвавшую функцию, а если и в ней не найдется обработчика, – то в функцию более высокого уровня, и так до функции main().При отсутствии обработчика и там, программа аварийно завершится.

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

catch (...) {

  // обрабатывает любое исключение,

  // однако ему недоступен объект, переданный

  // в обработчик в инструкции throw

}

(Детально обработка исключительных ситуаций рассматривается в главах 11 и 19.)

Упражнение 2.18

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

int *alloc_and_init (string file_name)



{

  ifstream infile (file_name)

  int elem_cnt;

  infile >> elem_cnt;

  int *pi = allocate_array(elem_cnt);

  int elem;

  int index=0;

  while (cin >> elem)

    pi[index++] = elem;

  sort_array(pi,elem_cnt);

  register_data(pi);

  return pi;

}

Упражнение 2.19

В предыдущем примере вызываемые функции allocate_array(), sort_array() и register_data() могут возбуждать исключения типов noMem, int и string соответственно. Перепишите функцию alloc_and_init(), вставив соответствующие блоки try и catch для обработки этих исключений. Пусть обработчики просто выводят в cerr сообщение об ошибке.

Упражнение 2.20

Усовершенствуйте функцию alloc_and_init() так, чтобы она сама возбуждала исключение в случае возникновения всех возможных ошибок (это могут быть исключения, относящиеся к вызываемым функциям allocate_array(), sort_array() и register_data() и какими-то еще операторами внутри функции alloc_and_init()). Пусть это исключение имеет тип string и строка, передаваемая обработчику, содержит описание ошибки.


Содержание раздела