Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
267
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

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

992

(d)A *pa = new D;

X *px = dynamic_cast< X* > ( pa );

Упражнение 19.2

Объясните, когда нужно пользоваться оператором dynamic_cast вместо виртуальной функции?

Упражнение 19.3

Пользуясь иерархией классов из упражнения 19.1, перепишите следующий фрагмент так,

чтобы в нем использовался ссылочный вариант dynamic_cast для преобразования *pa в

if ( D *pd = dynamic_cast< D* >( pa ) ) { // использовать члены D

}

else {

// использовать члены A

тип D&:

}

Упражнение 19.4 Дана иерархия классов, в которой у каждого класса есть конструктор по умолчанию и

class X { ... }; class A { ... };

class B : public A { ... }; class C : public B { ... };

виртуальный деструктор:

class D : public X, public C { ... };

(a)A *pa = new D;

cout << typeid( pa ).name() << endl;

(b)X *px = new D;

cout << typeid( *px ).name() << endl;

(c)C obj;

A& ra = cobj;

cout << typeid( &ra ).name() << endl;

(d)X *px = new D; A& ra = *px;

Какое имя типа будет напечатано в каждом из следующих случаев: cout << typeid( ra ).name() << endl;

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

993

19.2. Исключения и наследование

Обработка исключений это стандартное языковое средство для реакции на аномальное поведение программы во время выполнения. C++ поддерживает единообразный синтаксис и стиль обработки исключений, а также способы тонкой настройки этого механизма в специальных ситуациях. Основы его поддержки в языке C++ описаны в главе 11, где показано, как программа может возбудить исключение, передать управление его обработчику (если таковой существует) и как обработчики исключений ассоциируются с try-блоками.

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

19.2.1. Исключения, определенные как иерархии классов

В главе 11 мы использовали два типа класса для описания исключений, возбуждаемых

class popOnEmpty { ... };

функциями-членами нашего класса iStack: class pushOnFull { ... };

В реальных программах на C++ типы классов, представляющих исключения, чаще всего организуются в группы, или иерархии. Как могла бы выглядеть вся иерархия для этих классов?

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

class

Excp { ...

};

};

class

popOnEmpty

: public Excp { ...

производных:

class pushOnFull : public Excp { ... };

Одной из операцией, которые предоставляет базовый класс, является вывод сообщения об

class Excp { public:

// напечатать сообщение об ошибке static void print( string msg ) {

cerr << msg << endl;

}

ошибке. Эта возможность используется обоими классами, стоящими ниже в иерархии:

};

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

994

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

class Excp { ... };

class stackExcp : public Excp { ... };

class popOnEmpty : public stackExcp { ... };

программой:

class mathExcp : public Excp ( ...

};

};

class zeroOp : public mathExcp { ...

class pushOnFull : public stackExcp { ... }; class divideByZero : public mathExcp { ... };

Последующие уточнения позволяют более детально идентифицировать аномальные ситуации в работе программы. Дополнительные классы исключений организуются как слои. По мере углубления иерархии каждый новый слой описывает все более специфичные исключения. Например, первый, самый общий слой в приведенной выше иерархии представлен классом Excp. Второй специализирует Excp, выделяя из него два подкласса: stackExcp (для исключений при работе с нашим iStack) и mathExcp (для исключений, возбуждаемых функциями из математической библиотеки). Третий, самый специализированный слой данной иерархии уточняет классы исключений: popOnEmpty и pushOnFull определяют два вида исключений работы со стеком, а ZeroOp и divideByZero два вида исключений математических операций.

В последующих разделах мы рассмотрим, как возбуждаются и обрабатываются исключения, представленные классами в нашей иерархии.

19.2.2. Возбуждение исключения типа класса

Теперь, познакомившись с классами, посмотрим, что происходит, когда функция-член

void iStack::push( int value )

{

if ( full() )

// value сохраняется в объекте-исключении throw pushOnFull( value );

// ...

push() нашего iStack возбуждает исключение:

}

Выполнение инструкции throw инициирует несколько последовательных действий:

1.Инструкция throw создает временный объект типа класса pushOnFull, вызывая его конструктор.

2.С помощью копирующего конструктора генерируется объект-исключение типа pushOnFull копия временного объекта, полученного на шаге 1. Затем он передается обработчику исключения.

3.Временный объект, созданный на шаге 1, уничтожается до начала поиска обработчика.

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

995

Зачем нужно генерировать объект-исключение (шаг 2)? Инструкция

throw pushOnFull( value );

создает временный объект, который уничтожается в конце работы throw. Но исключение должно существовать до тех пор, пока не будет найден его обработчик, а он может находиться намного выше в цепочке вызовов. Поэтому необходимо скопировать временный объект в некоторую область памяти (объект-исключение), которая гарантированно существует, пока исключение не будет обработано. Иногда компилятор создает объект-исключение сразу, минуя шаг 1. Однако стандарт этого не требует, да и не всегда такое возможно.

Поскольку объект-исключение создается путем копирования значения, переданного инструкции throw, то возбужденное исключение всегда имеет такой же тип, как и это

void iStack::push( int value ) { if ( full() ) {

pushOnFull except( value ); stackExcp *pse = &except;

throw *pse; // объект-исключение имеет тип stackExcp

}

// ...

значение:

}

Выражение *pse имеет тип stackExcp. Тип созданного объекта-исключения stackExcp, хотя pse ссылается на объект с фактическим типом pushOnFull. Фактический тип объекта, на который ссылается throw, при создании объекта- исключения не учитывается. Поэтому исключение не будет перехвачено catch- обработчиком pushOnFull.

Действия, выполняемые инструкцией throw, налагают определенные ограничения на то, какие классы можно использовать для создания объектов-исключений. Оператор throw в функции-члене push() класса iStack вызовет ошибку компиляции, если:

в классе pushOnFull нет конструктора, принимающего аргумент типа int, или этот конструктор недоступен;

в классе pushOnFull есть копирующий конструктор или деструктор, но хотя бы один из них недоступен;

pushOnFull это абстрактный базовый класс. Напомним, что программа не может создавать объекты абстрактных классов (см. раздел 17.1).

19.2.3. Обработка исключения типа класса

Если исключения организуются в иерархии, то исключение типа некоторого класса может быть перехвачено обработчиком, соответствующим любому его открытому базовому классу. Например, исключение типа pushOnFull перехватывается обработчиками исключений типа stackExcp или Excp.

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

996

int main() { try {

// ...

}

catch ( Excp ) {

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

}

catch ( pushOnFull ) {

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

}

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

catch ( pushOnFull ) {

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

}

catch ( Excp ) {

// обрабатывает другие исключения

последовательность такова:

}

catch-обработчик для производного класса должен идти первым. Тогда catch-обработчик для базового класса получит управление только в том случае, если более специализированного обработчика не нашлось.

Если исключения организованы в иерархии, то пользователи библиотеки классов могут выбрать в своем приложении уровень детализации при работе с исключениями, возбужденными внутри библиотеки. Например, кодируя функцию main(), мы решили, что исключения типа pushOnFull должны обрабатываться несколько иначе, чем прочие,

ипотому написали для них специализированный catch-обработчик. Что касается

catch ( pushOnFull eObj ) {

//используется функция-член value() класса pushOnFull

//см. раздел 11.3

cerr << "попытка поместить значение " << eObj.value() << " в полный стек\n";

}

catch ( Excp ) {

// используется функция-член print() базового класса Excp::print( "произошло исключение" );

остальных исключений, то они обрабатываются единообразно:

}

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

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

997

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

Объявление исключения в catch-обработчике (находящееся в скобках после слова catch) очень похоже на объявление параметра функции. В приведенном примере оно напоминает параметр, передаваемый по значению. Объект eObj инициализируется копией значения объекта-исключения точно так же, как передаваемый по значению формальный параметр функции инициализируется значением фактического аргумента. Как и в случае с параметрами функции, в объявлении исключения можно использовать ссылки. Тогда catch-обработчик имеет доступ непосредственно к объекту-исключению, созданному выражением throw, а не к его локальной копии. Чтобы избежать копирования больших объектов, параметры типа класса следует объявлять как ссылки; в объявлениях исключений тоже желательно делать исключения типа класса ссылками. В зависимости от того, что находится в таком объявлении (объект или ссылка), поведение обработчика различается (мы покажем эти различия в данном разделе).

В главе 11 были введены выражения повторного возбуждения исключения, которые используются в catch-обработчике для передачи исключения какому-то другому обработчику выше в цепочке вызовов. Такое выражение имеет вид

throw;

Как ведет себя эта инструкция, если она расположена в catch-обработчике исключений базового класса? Например, каким будет тип повторно возбужденного исключения, если

void calculate( int parm ) { try {

mathFunc( parm ); // возбуждает исключение divideByZero

}

catch ( mathExcp mExcp ) {

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

//и генерирует объект-исключение еще раз

throw;

}

mathFunc() возбуждает исключение типа divideByZero?

}

Будет ли повторно возбужденное исключение иметь тип divideByZero тот же, что и исключение, возбужденное функцией mathFunc()? Или тип mathExcp, который указан в объявлении исключения в catch-обработчике?

Напомним, что выражение throw повторно генерирует исходный объект-исключение. Так как исходный объект имеет тип divideByZero, то повторно возбужденное исключение будет такого же типа. В catch-обработчике объект mExcp инициализируется копией подобъекта объекта типа divideByZero, который соответствует его базовому классу MathExcp. Доступ к ней осуществляется только внутри catch-обработчика, она не является исходным объектом-исключением, который повторно генерируется.

Предположим, что классы в нашей иерархии исключений имеют деструкторы:

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

998

class pushOnFull { public:

pushOnFull( int i ) : _value( i ) { } int value() { return _value; }

~pushOnFull(); // вновь объявленный деструктор private:

int _value;

};

catch ( pushOnFull eObj ) {

cerr << "попытка поместить значение " << eObj.value() << " в полный стек\n";

Когда они вызываются? Чтобы ответить на этот вопрос, рассмотрим catch-обработчик:

}

Поскольку в объявлении исключения eObj объявлен как локальный для catch- обработчика объект, а в классе pushOnFull есть деструктор, то eObj уничтожается при выходе из обработчика. Когда же вызывается деструктор для объекта-исключения, созданного в момент возбуждения исключения, – при входе в catch-обработчик или при выходе из него? Однако уничтожать исключение в любой из этих точек может быть слишком рано. Можете сказать, почему? Если catch-обработчик возбуждает исключение повторно, передавая его выше по цепочке вызовов, то уничтожать объект-исключение нельзя до момента выхода из последнего catch-обработчика.

19.2.4. Объекты-исключения и виртуальные функции

Если сгенерированный объект-исключение имеет тип производного класса, а обрабатывается catch-обработчиком для базового, то этот обработчик не может использовать особенности производного класса. Например, к функции-члену value(),

catch ( const Excp &eObj ) {

// ошибка: в классе Excp нет функции-члена value() cerr << "попытка поместить значение " << eObj.value()

<< " в полный стек\n";

которая объявлена в классе pushOnFull, нельзя обращаться в catch-обработчике Excp:

}

Но мы можем перепроектировать иерархию классов исключений и определить виртуальные функции, которые можно вызывать из catch-обработчика для базового класса Excp с целью получения доступа к функциям-членам более специализированного производного:

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

999

// новые определения классов, включающие виртуальные функции class Excp {

public:

virtual void print( string msg ) { cerr << "Произошло исключение"

<< endl;

}

class stackExcp : public Excp { }; class pushOnFull : public stackExcp { public:

virtual void print() {

cerr << "попытка поместить значение " << _value << " в полный стек\n";

}

// ...

int main() { try {

//iStack::push() возбуждает исключение pushOnFull

}catch ( Excp eObj ) {

eObj.print();

//

хотим вызвать

виртуальную функцию,

 

//

но вызывается

экземпляр из базового класса

}

};

};

Функцию print() теперь можно использовать в catch-обработчике следующим образом:

}

Хотя возбужденное исключение имеет тип pushOnFull, а функция print() виртуальна, инструкция eObj.print() печатает такую строку:

Произошло исключение

Вызываемая print() является членом базового класса Excp, а не замещает ее в производном. Но почему?

Вспомните, что объявление исключения в catch-обработчике ведет себя почти так же, так объявление параметра. Когда управление попадает в catch-обработчик, то, поскольку в нем объявлен объект, а не ссылка, eObj инициализируется копией подобъекта Excp базового класса объекта исключения. Поэтому eObj это объект типа Excp, а не pushOnFull. Чтобы вызвать виртуальные функции из производных классов, в объявлении исключения должен быть указатель или ссылка:

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

1000

int main() { try {

//iStack::push() возбуждает исключение pushOnFull

}catch ( const Excp &eObj ) {

eObj.print(); // вызывается виртуальная функция

// pushOnFull::print()

}

}

Объявление исключения в этом примере тоже относится к базовому классу Excp, но так как eObj ссылка и при этом именует объект-исключение типа pushOnFull, то для нее можно вызывать виртуальные функции, определенные в классе pushOnFull. Когда catch-обработчик обращается к виртуальной функции print(), вызывается функция из производного класса, и программа печатает следующую строку:

попытка поместить значение 879 в полный стек

Таким образом, ссылка в объявлении исключения позволяет вызывать виртуальные функции, ассоциированные с классом объекта-исключения.

19.2.5. Раскрутка стека и вызов деструкторов

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

Во время раскрутки поочередно происходят аномальные выходы из просмотренных функций. Если функция захватила некоторый ресурс (например, открыла файл или выделила из хипа память), он в таком случае не освобождается.

Существует прием, позволяющий решить эту проблему. Всякий раз, когда во время поиска обработчика происходит выход из составной инструкции или блока, где определен некоторый локальный объект, для этого объекта автоматически вызывается деструктор. (Локальные объекты рассматривались в разделе 8.1.)

Например, следующий класс инкапсулирует выделение памяти для массива целых в

class PTR { public:

PTR() { ptr = new int[ chunk ]; } ~PTR { delete[] ptr; }

private: int *ptr;

конструкторе и ее освобождение в деструкторе:

};

Локальный объект такого типа создается в функции manip() перед вызовом mathFunc():

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

1001

void manip( int parm ) {

PTR localPtr;

// ...

mathFunc( parm ); // возбуждает исключение divideByZero

// ...

}

Если mathFunc() возбуждает исключение типа divideByZero, то начинается раскрутка стека. В процессе поиска подходящего catch-обработчика проверяется и функция manip(). Поскольку вызов mathFunc() не заключен в try-блок, то manip() нужного обработчика не содержит. Поэтому стек раскручивается дальше по цепочке вызовов. Но

перед выходом из manip() с необработанным исключением процесс раскрутки уничтожает все объекты типа классов, которые локальны в ней и были созданы до вызова mathFunc(). Таким образом, локальный объект localPtr уничтожается до того, как поиск пойдет дальше, а следовательно, память, на которую он указывает, будет освобождена и утечки не произойдет.

Поэтому говорят, что процесс обработки исключений в C++ поддерживает технику программирования, основной принцип которой можно сформулировать так: “захват ресурса это инициализация; освобождение ресурса это уничтожение”. Если ресурс реализован в виде класса и, значит, действия по его захвату сосредоточены в конструкторе, а действия по освобождению в деструкторе (как, например, в классе PTR выше), то локальный для функции объект такого класса автоматически уничтожается при выходе из функции в результате необработанного исключения. Действия, которые должны быть выполнены для освобождения ресурса, не будут пропущены при раскрутке стека, если они инкапсулированы в деструкторы, вызываемые для локальных объектов.

Класс auto_ptr, определенный в стандартной библиотеке (см. раздел 8.4), ведет себя почти так же, как наш класс PTR. Это средство для инкапсуляции выделения памяти в конструкторе и ее освобождения в деструкторе. Если для выделения одиночного объекта из хипа используется auto_ptr, то гарантируется, что при выходе из составной инструкции или функции из-за необработанного исключения память будет освобождена.

19.2.6. Спецификации исключений

С помощью спецификации исключений (см. раздел 11.4) в объявлении функции указывается множество исключений, которые она может возбуждать прямо или косвенно. Спецификация позволяет гарантировать, что функция не возбудит не перечисленные в ней исключения.

Такую спецификацию разрешается задавать для функций-членов класса так же, как и для обычных функций; она должна следовать за списком параметров функции-члена. Например, в определении класса bad_alloc из стандартной библиотеки C++ функции- члены имеют пустую спецификацию исключений throw(), т.е. гарантированно не возбуждают никаких исключений: