Главная страница
Навигация по странице:

  • 11.3.1. Объекты-исключения

  • 11.3.2. Раскрутка стека

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница50 из 93
    1   ...   46   47   48   49   50   51   52   53   ...   93
    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++ требует языковой поддержки во время выполнения. Например, для обычного вызова

    С++ для начинающих
    1   ...   46   47   48   49   50   51   52   53   ...   93


    написать администратору сайта