Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
986(Исключения из стандартной библиотеки мы будем рассматривать в следующем разделе.) Когда следует употреблять ссылочный вариант dynamic_cast вместо указательного? Это зависит только от желания программиста. При его использовании игнорировать ошибку приведения типа и работать с результатом без проверки (как в указательном варианте) невозможно; с другой стороны, применение исключений увеличивает накладные расходы во время выполнения программы (см. главу 11). #include { try { programmer &rm = dynamic_cast< programmer & >( re ); // èñïîëüçîâàòü rm äëÿ âûçîâà programmer::bonus() } catch ( std::bad_cast ) { // èñïîëüçîâàòü ôóíêöèè-÷ëåíû êëàññà employee } С++ для начинающих 987 19.1.2. Оператор typeid Второй оператор, входящий в состав RTTI, – это typeid, который позволяет выяснить фактический тип выражения. Если оно принадлежит типу класса и этот класс содержит хотя бы одну виртуальную функцию-член, то ответ может и не совпадать с типом самого выражения. Так, если выражение является ссылкой на базовый класс, то typeid сообщает тип производного класса объекта: coiut << typeid( re ).name() << endl; Операнд re оператора typeid имеет тип employee. Но так как re – это ссылка на тип класса с виртуальными функциями, то typeid говорит, что тип адресуемого объекта – programmer (а не employee, на который ссылается re). Программа, использующая такой оператор, должна включать заголовочный файл Где применяется typeid? В сложных системах разработки, например при построении отладчиков, а также при использовании устойчивых объектов, извлеченных из базы данных. В таких системах необходимо знать фактический тип объекта, которым программа манипулирует с помощью указателя или ссылки на базовый класс, например для получения списка его свойств во время сеанса работы с отладчиком или для правильного сохранения или извлечения объекта из базы данных. Оператор typeid допустимо использовать с выражениями и именами любых типов. Например, его операндами могут быть выражения встроенных типов и константы. Если операнд не принадлежит к типу класса, то typeid просто возвращает его тип: cout << typeid( 8.16 ).name() << endl; // печатается: double Если операнд имеет тип класса, в котором нет виртуальных функций, то typeid возвращает тип операнда, а не связанного с ним объекта: cout << typeid( *pb ).name() << endl; // печатается: Base #include // ñ ôóíêöèåé name() ìû ïîçíàêîìèìñÿ â ïîäðàçäåëå, ïîñâÿùåííîì type_info // îíà âîçâðàùàåò C-ñòðîêó "programmer" int iobj; cout << typeid( iobj ).name() << endl; // ïå÷àòàåòñÿ: int class Base { /* нет виртуальных функций */ }; class Derived : public Base { /* íåò âèðòóàëüíûõ ôóíêöèé */ }; Derived dobj; Base *pb = &dobj; С++ для начинающих 988 Операнд typeid имеет тип Base, т.е. тип выражения *pb. Поскольку в классе Base нет виртуальных функций, результатом typeid будет Base, хотя объект, на который указывает pb, имеет тип Derived. Результаты, возвращенные оператором typeid, можно сравнивать. Например: */ Условие в инструкции if сравнивает результаты применения typeid к операнду, являющемуся выражением, и к операнду, являющемуся именем типа. Обратите внимание, что сравнение typeid( pe ) == typeid( employee* ) возвращает истину. Это удивит пользователей, привыкших писать: pe->salary(); что приводит к вызову виртуальной функции salary() из производного класса manager. Поведение typeid(pe) не подчиняется данному механизму. Это связано с тем, что pe – указатель, а для получения типа производного класса операндом typeid должен быть тип класса с виртуальными функциями. Выражение typeid(pe) возвращает тип pe, т.е. указатель на employee. Это значение совпадает со значением typeid(employee*), тогда как все остальные сравнения дают ложь. Только при употреблении выражения *pe в качестве операнда typeid результат будет содержать тип объекта, на который указывает pe: typeid( *pe ) == typeid( employee ) // ложно В этих сравнениях *pe – выражение типа класса, который имеет виртуальные функции, поэтому результатом применения typeid будет тип адресуемого операндом объекта manager Такой оператор можно использовать и со ссылками: #include èñòèííî // ÷òî-òî ñäåëàòü /* if ( typeid( pe ) == typeid( manager* ) ) // ëîæíî if ( typeid( pe ) == typeid( employee ) ) // ëîæíî if ( typeid( pe ) == typeid( manager ) ) // ëîæíî // вызов виртуальной функции typeid( *pe ) == typeid( manager ) // истинно С++ для начинающих 989 typeid( &re ) == typeid( manager* ) // ложно В первых двух сравнениях операнд re имеет тип класса с виртуальными функциями, поэтому результат применения typeid содержит тип объекта, на который ссылается re. В последних двух сравнениях операнд &re имеет тип указателя, следовательно, результатом будет тип самого операнда, т.е. employee*. На самом деле оператор typeid возвращает объект класса типа type_info, который определен в заголовочном файле 19.1.3. Класс type_info Точное определение класса type_info зависит от реализации, но некоторые его характерные черты остаются неизменными в любой программе на C++: }; Поскольку копирующие конструктор и оператор присваивания – закрытые члены класса type_info , то пользователь не может создать его объекты в своей программе: type_info t2 (typeid( unsigned int ) ); Единственный способ создать объект класса type_info – воспользоваться оператором typeid В классе определены также операторы сравнения. Они позволяют сравнивать два объекта type_info , а следовательно, и результаты, возвращенные двумя операторами typeid. (Мы говорили об этом в предыдущем подразделе.) typeid( re ) == typeid( manager ) // истинно typeid( re ) == typeid( employee ) // ложно typeid( &re ) == typeid( employee* ) // истинно class type_info { // ïðåäñòàâëåíèå çàâèñèò îò ðåàëèçàöèè private: type_info( const type_info& ); type_info& operator= ( const type_info& ); public: virtual type_info(); int operator==( const type_info& ); int operator!=( const type_info& ); const char * name() const; #include îøèáêà: íåò êîíñòðóêòîðà ïî óìîë÷àíèþ // îøèáêà: êîïèðóþùèé êîíñòðóêòîð çàêðûò С++ для начинающих 990 typeid( *pe ) != typeid( employee ) // ложно Функция name() возвращает C-строку с именем типа, представленного объектом type_info . Этой функцией можно пользоваться в программах следующим образом: } Для работы с функцией-членом name() нужно включить заголовочный файл Имя типа – это единственная информация, которая гарантированно возвращается всеми реализациями C++, при этом используется функция-член name() класса type_info. В начале этого раздела упоминалось, что поддержка RTTI зависит от реализации и иногда в классе type_info бывают дополнительные функции-члены. Чтобы узнать, каким образом обеспечивается поддержка RTTI в вашем компиляторе, обратитесь к справочному руководству по нему. Кроме того, можно получить любую информацию, которую компилятор знает о типе, например: • список функций-членов класса; • способ размещения объекта в памяти, т.е. взаимное расположение подобъектов базового и производных классов. Одним из способов расширения поддержки RTTI является включение дополнительной информации в класс, производный от type_info. Поскольку в классе type_info есть виртуальный деструктор, то оператор dynamic_cast позволяет выяснить, имеется ли некоторое конкретное расширение RTTI. Предположим, что некоторый компилятор предоставляет расширенную поддержку RTTI посредством класса extended_type_info, производного от type_info. С помощью оператора dynamic_cast программа может узнать, принадлежит ли объект типа type_info, возвращенный оператором typeid, к типу extended_type_info. Если да, то пользоваться расширенной поддержкой RTTI разрешено. typeid( re ) == typeid( manager ) // èñòèííî #include // ïå÷àòàåò: "manager" cout << typeid( *pe ).name() << endl; С++ для начинающих 991 } Если dynamic_cast завершается успешно, то оператор typeid вернет объект класса extended_type_info , т.е. компилятор обеспечивает расширенную поддержку RTTI, чем программа может воспользоваться. В противном случае допустимы только базовые средства RTTI. Упражнение 19.1 Дана иерархия классов, в которой у каждого класса есть конструктор по умолчанию и виртуальный деструктор: class D : public X, public C { ... }; Какие из данных операторов dynamic_cast завершатся неудачно? A *pa = dynamic_cast< A* > ( pd ); C *pc = dynamic_cast< C* > ( pa ); D *pd = dynamic_cast< D* > ( pb ); #include // Файл typeinfo содержит определение типа extended_type_info void func( employee* p ) { // понижающее приведение типа type_info* к extended_type_info* if ( eti *eti_p = dynamic_cast { // если dynamic_cast завершается успешно, // можно пользоваться информацией из extended_type_info через eti_p } else { // если dynamic_cast завершается неудачно, // можно пользоваться только стандартным type_info } class X { ... }; class A { ... }; class B : public A { ... }; class C : public B { ... }; (a) D *pd = new D; (b) A *pa = new C; (c) B *pb = new B; С++ для начинающих 992 X *px = dynamic_cast< X* > ( pa ); Упражнение 19.2 Объясните, когда нужно пользоваться оператором dynamic_cast вместо виртуальной функции? Упражнение 19.3 Пользуясь иерархией классов из упражнения 19.1, перепишите следующий фрагмент так, чтобы в нем использовался ссылочный вариант dynamic_cast для преобразования *pa в тип D&: } Упражнение 19.4 Дана иерархия классов, в которой у каждого класса есть конструктор по умолчанию и виртуальный деструктор: class D : public X, public C { ... }; Какое имя типа будет напечатано в каждом из следующих случаев: cout << typeid( ra ).name() << endl; (d) A *pa = new D; if ( D *pd = dynamic_cast< D* >( pa ) ) { // использовать члены D } else { // использовать члены A class X { ... }; class A { ... }; class B : public A { ... }; class C : public B { ... }; (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; С++ для начинающих 993 19.2. Исключения и наследование Обработка исключений – это стандартное языковое средство для реакции на аномальное поведение программы во время выполнения. C++ поддерживает единообразный синтаксис и стиль обработки исключений, а также способы тонкой настройки этого механизма в специальных ситуациях. Основы его поддержки в языке C++ описаны в главе 11, где показано, как программа может возбудить исключение, передать управление его обработчику (если таковой существует) и как обработчики исключений ассоциируются с try-блоками. Возможности механизма обработки исключений становятся больше, если в качестве исключений использовать иерархии классов. В этом разделе мы расскажем, как писать программы, которые умеют возбуждать и обрабатывать исключения, принадлежащие таким иерархиям. 19.2.1. Исключения, определенные как иерархии классов В главе 11 мы использовали два типа класса для описания исключений, возбуждаемых функциями-членами нашего класса iStack: class pushOnFull { ... }; В реальных программах на C++ типы классов, представляющих исключения, чаще всего организуются в группы, или иерархии. Как могла бы выглядеть вся иерархия для этих классов? Мы можем определить базовый класс Excp, которому наследуют оба наши класса исключений. Он инкапсулирует данные и функции-члены, общие для обоих производных: class pushOnFull : public Excp { ... }; Одной из операцией, которые предоставляет базовый класс, является вывод сообщения об ошибке. Эта возможность используется обоими классами, стоящими ниже в иерархии: }; class popOnEmpty { ... }; class Excp { ... }; class popOnEmpty : public Excp { ... }; class Excp { public: // напечатать сообщение об ошибке static void print( string msg ) { cerr << msg << endl; } С++ для начинающих 994 Иерархию классов исключений разрешается развивать и дальше. От Excp можно произвести другие классы для более точного описания исключений, обнаруживаемых программой: class pushOnFull : public stackExcp { ... }; class divideByZero : public mathExcp { ... }; Последующие уточнения позволяют более детально идентифицировать аномальные ситуации в работе программы. Дополнительные классы исключений организуются как слои. По мере углубления иерархии каждый новый слой описывает все более специфичные исключения. Например, первый, самый общий слой в приведенной выше иерархии представлен классом Excp. Второй специализирует Excp, выделяя из него два подкласса: stackExcp (для исключений при работе с нашим iStack) и mathExcp (для исключений, возбуждаемых функциями из математической библиотеки). Третий, самый специализированный слой данной иерархии уточняет классы исключений: popOnEmpty и pushOnFull определяют два вида исключений работы со стеком, а ZeroOp и divideByZero – два вида исключений математических операций. В последующих разделах мы рассмотрим, как возбуждаются и обрабатываются исключения, представленные классами в нашей иерархии. 19.2.2. Возбуждение исключения типа класса Теперь, познакомившись с классами, посмотрим, что происходит, когда функция-член push() нашего iStack возбуждает исключение: } Выполнение инструкции throw инициирует несколько последовательных действий: 1. Инструкция throw создает временный объект типа класса pushOnFull, вызывая его конструктор. 2. С помощью копирующего конструктора генерируется объект-исключение типа pushOnFull – копия временного объекта, полученного на шаге 1. Затем он передается обработчику исключения. 3. Временный объект, созданный на шаге 1, уничтожается до начала поиска обработчика. class Excp { ... }; class stackExcp : public Excp { ... }; class popOnEmpty : public stackExcp { ... }; class mathExcp : public Excp ( ... }; class zeroOp : public mathExcp { ... }; void iStack::push( int value ) { if ( full() ) // value сохраняется в объекте-исключении throw pushOnFull( value ); // ... С++ для начинающих 995 Зачем нужно генерировать объект-исключение (шаг 2)? Инструкция throw pushOnFull( value ); создает временный объект, который уничтожается в конце работы throw. Но исключение должно существовать до тех пор, пока не будет найден его обработчик, а он может находиться намного выше в цепочке вызовов. Поэтому необходимо скопировать временный объект в некоторую область памяти(объект-исключение), которая гарантированно существует, пока исключение не будет обработано. Иногда компилятор создает объект-исключение сразу, минуя шаг 1. Однако стандарт этого не требует, да и не всегда такое возможно. Поскольку объект-исключение создается путем копирования значения, переданного инструкции throw, то возбужденное исключение всегда имеет такой же тип, как и это значение: } Выражение *pse имеет тип stackExcp. Тип созданного объекта-исключения – stackExcp , хотя pse ссылается на объект с фактическим типом pushOnFull. Фактический тип объекта, на который ссылается throw, при создании объекта- исключения не учитывается. Поэтому исключение не будет перехвачено catch- обработчиком pushOnFull. Действия, выполняемые инструкцией throw, налагают определенные ограничения на то, какие классы можно использовать для создания объектов-исключений. Оператор throw в функции-члене push() класса iStack вызовет ошибку компиляции, если: • в классе pushOnFull нет конструктора, принимающего аргумент типа int, или этот конструктор недоступен; • в классе pushOnFull есть копирующий конструктор или деструктор, но хотя бы один из них недоступен; • pushOnFull – это абстрактный базовый класс. Напомним, что программа не может создавать объекты абстрактных классов (см. раздел 17.1). |