Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
524 } Упражнение 11.1 Какие из приведенных инструкций throw ошибочны? Почему? Для правильных инструкций укажите тип возбужденного исключения: throw pi; Упражнение 11.2 У класса IntArray, определенного в разделе 2.3, имеется функция-оператор operator[]() , в которой используется assert() для извещения о том, что индекс вышел за пределы массива. Измените определение этого оператора так, чтобы в подобной ситуации он генерировал исключение. Определите класс, который будет употребляться как тип возбужденного исключения. 11.2. try- блок В нашей программе тестируется определенный в предыдущем разделе класс iStack и его функции-члены pop() и push(). Выполняется 50 итераций цикла for. На каждой итерации в стек помещается значение, кратное 3: 3, 6, 9 и т.д. Если значение кратно 4 (4, 8, 12...), то выводится текущее содержимое стека, а если кратно 10 (10, 20, 30...), то с вершины снимается один элемент, после чего содержимое стека выводится снова. Как нужно изменить функцию main(), чтобы она обрабатывала исключения, возбуждаемые функциями-членами класса iStack? enum EHstate { noErr, zeroOp, negativeOp, severeError }; int mathFunc( int i ) { if ( i == 0 ) throw zeroOp; // исключение в виде объекта-перечисления // в противном случае продолжается нормальная обработка (a) class exceptionType { }; throw exceptionType(); (b) int excpObj; throw excpObj; (c) enum mathErr { overflow, underflow, zeroDivide }; throw mathErr zeroDivide(); (d) int *pi = excpObj; С++ для начинающих 525 } Инструкции, которые могут возбуждать исключения, должны быть заключены в try-блок. Такой блок начинается с ключевого слова try, за которым идет последовательность инструкций, заключенная в фигурные скобки, а после этого – список обработчиков, называемых catch-предложениями. Try-блок группирует инструкции программы и ассоциирует с ними обработчики исключений. Куда нужно поместить try-блоки в функции main(), чтобы были обработаны исключения popOnEmpty и pushOnFull? } В таком виде программа выполняется корректно. Однако обработка исключений в ней перемежается с кодом, использующимся при нормальных обстоятельствах, а такая организация несовершенна. В конце концов, исключения – это аномальные ситуации, возникающие только в особых случаях. Желательно отделить код для обработки #include #include "iStack.h" int main() { iStack stack( 32 ); stack.display(); for ( int ix = 1; ix < 51; ++ix ) { if ( ix % 3 == 0 ) stack.push( ix ); if ( ix % 4 == 0 ) stack.display(); if ( ix % 10 == 0 ) { int dummy; stack.pop( dummy ); stack.display(); } } return 0; for ( int ix = 1; ix < 51; ++ix ) { try { // try- блок для исключений pushOnFull if ( ix % 3 == 0 ) stack.push( ix ); } catch ( pusOnFull ) { ... } if ( ix % 4 == 0 ) stack.display(); try { // try- блок для исключений popOnEmpty if ( ix % 10 == 0 ) { int dummy; stack.pop( dummy ); stack.display(); } } catch ( popOnEmpty ) { ... } С++ для начинающих 526 аномалий от кода, реализующего операции со стеком. Мы полагаем, что показанная ниже схема облегчает чтение и сопровождение программы: catch ( popOnEmpty ) { ... } С try-блоком ассоциированы два catch-предложения, которые могут обработать исключения pushOnFull и popOnEmpty, возбуждаемые функциями-членами push() и pop() внутри этого блока. Каждый catch-обработчик определяет тип “своего” исключения. Код для обработки исключения помещается внутрь составной инструкции (между фигурными скобками), которая является частью catch-обработчика. (Подробнее catch-предложения мы рассмотрим в следующем разделе.) Исполнение программы может пойти по одному из следующих путей: • если исключение не возбуждено, то выполняется код внутри try-блока, а ассоциированные с ним обработчики игнорируются. Функция main() возвращает 0; • если функция-член push(), вызванная из первой инструкции if внутри цикла for, возбуждает исключение, то вторая и третья инструкции if игнорируются, управление покидает цикл for и try-блок, и выполняется обработчик исключений типа pushOnFull; • если функция-член pop(), вызванная из третьей инструкции if внутри цикла for, возбуждает исключение, то вызов display() игнорируется, управление покидает цикл for и try-блок, и выполняется обработчик исключений типа popOnEmpty. Когда возбуждается исключение, пропускаются все инструкции, следующие за той, где оно было возбуждено. Исполнение программы возобновляется в catch-обработчике этого исключения. Если такого обработчика не существует, то управление передается в функцию terminate(), определенную в стандартной библиотеке C++. Try-блок может содержать любую инструкцию языка C++: как выражения, так и объявления. Он вводит локальную область видимости, так что объявленные внутри него переменные недоступны вне этого блока, в том числе и в catch-обработчиках. Например, функцию main() можно переписать так, что объявление переменной stack окажется в try-блоке. В таком случае обращаться к этой переменной в catch-обработчиках нельзя: try { for ( int ix = 1; ix < 51; ++ix ) { if ( ix % 3 == 0 ) stack.push( ix ); if ( ix % 4 == 0 ) stack.display(); if ( ix % 10 == 0 ) { int dummy; stack.pop( dummy ); stack.display(); } } } catch ( pushOnFull ) { ... } С++ для начинающих 527 } Можно объявить функцию так, что все ее тело будет заключено в try-блок. При этом не обязательно помещать try-блок внутрь определения функции, удобнее заключить ее тело в функциональный try-блок. Такая организация поддерживает наиболее чистое разделение кода для нормальной обработки и кода для обработки исключений. Например: } Обратите внимание, что ключевое слово try находится перед фигурной скобкой, открывающей тело функции, а catch-обработчики перечислены после закрывающей его скобки. Как видим, код, осуществляющий нормальную обработку, находится внутри тела функции и четко отделен от кода для обработки исключений. Однако к переменным, объявленным в main(), нельзя обратиться из обработчиков исключений. Функциональный try-блок ассоциирует группу catch-обработчиков с телом функции. Если инструкция возбуждает исключение, то поиск обработчика, способного перехватить это исключение, ведется среди тех, что идут за телом функции. Функциональные try-блоки особенно полезны в сочетании с конструкторами классов. (Мы еще вернемся к этой теме в главе 19.) int main() { try { iStack stack( 32 ); // правильно: объявление внутри try-блока stack.display(); for ( int ix = 1; ix < 51; ++ix ) { // то же, что и раньше } } catch ( pushOnFull ) { // здесь к переменной stack обращаться нельзя } catch ( popOnEmpty ) { // здесь к переменной stack обращаться нельзя } // и здесь к переменной stack обращаться нельзя return 0; int main() try { iStack stack( 32 ); // правильно: объявление внутри try-блока stack.display(); for ( int ix = 1; ix < 51; ++ix ) { // то же, что и раньше } return 0; } catch ( pushOnFull ) { // здесь к переменной stack обращаться нельзя } catch ( popOnEmpty ) { // здесь к переменной stack обращаться нельзя С++ для начинающих 528 Упражнение 11.3 Напишите программу, которая определяет объект IntArray (тип класса IntArray рассматривался в разделе 2.3) и выполняет описанные ниже действия. Пусть есть три файла, содержащие целые числа. 1. Прочитать первый файл и поместить в объект IntArray первое, третье, пятое, ..., n-ое значение (где n нечетно). Затем вывести содержимое объекта IntArray. 2. Прочитать второй файл и поместить в объект IntArray пятое, десятое, ..., n-ое значение (где n кратно 5). Вывести содержимое объекта. 3. Прочитать третий файл и поместить в объект 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, а во втором – popOnEmpty. Для обработки исключения выбирается тот обработчик, для которого типы в объявлении исключения и в возбужденном исключении совпадают. (В главе 19 мы увидим, что типы не обязаны совпадать точно: обработчик для базового класса подходит и для исключений с производными классами.) Например, когда функция-член pop() класса iStack возбуждает исключение popOnEmpty, то управление попадает во второй обработчик. После вывода сообщения об ошибке в cerr, функция main() возвращает код errorCode89. А если catch-обработчики не содержат инструкции return, с какого места будет продолжено выполнение программы? После завершения обработчика выполнение 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; С++ для начинающих 529 возобновляется с инструкции, идущей за последним catch-обработчиком в списке. В нашем примере оно продолжается с инструкции return в функции main(). После того как catch-обработчик popOnEmpty выведет сообщение об ошибке, main() вернет 0. } Говорят, что механизм обработки исключений в C++ невозвратный: после того как исключение обработано, управление не возобновляется с того места, где оно было возбуждено. В нашем примере управление не возвращается в функцию-член pop(), возбудившую исключение. 11.3.1. Объекты-исключения Объявлением исключения в catch-обработчике могут быть объявления типа или объекта. В каких случаях это следует делать? Тогда, когда необходимо получить значение или как- то манипулировать объектом, созданным в выражении throw. Если классы исключений спроектированы так, что в объектах-исключениях при возбуждении сохраняется некоторая информация и если в объявлении исключения фигурирует такой объект, то инструкции внутри catch-обработчика могут обращаться к информации, сохраненной в объекте выражением throw. Изменим реализацию класса исключения pushOnFull, сохранив в объекте-исключении то значение, которое не удалось поместить в стек. Catch-обработчик, сообщая об ошибке, теперь будет выводить его в cerr. Для этого мы сначала модифицируем определение типа класса pushOnFull следующим образом: 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; // новый класс исключения: // он сохраняет значение, которое не удалось поместить в стек class pushOnFull { public: pushOnFull( int i ) : _value( i ) { } int value { return _value; } private: int _value; С++ для начинающих 530 }; Новый закрытый член _value содержит число, которое не удалось поместить в стек. Конструктор принимает значение типа int и сохраняет его в члене _data. Вот как вызывается этот конструктор для сохранения значения из выражения throw: } У класса pushOnFull появилась также новая функция-член value(), которую можно использовать в catch-обработчике для вывода хранящегося в объекте-исключении значения: } Обратите внимание, что в объявлении исключения в catch-обработчике фигурирует объект eObj, с помощью которого вызывается функция-член value() класса pushOnFull Объект-исключение всегда создается в точке возбуждения, даже если выражение throw – это не вызов конструктора и, на первый взгляд, не должно создавать объекта. Например: } В этом примере объект state не используется в качестве объекта-исключения. Вместо этого выражением throw создается объект-исключение типа EHstate, который инициализируется значением глобального объекта state. Как программа может различить их? Для ответа на этот вопрос мы должны присмотреться к объявлению исключения в catch-обработчике более внимательно. Это объявление ведет себя почти так же, как объявление формального параметра. Если при входе в catch-обработчик исключения выясняется, что в нем объявлен объект, то он инициализируется копией объекта-исключения. Например, следующая функция void iStack::push( int value ) { if ( full() ) // значение, сохраняемое в объекте-исключении throw pushOnFull( value ); // ... catch ( pushOnFull eObj ) { cerr << "trying to push value " << eObj.value() << " on a full stack\n"; enum EHstate { noErr, zeroOp, negativeOp, severeError }; enum EHstate state = noErr; int mathFunc( int i ) { if ( i == 0 ) { state = zeroOp; throw state; // создан объект-исключение } // иначе продолжается обычная обработка С++ для начинающих 531 calculate() вызывает определенную выше mathFunc(). При входе в catch-обработчик внутри calculate() объект eObj инициализируется копией объекта-исключения, созданного выражением throw. } Объявление исключения в этом примере напоминает передачу параметра по значению. Объект eObj инициализируется значением объекта-исключения точно так же, как переданный по значению формальный параметр функции – значением соответствующего фактического аргумента. (Передача параметров по значению рассматривалась в разделе 7.3.) Как и в случае параметров функции, в объявлении исключения может фигурировать ссылка. Тогда catch-обработчик будет напрямую ссылаться на объект-исключение, сгенерированный выражением throw, а не создавать его локальную копию: } Для предотвращения ненужного копирования больших объектов применять ссылки следует не только в объявлениях параметров типа класса, но и в объявлениях исключений того же типа. В последнем случае catch-обработчик сможет модифицировать объект-исключение. Однако переменные, определенные в выражении throw, остаются без изменения. Например, модификация eObj внутри catch-обработчика не затрагивает глобальную переменную state, установленную в выражении throw: } void calculate( int op ) { try { mathFunc( op ); } catch ( EHstate eObj ) { // eObj - копия сгенерированного объекта-исключения } void calculate( int op ) { try { mathFunc( op ); } catch ( EHstate &eObj ) { // eObj ссылается на сгенерированный объект-исключение } void calculate( int op ) { try { mathFunc( op ); } catch ( EHstate &eObj ) { // исправить ошибку, вызвавшую исключение eObj = noErr; // глобальная переменная state не изменилась } С++ для начинающих 532 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++ требует языковой поддержки во время выполнения. Например, для обычного вызова |