<< Пред.           стр. 51 (из 121)           След. >>

Список литературы по разделу

  }
  catch ( pushOnFull ) {
  // здесь к переменной stack обращаться нельзя
  }
  catch ( popOnEmpty ) {
  // здесь к переменной stack обращаться нельзя
  }
 Обратите внимание, что ключевое слово try находится перед фигурной скобкой, открывающей тело функции, а catch-обработчики перечислены после закрывающей его скобки. Как видим, код, осуществляющий нормальную обработку, находится внутри тела функции и четко отделен от кода для обработки исключений. Однако к переменным, объявленным в main(), нельзя обратиться из обработчиков исключений.
 Функциональный try-блок ассоциирует группу catch-обработчиков с телом функции. Если инструкция возбуждает исключение, то поиск обработчика, способного перехватить это исключение, ведется среди тех, что идут за телом функции. Функциональные try-блоки особенно полезны в сочетании с конструкторами классов. (Мы еще вернемся к этой теме в главе 19.)
 Упражнение 11.3
 Напишите программу, которая определяет объект IntArray (тип класса IntArray рассматривался в разделе 2.3) и выполняет описанные ниже действия.
 Пусть есть три файла, содержащие целые числа.
 Прочитать первый файл и поместить в объект IntArray первое, третье, пятое, ..., n-ое значение (где n нечетно). Затем вывести содержимое объекта IntArray.
 Прочитать второй файл и поместить в объект IntArray пятое, десятое, ..., n-ое значение (где n кратно 5). Вывести содержимое объекта.
 Прочитать третий файл и поместить в объект IntArray второе, четвертое, ..., n-ое значение (где n четно). Вывести содержимое объекта.
 Воспользуйтесь оператором operator[]() класса IntArray, определенным в упражнении 11.2, для сохранения и получения значений из объекта IntArray. Так как operator[]() может возбуждать исключения, обработайте их, поместив необходимое количество try-блоков и catch-обработчиков. Объясните, почему вы разместили try-блоки именно так, а не иначе.
 11.3. Перехват исключений
 В языке C++ исключения обрабатываются в предложениях catch. Когда какая-то инструкция внутри try-блока возбуждает исключение, то просматривается список последующих предложений catch в поисках такого, который может его обработать.
 Catch-обработчик состоит из трех частей: ключевого слова catch, объявления одного типа или одного объекта, заключенного в круглые скобки (оно называется объявлением исключения), и составной инструкции. Если для обработки исключения выбрано некоторое catch-предложение, то выполняется эта составная инструкция. Рассмотрим catch-обработчики исключений pushOnFull и popOnEmpty в функции main() более подробно:
 catch ( pushOnFull ) {
  cerr << "trying to push value on a full stack\n";
  return errorCode88;
 }
 catch ( popOnEmpty ) {
  cerr << "trying to pop a value on an empty stack\n";
  return errorCode89;
 }
 В обоих catch-обработчиках есть объявление типа класса; в первом это pushOnFull, а во втором – popOnEmpty. Для обработки исключения выбирается тот обработчик, для которого типы в объявлении исключения и в возбужденном исключении совпадают. (В главе 19 мы увидим, что типы не обязаны совпадать точно: обработчик для базового класса подходит и для исключений с производными классами.) Например, когда функция-член pop() класса iStack возбуждает исключение popOnEmpty, то управление попадает во второй обработчик. После вывода сообщения об ошибке в cerr, функция main() возвращает код errorCode89.
 А если catch-обработчики не содержат инструкции return, с какого места будет продолжено выполнение программы? После завершения обработчика выполнение возобновляется с инструкции, идущей за последним catch-обработчиком в списке. В нашем примере оно продолжается с инструкции return в функции main(). После того как catch-обработчик popOnEmpty выведет сообщение об ошибке, main() вернет 0.
 int main() {
  iStack stack( 32 );
 
  try {
  stack.display();
  for ( int x = 1; ix < 51; ++ix )
  {
  // то же, что и раньше
  }
  }
  catch ( pushOnFull ) {
  cerr << "trying to push value on a full stack\n";
  }
  catch ( popOnEmpty ) {
  cerr << "trying to pop a value on an empty stack\n";
  }
 
  // исполнение продолжается отсюда
  return 0;
 }
 Говорят, что механизм обработки исключений в C++ невозвратный: после того как исключение обработано, управление не возобновляется с того места, где оно было возбуждено. В нашем примере управление не возвращается в функцию-член pop(), возбудившую исключение.
 11.3.1. Объекты-исключения
 Объявлением исключения в catch-обработчике могут быть объявления типа или объекта. В каких случаях это следует делать? Тогда, когда необходимо получить значение или как-то манипулировать объектом, созданным в выражении throw. Если классы исключений спроектированы так, что в объектах-исключениях при возбуждении сохраняется некоторая информация и если в объявлении исключения фигурирует такой объект, то инструкции внутри catch-обработчика могут обращаться к информации, сохраненной в объекте выражением throw.
 Изменим реализацию класса исключения pushOnFull, сохранив в объекте-исключении то значение, которое не удалось поместить в стек. Catch-обработчик, сообщая об ошибке, теперь будет выводить его в cerr. Для этого мы сначала модифицируем определение типа класса pushOnFull следующим образом:
 // новый класс исключения:
 // он сохраняет значение, которое не удалось поместить в стек
 class pushOnFull {
 public:
  pushOnFull( int i ) : _value( i ) { }
  int value { return _value; }
 private:
  int _value;
 };
 Новый закрытый член _value содержит число, которое не удалось поместить в стек. Конструктор принимает значение типа int и сохраняет его в члене _data. Вот как вызывается этот конструктор для сохранения значения из выражения throw:
 void iStack::push( int value )
 {
  if ( full() )
  // значение, сохраняемое в объекте-исключении
  throw pushOnFull( value );
 
  // ...
 }
 У класса pushOnFull появилась также новая функция-член value(), которую можно использовать в catch-обработчике для вывода хранящегося в объекте-исключении значения:
 catch ( pushOnFull eObj ) {
  cerr << "trying to push value " << eObj.value()
  << " on a full stack\n";
 }
 Обратите внимание, что в объявлении исключения в catch-обработчике фигурирует объект eObj, с помощью которого вызывается функция-член value() класса pushOnFull.
 Объект-исключение всегда создается в точке возбуждения, даже если выражение throw – это не вызов конструктора и, на первый взгляд, не должно создавать объекта. Например:
 enum EHstate { noErr, zeroOp, negativeOp, severeError };
 enum EHstate state = noErr;
 
 int mathFunc( int i ) {
  if ( i == 0 ) {
  state = zeroOp;
  throw state; // создан объект-исключение
  }
  // иначе продолжается обычная обработка
 }
 В этом примере объект state не используется в качестве объекта-исключения. Вместо этого выражением throw создается объект-исключение типа EHstate, который инициализируется значением глобального объекта state. Как программа может различить их? Для ответа на этот вопрос мы должны присмотреться к объявлению исключения в catch-обработчике более внимательно.
 Это объявление ведет себя почти так же, как объявление формального параметра. Если при входе в catch-обработчик исключения выясняется, что в нем объявлен объект, то он инициализируется копией объекта-исключения. Например, следующая функция calculate() вызывает определенную выше mathFunc(). При входе в catch-обработчик внутри calculate() объект eObj инициализируется копией объекта-исключения, созданного выражением throw.
 void calculate( int op ) {
  try {
  mathFunc( op );
  }
  catch ( EHstate eObj ) {
  // eObj - копия сгенерированного объекта-исключения
  }
 }
 Объявление исключения в этом примере напоминает передачу параметра по значению. Объект eObj инициализируется значением объекта-исключения точно так же, как переданный по значению формальный параметр функции – значением соответствующего фактического аргумента. (Передача параметров по значению рассматривалась в разделе 7.3.)
 Как и в случае параметров функции, в объявлении исключения может фигурировать ссылка. Тогда catch-обработчик будет напрямую ссылаться на объект-исключение, сгенерированный выражением throw, а не создавать его локальную копию:
 void calculate( int op ) {
 try {
  mathFunc( op );
  }
  catch ( EHstate &eObj ) {
  // eObj ссылается на сгенерированный объект-исключение
  }
 }
 Для предотвращения ненужного копирования больших объектов применять ссылки следует не только в объявлениях параметров типа класса, но и в объявлениях исключений того же типа.
 В последнем случае catch-обработчик сможет модифицировать объект-исключение. Однако переменные, определенные в выражении throw, остаются без изменения. Например, модификация eObj внутри catch-обработчика не затрагивает глобальную переменную state, установленную в выражении throw:
 void calculate( int op ) {
 try {
  mathFunc( op );
  }
  catch ( EHstate &eObj ) {
  // исправить ошибку, вызвавшую исключение
  eObj = noErr; // глобальная переменная state не изменилась
  }
 }
 Catch-обработчик переустанавливает eObj в noErr после исправления ошибки, вызвавшей исключение. Поскольку eObj – это ссылка, можно ожидать, что присваивание модифицирует глобальную переменную state. Однако изменяется лишь объект-исключение, созданный в выражении throw, поэтому модификация eObj не затрагивает state.
 11.3.2. Раскрутка стека
 Поиск catch-обработчикадля возбужденного исключения происходит следующим образом. Когда выражение throw находится в try-блоке, все ассоциированные с ним предложения catch исследуются с точки зрения того, могут ли они обработать исключение. Если подходящее предложение catch найдено, то исключение обрабатывается. В противном случае поиск продолжается в вызывающей функции. Предположим, что вызов функции, выполнение которой прекратилось в результате исключения, погружен в try-блок; в такой ситуации исследуются все предложения catch, ассоциированные с этим блоком. Если один из них может обработать исключение, то процесс заканчивается. В противном случае переходим к следующей по порядку вызывающей функции. Этот поиск последовательно проводится во всей цепочке вложенных вызовов. Как только будет найдено подходящее предложение, управление передается в соответствующий обработчик.
 В нашем примере первая функция, для которой нужен catch-обработчик, – это функция-член pop() класса iStack. Поскольку выражение throw внутри pop() не находится в try-блоке, то программа покидает pop(), не обработав исключение. Следующей рассматривается функция, вызвавшая pop(), то есть main(). Вызов pop() внутри main() находится в try-блоке, и далее исследуется, может ли хотя бы одно ассоциированное с ним предложение catch обработать исключение. Поскольку обработчик исключения popOnEmpty имеется, то управление попадает в него.
 Процесс, в результате которого программа последовательно покидает составные инструкции и определения функций в поисках предложения catch, способного обработать возникшее исключение, называется раскруткой стека. По мере раскрутки прекращают существование локальные объекты, объявленные в составных инструкциях и определениях функций, из которых произошел выход. C++ гарантирует, что во время описанного процесса вызываются деструкторы локальных объектов классов, хотя они исчезают из-за возбужденного исключения. (Подробнее мы поговорим об этом в главе 19.)
 Если в программе нет предложения catch, способного обработать исключение, оно остается необработанным. Но исключение – это настолько серьезная ошибка, что программа не может продолжать выполнение. Поэтому, если обработчик не найден, вызывается функция terminate() из стандартной библиотеки C++. По умолчанию terminate() активизирует функцию abort(), которая аномально завершает программу. (В большинстве ситуаций вызов abort() оказывается вполне приемлемым решением. Однако иногда необходимо переопределить действия, выполняемые функцией terminate(). Как это сделать, рассказывается в книге [STROUSTRUP97].)
 Вы уже, наверное, заметили, что обработка исключений и вызов функции во многом похожи. Выражение throw ведет себя аналогично вызову, а предложение catch чем-то напоминает определение функции. Основная разница между этими двумя механизмами заключается в том, что информация, необходимая для вызова функции, доступна во время компиляции, а для обработки исключений – нет. Обработка исключений в C++ требует языковой поддержки во время выполнения. Например, для обычного вызова функции компилятору в точке активизации уже известно, какая из перегруженных функций будет вызвана. При обработке же исключения компилятор не знает, в какой функции находится catch-обработчик и откуда возобновится выполнение программы. Функция terminate() предоставляет механизм времени выполнения, который извещает пользователя о том, что подходящего обработчика не нашлось.
 11.3.3. Повторное возбуждение исключения
 Может оказаться так, что в одном предложении catch не удалось полностью обработать исключение. Выполнив некоторые корректирующие действия, catch-обработчик может решить, что дальнейшую обработку следует поручить функции, расположенной “выше” в цепочке вызовов. Передать исключение другому catch-обработчику можно с помощью повторного возбуждения исключения. Для этой цели в языке предусмотрена конструкция
 throw;
 которая вновь генерирует объект-исключение. Повторное возбуждение возможно только внутри составной инструкции, являющейся частью catch-обработчика:
 catch ( exception eObj ) {
  if ( canHandle( eObj ) )
  // обработать исключение
  return;
  else
  // повторно возбудить исключение, чтобы его перехватил другой
  // catch-обработчик
  throw;
 }
 При повторном возбуждении новый объект-исключение не создается. Это имеет значение, если catch-обработчик модифицирует объект, прежде чем возбудить исключение повторно. В следующем фрагменте исходный объект-исключение не изменяется. Почему?
 enum EHstate { noErr, zeroOp, negativeOp, severeError };
 
 void calculate( int op ) {
 try {
  // исключение, возбужденное mathFunc(), имеет значение zeroOp
  mathFunc( op );
  }
  catch ( EHstate eObj ) {
  // что-то исправить
 
  // пытаемся модифицировать объект-исключение
  eObj = severeErr;
 
  // предполагалось, что повторно возбужденное исключение будет
  // иметь значение severeErr
  throw;
  }
 }
 Так как eObj не является ссылкой, то catch-обработчик получает копию объекта-исключения, так что любые модификации eObj относятся к локальной копии и не отражаются на исходном объекте-исключении, передаваемом при повторном возбуждении. Таким образом, переданный далее объект по-прежнему имеет тип zeroOp.
 Чтобы модифицировать исходный объект-исключение, в объявлении исключения внутри catch-обработчика должна фигурировать ссылка:
 catch ( EHstate &eObj ) {
  // модифицируем объект-исключение
  eObj = severeErr;
 
  // повторно возбужденное исключение имеет значение severeErr
  throw;
 }
 Теперь eObj ссылается на объект-исключение, созданный выражением throw, так что все изменения относятся непосредственно к исходному объекту. Поэтому при повторном возбуждении исключения далее передается модифицированный объект.
 Таким образом, другая причина для объявления ссылки в catch-обработчике заключается в том, что сделанные внутри обработчика модификации объекта-исключения в таком случае будут видны при повторном возбуждении исключения. (Третья причина будет рассмотрена в разделе 19.2, где мы расскажем, как catch-обработчик вызывает виртуальные функции класса.)
 11.3.4. Перехват всех исключений
 Иногда функции нужно выполнить определенное действие до того, как она завершит обработку исключения, даже несмотря на то, что обработать его она не может. К примеру, функция захватила некоторый ресурс, скажем открыла файл или выделила память из хипа, и этот ресурс необходимо освободить перед выходом:
 void manip() {
  resource res;
  res.lock(); // захват ресурса
 
  // использование ресурса
  // действие, в результате которого возбуждено исключение
 
  res.release(); // не выполняется, если возбуждено исключение
 }
 Если исключение возбуждено, то управление не попадет на инструкцию, где ресурс освобождается. Чтобы освободить ресурс, не пытаясь перехватить все возможные исключения (тем более, что мы не всегда знаем, какие именно исключения могут возникнуть), воспользуемся специальной конструкцией, позволяющей перехватывать любые исключения. Это не что иное, как предложение catch, в котором объявление исключения имеет вид (...) и куда управление попадает при любом исключении. Например:
 // управление попадает сюда при любом возбужденном исключении
 catch (...) {
  // здесь размещаем наш код
 }
 Конструкция catch(...) используется в сочетании с повторным возбуждением исключения. Захваченный ресурс освобождается внутри составной инструкции в catch-обработчике перед тем, как передать исключение по цепочке вложенных вызовов в результате повторного возбуждения:
 void manip() {
  resource res;
  res.lock();
  try {
  // использование ресурса
  // действие, в результате которого возбуждено исключение
  }
  catch (...) {
  res.release();
  throw;
  }
  res.release(); // не выполняется, если возбуждено исключение
 }
 Чтобы гарантировать освобождение ресурса в случае, когда выход из manip() происходит в результате исключения, мы освобождаем его внутри catch(...) до того, как исключение будет передано дальше. Можно также управлять захватом и освобождением ресурса путем инкапсуляции в класс всей работы с ним. Тогда захват будет реализован в конструкторе, а освобождение – в автоматически вызываемом деструкторе. (С этим подходом мы познакомимся в главе 19.)
 Предложение catch(...) используется самостоятельно или в сочетании с другими catch-обработчиками. В последнем случае следует позаботиться о правильной организации обработчиков, ассоциированных с try-блоком.
 Catch-обработчики исследуются по очереди, в том порядке, в котором они записаны. Как только найден подходящий, просмотр прекращается. Следовательно, если предложение catch(...) употребляется вместе с другими catch-обработчиками, то оно должно быть последним в списке, иначе компилятор выдаст сообщение об ошибке:
 try {
  stack.display();
  for ( int ix = 1; ix < 51; ++x )
  {
  // то же, что и выше
  }
 }
 catch ( pushOnFull ) { }
 catch ( popOnEmpty ) { }
 catch ( ... ) { } // должно быть последним в списке catch-обработчиков
 Упражнение 11.4
 Объясните, почему модель обработки исключений в C++ называется невозвратной.
 Упражнение 11.5
 Даны следующие объявления исключений. Напишите выражения throw, создающие объект-исключение, который может быть перехвачен указанными обработчиками:
 (a) class exceptionType { };
  catch( exceptionType *pet ) { }
 (b) catch(...) { }
 (c) enum mathErr { overflow, underflow, zeroDivide };
  catch( mathErr &ref ) { }
 (d) typedef int EXCPTYPE;
  catch( EXCPTYPE ) { }
 Упражнение 11.6
 Объясните, что происходит во время раскрутки стека.
 Упражнение 11.7
 Назовите две причины, по которым объявление исключения в предложении catch следует делать ссылкой.
 Упражнение 11.8
 На основе кода, написанного вами в упражнении 11.3, модифицируйте класс созданного исключения: неправильный индекс, использованный в операторе operator[](), должен сохраняться в объекте-исключении и затем выводиться catch-обработчиком. Измените программу так, чтобы operator[]() возбуждал при ее выполнении исключение.
 11.4. Спецификации исключений
 По объявлениям функций-членов pop() и push() класса iStack невозможно определить, что они возбуждают исключения. Можно, конечно, включить в объявление подходящий комментарий. Тогда описание интерфейса класса в заголовочном файле будет содержать документацию возбуждаемых исключений:
 class iStack {
 public:
  // ...
 
  void pop( int &value ); // возбуждает popOnEmpty
  void push( int value ); // возбуждает pushOnFull
 
 private:
  // ...
 };
 Но такое решение несовершенно. Неизвестно, будет ли обновлена документация при выпуске следующих версий iStack. Кроме того, комментарий не дает компилятору достоверной информации о том, что никаких других исключений функция не возбуждает. Спецификация исключений позволяет перечислить в объявлении функции все исключения, которые она может возбуждать. При этом гарантируется, что другие исключения функция возбуждать не будет.
 Такая спецификация следует за списком формальных параметров функции. Она состоит из ключевого слова throw, за которым идет список типов исключений, заключенный в скобки. Например, объявления функций-членов класса iStack можно модифицировать, добавив спецификации исключений:
 class iStack {
 public:
  // ...
 
  void pop( int &value ) throw(popOnEmpty);
  void push( int value ) throw(pushOnFull);
 
 private:
  // ...
 };
 Гарантируется, что при обращении к pop() не будет возбуждено никаких исключений, кроме popOnEmpty, а при обращении к push()–только pushOnFull.
 Объявление исключения – это часть интерфейса функции, оно должно быть задано при ее объявлении в заголовочном файле. Спецификация исключений – это своего рода “контракт” между функцией и остальной частью программы, гарантия того, что функция не будет возбуждать никаких исключений, кроме перечисленных.
 Если в объявлении функции присутствует спецификация исключений, то при повторном объявлении этой же функции должны быть перечислены точно те же типы. Спецификации исключений в разных объявлениях одной и той же функции не суммируются:
 // два объявления одной и той же функции
 extern int foo( int = 0 ) throw(string);
 
 // ошибка: опущена спецификация исключений
 extern int foo( int parm ) { }
 Что произойдет, если функция возбудит исключение, не перечисленное в ее спецификации? Исключения возбуждаются только при обнаружении определенных аномалий в поведении программы, и во время компиляции неизвестно, встретится ли то или иное исключение во время выполнения. Поэтому нарушения спецификации исключений функции могут быть обнаружены только во время выполнения. Если функция возбуждает исключение, не указанное в спецификации, то вызывается unexpected() из стандартной библиотеки C++, а та по умолчанию вызывает terminate(). (В некоторых случаях необходимо переопределить действия, выполняемые функцией unexpected(). Стандартная библиотека предоставляет механизм для этого. Подробнее см. [STRAUSTRUP97].)
 Необходимо уточнить, что unexpected() не вызывается только потому, что функция возбудила исключение, не указанное в ее спецификации. Все нормально, если она обработает это исключение самостоятельно, внутри функции. Например:
 void recoup( int op1, int op2 ) throw(ExceptionType)
 {
  try {
  // ...
  throw string("we're in control");
  }
  // обрабатывается возбужденное исключение
  catch ( string ) {
  // сделать все необходимое
  }
 } // все хорошо, unexpected() не вызывается
 Функция recoup() возбуждает исключение типа string, несмотря на его отсутствие в спецификации. Поскольку это исключение обработано в теле функции, unexpected() не вызывается.
 Нарушения спецификации исключений функции обнаруживаются только во время выполнения. Компилятор не сообщает об ошибке, если в выражении throw возбуждается исключение неуказанного типа. Если такое выражение никогда не выполнится или не возбудит исключения, нарушающего спецификацию, то программа будет работать, как и ожидалось, и нарушение никак не проявится:
 extern void doit( int, int ) throw(string, exceptionType);
 
 void action ( int op1, int op2 ) throw(string) {
  doit( op1, op2 ); // ошибки компиляции не будет
  // ...
 }
 doit() может возбудить исключение типа exceptionType, которое не разрешено спецификацией action(). Однако функция компилируется успешно. Компилятор при этом генерирует код, гарантирующий, что при возбуждении исключения, нарушающего спецификацию, будет вызвана библиотечная функция unexpected().
 Пустая спецификация показывает, что функция не возбуждает никаких исключений:
 extern void no_problem () throw();
 Если же в объявлении функции спецификация исключений отсутствует, то может быть возбуждено исключение любого типа.
 Между типом возбужденного исключения и типом исключения, указанного в спецификации, не разрешается проводить никаких преобразований:
 int convert( int parm ) throw(string)
 {
  //...
  if ( somethingRather )
  // ошибка программы:
  // convert() не допускает исключения типа const char*
  throw "help!";
 }
 Выражение throw в функции convert() возбуждает исключение типа строки символов в стиле языка C. Созданный объект-исключение имеет тип const char*. Обычно выражение типа const char* можно привести к типу string. Однако спецификация не допускает преобразования типов, поэтому если convert() возбуждает такое исключение, то вызывается unexpected(). Для исправления ошибки выражение throw можно модифицировать так, чтобы оно явно преобразовывало значение выражения в тип string:
 throw string( "help!" );
 11.4.1. Спецификации исключений и указатели на функции
 Спецификацию исключений можно задавать и при объявлении указателя на функцию. Например:
 void (*pf)( int ) throw(string);

<< Пред.           стр. 51 (из 121)           След. >>

Список литературы по разделу