Главная страница

Объектноориентированное программирование


Скачать 1.73 Mb.
НазваниеОбъектноориентированное программирование
Дата21.11.2018
Размер1.73 Mb.
Формат файлаpdf
Имя файлаOOP-PrePrint.pdf
ТипКонспект
#57177
страница8 из 15
1   ...   4   5   6   7   8   9   10   11   ...   15
10.1
Перехват исключений
В языке 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
. Для обработки исключения выби- рается тот обработчик, для которого типы в объявлении исключения и в воз- бужденном исключении совпадают. Например, когда функция-член pop()

82 класса 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()
, возбудившую исключение.
10.2
Объекты-исключения
Объявлением исключения в catch
-обработчике могут быть объявления типа или объекта. В каких случаях это следует делать? Тогда, когда необхо- димо получить значение или как-то манипулировать объектом, созданным в выражении throw
. Если классы исключений спроектированы так, что в объектах-исключениях при возбуждении сохраняется некоторая информа- ция и если в объявлении исключения фигурирует такой объект, то инструк- ции внутри catch
-обработчика могут обращаться к информации, сохранен- ной в объекте выражением throw

83
Изменим реализацию класса исключения 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;

84 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 инициализируется значением объекта-ис- ключения точно так же, как переданный по значению формальный параметр функции – значением соответствующего фактического аргумента.
Как и в случае параметров функции, в объявлении исключения может фигурировать ссылка. Тогда catch
-обработчик будет напрямую ссылаться на объект-исключение, сгенерированный выражением throw
, а не создавать его локальную копию: void calculate( int op ) { try { mathFunc( op );
} catch ( EHstate &eObj ) {
// eObj ссылается на сгенерированный объект-исключение
}
}

85
Для предотвращения ненужного копирования больших объектов приме- нять ссылки следует не только в объявлениях параметров типа класса, но и в объявлениях исключений того же типа.
В последнем случае 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
10.3
Раскрутка стека
Поиск catch
-обработчикадля возбужденного исключения происходит следующим образом. Когда выражение throw находится в try
-блоке, все ассоциированные с ним предложения catch исследуются с точки зрения того, могут ли они обработать исключение. Если подходящее предложение catch найдено, то исключение обрабатывается. В противном случае поиск продолжается в вызывающей функции. Предположим, что вызов функции, выполнение которой прекратилось в результате исключения, погружен в try
-блок; в такой ситуации исследуются все предложения catch
, ассоции- рованные с этим блоком. Если один из них может обработать исключение, то процесс заканчивается. В противном случае переходим к следующей по порядку вызывающей функции. Этот поиск последовательно проводится во всей цепочке вложенных вызовов. Как только будет найдено подходящее предложение, управление передается в соответствующий обработчик.
В нашем примере первая функция, для которой нужен catch
-обработ- чик, – это функция-член pop()
класса iStack
. Поскольку выражение throw внутри pop()
не находится в try
-блоке, то программа покидает pop()
, не

86 обработав исключение. Следующей рассматривается функция, вызвавшая pop()
, то есть main()
. Вызов pop()
внутри main()
находится в try- блоке, и далее исследуется, может ли хотя бы одно ассоциированное с ним пред- ложение catch обработать исключение. Поскольку обработчик исключения popOnEmpty имеется, то управление попадает в него.
Процесс, в результате которого программа последовательно покидает составные инструкции и определения функций в поисках предложения catch
, способного обработать возникшее исключение, называется раскрут- кой стека. По мере раскрутки прекращают существование локальные объ- екты, объявленные в составных инструкциях и определениях функций, из которых произошел выход. C++ гарантирует, что во время описанного про- цесса вызываются деструкторы локальных объектов классов, хотя они исче- зают из-за возбужденного исключения.
Если в программе нет предложения catch
, способного обработать ис- ключение, оно остается необработанным. Но исключение – это настолько серьезная ошибка, что программа не может продолжать выполнение. По- этому, если обработчик не найден, вызывается функция terminate()
из стандартной библиотеки C++. По умолчанию terminate()
активизирует функцию abort()
, которая аномально завершает программу. (В большин- стве ситуаций вызов abort()
оказывается вполне приемлемым решением.
Однако иногда необходимо переопределить действия, выполняемые функ- цией terminate()
Вы уже, наверное, заметили, что обработка исключений и вызов функ- ции во многом похожи. Выражение throw ведет себя аналогично вызову, а предложение catch чем-то напоминает определение функции. Основная разница между этими двумя механизмами заключается в том, что информа- ция, необходимая для вызова функции, доступна во время компиляции, а для обработки исключений – нет. Обработка исключений в C++ требует языковой поддержки во время выполнения. Например, для обычного вызова функции компилятору в точке активизации уже известно, какая из перегру- женных функций будет вызвана. При обработке же исключения компилятор не знает, в какой функции находится catch
-обработчик и откуда возобно- вится выполнение программы. Функция terminate()
предоставляет меха- низм времени выполнения, который извещает пользователя о том, что под- ходящего обработчика не нашлось.

87
10.4
Повторное возбуждение исключений
Может оказаться так, что в одном предложении 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

88
Чтобы модифицировать исходный объект-исключение, в объявлении ис- ключения внутри catch
-обработчика должна фигурировать ссылка: catch ( EHstate &eObj ) {
// модифицируем объект-исключение eObj = severeErr;
// повторно возбужденное исключение имеет значение severeErr throw;
}
Теперь eObj ссылается на объект-исключение, созданный выражением throw
, так что все изменения относятся непосредственно к исходному объ- екту. Поэтому при повторном возбуждении исключения далее передается модифицированный объект.
Таким образом, другая причина для объявления ссылки в catch
-обработ- чике заключается в том, что сделанные внутри обработчика модификации объекта-исключения в таком случае будут видны при повторном возбужде- нии исключения
10.5
Обработка исключений в С
В эпоху расцвета процедурного программирования синтаксис работы с ошибками был тривиален и основывался на том, что вернула функция.
Если функция возвращала
TRUE
– все хорошо, если же
FALSE
– то произо- шла ошибка. При этом сразу выделились два подхода к работе с ошиб- ками:

Подход два в одном – функция возвращает
FALSE
или нулевой указа- тель как для ожидаемой, так и для неожиданной ошибки. Такой подход как правило применялся в API общего назначения и коде пользовательских про- грамм, когда большую часть ошибок можно было смело считать фаталь- ными и падать. Для тех редких случаев когда делить было все же нужно использовалась некая дополнительная машинерия вида
GetLastError()
Фрагмент кода того времени, копирующего данные из одного файла в дру- гой и возвращающего ошибку в случае возникновения любых проблем:
BOOL Copy( CHAR* sname, CHAR* dname ) {
FILE *sfile = 0, *dfile = 0; void* mem = 0;
UINT32 size = 0, written = 0;
BOOL ret = FALSE; sfile = fopen( sname, "rb" ); if( ! sfile ) goto cleanup; dfile = fopen( dname, "wb" );

89 if( ! dfile ) goto cleanup; mem = malloc( F_CHUNK_SIZE ); if( ! mem ) goto cleanup; do
{ size = fread( sfile, mem, F_CHUNK_SIZE ); written = fwrite( dfile, mem, size ); if( size != written ) goto cleanup;
} while( size ) ret = TRUE; cleanup: // Аналог деструктора. if( sfile) fclose( sfile ); if( dfile) fclose( dfile ); if( mem ) free( mem ); return ret; // Ожидаемая ошибка.
}

Подход разделения ошибок, при котором функция возвращает
FALSE
в случае неожиданной ошибки, а ожидаемую ошибку возвращает отдель- ным возвращаемым значением (в примере это error), если нужно. Такой под- ход применялся в более надежном коде, например apache, и подразумевал разделение на ожидаемые ошибки (файл не получилось открыть потому что его нет) и неожиданные (файл не получилось открыть потому, что закончи- лась память и не получилось выделить 20 байт чтобы скопировать строку с именем). Фрагмент того же код, но уже разделяющего неожиданную ошибку (возврат
FALSE
) и ожидаемую (возврат
HANDLE
).
BOOL Copy( CHAR* sname, CHAR* dname, OUT HANDLE* error ) {
HANDLE sfile = 0, dfile = 0, data = 0;
UINT32 size = 0;
ENSURE( PoolAlloc() ); // Обработка неожиданной ошибки.
ENSURE( FileOpen( sname, OUT& sfile, OUT error ) );
REQUIRE( SUCCESS( error ) ); // Обработка ожидаемой ошибки.
ENSURE( FileOpen( dname, OUT& dfile, OUT error ) );
REQUIRE( SUCCESS( error ) );
ENSURE( MemAlloc( OUT& data ) );
REQUIRE( SUCCESS( error ) ); do
{
ENSURE( FileRead( sfile, F_CHUNK_SIZE, OUT& data, OUT error ) );
REQUIRE( SUCCESS( error ) );
ENSURE( FileWrite( dfile, & data ) );
REQUIRE( SUCCESS( error ) );
ENSURE( MemGetSize( OUT& size ) )
} while( size );

90
ENSURE( PoolFree() ); // Пул обеспечивает аналог деструкторов и RAII. return TRUE;
}
Через некоторое время разработчики заметили, что большинство успеш- ных решений использует ООП и решили, что неплохо бы его вынести в син- таксис языка, дабы писать больше кода по делу и меньше – повторяющегося кода для поддержки архитектуры.
Давайте возьмем код выше и посмотрим, как он трансформировался по- сле добавления ООП в синтаксис языков программирования. Конструиро- вание и уничтожение объектов (
fopen
, fclose
) стало конструкторами и де- структорами. Переброс неожиданной ошибки (
BOOL ret в первом примере, макрос
ENSURE
во втором) однозначно стал исключением.
А вот с ожидаемой ошибкой случилось самое интересное – случился вы- бор. Можно было использовать возвращаемое значение – теперь, когда за- боту о неожиданных ошибках взяли на себя исключения, возвращаемое зна- чение снова стало в полном распоряжении программиста. А можно было ис- пользовать исключения другого типа – если функции копирования файлов самой не нужно обрабатывать ожидаемые ошибки то логично вместо if и
REQUIRE
просто ничего не делать – и оба типа ошибок уйдут вверх по стеку.
Соответственно, у программистов снова получилось два варианта:

Подход только исключения – ожидаемые и неожиданные ошибки – это разные типы исключений. void Copy( string sname, string dname ) { file source( sname ); file destination( sname ); source.open( "rb" ); destination.open( "wb" ); data bytes; do
{ bytes = source.read( F_CHUNK_SIZE ); destination.write( bytes )
} while( bytes.size() )
}

Комбинированный подход – использование исключений для неожи- данных ошибок и кодов возврата / nullable типов для ожидаемых: bool Copy( string sname, string dname ) { file source( sname ); file destination( sname ); if( ! source.open( "rb" ) || ! destination.open( "wb" ) ) return false;

91 data bytes; do
{ bytes = source.read( F_CHUNK_SIZE ); if( bytes.isValid() )
{ if( ! destination.write( bytes ) ) return false;
}
} while( bytes.isValid() && bytes.size() )
}
Итак, если внимательно посмотреть на два приведенных выше фраг- мента кода то становится не совсем понятно почему выжил второй. Кода в нем объективно больше. Выглядит менее красиво. Если функция возвра- щает объект – то использовать коды возврата совсем неудобно. Вопрос – почему коды возврата вообще выжили в языках с поддержкой объектно- ориентированного программирования и исключений на уровне синтаксиса?
Что я могу по этому поводу сказать:
Первые реализации исключений, особенно в C++, были не очень удобны для ежедневного использования. Например, бросание исключения во время обработки другого исключения приводил к завершению программы. Или же бросание исключения в конструкторе приводило к тому, что деструктор не вызывался.
Разработчикам API забыли объяснить для чего нужны исключения. В ре- зультате первое время не было даже деления на ожидаемые (
checked
) и неожиданные (
unchecked
), а API комбинировали как исключения, так и коды возврата.
В большинстве языков для исключений забыли добавить семантику «иг- норировать ожидаемую ошибку». В результате на практике код, использу- ющий исключения как для ожидаемых так и для неожиданных ошибок, с невероятной скоростью обрастал try и catch везде, где только можно.
1   ...   4   5   6   7   8   9   10   11   ...   15


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