
Язык Программирования C++. Вводный курс [rus]
.pdf
С++ для начинающих |
1002 |
class bad_alloc : public exception { // ...
public:
bad_alloc() throw();
bad_alloc( const bad_alloc & ) throw();
bad_alloc & operator=( const bad_alloc & ) throw(); virtual ~bad_alloc() throw();
virtual const char* what() const throw();
};
Отметим, что если функция-член объявлена с модификатором const или volatile, как, скажем, what() в примере выше, то спецификация исключений должна идти после него.
Во всех объявлениях одной и той же функции спецификации исключений обязаны содержать одинаковые типы. Если речь идет о функции-члене, определение которой находится вне определения класса, то спецификации исключений в этом определении и в
#include <stdexcept>
// <stdexcept> определяет класс overflow_error
class transport {
//...
public:
double cost( double, double ) throw ( overflow_error );
//...
};
//ошибка: спецификация исключений отличается от той, что задана
//в объявлении в списке членов класса
объявлении функции должны совпадать:
double transport::cost( double rate, double distance ) { }
Виртуальная функция в базовом классе может иметь спецификацию исключений, отличающуюся от той, что задана для замещающей функции-члена в производном.
Однако в производном классе эта спецификация для виртуальной функции должна накладывать не меньше ограничений, чем в базовом:

С++ для начинающих |
1003 |
class Base { public:
virtual double f1( double ) throw(); virtual int f2( int ) throw( int ); virtual string f3() throw( int, string ); // ...
}
class Derived : public Base { public:
//ошибка: спецификация исключений накладывает меньше ограничений,
//чем на Base::f1()
double f1( double ) throw( string );
//правильно: та же спецификация исключений, что и для Base::f2() int f2( int ) throw( int );
//правильно: спецификация исключений f3() накладывает больше
ограничений
string f3( ) throw( int ); // ...
};
Почему спецификация исключений в производном классе должна накладывать не меньше ограничений, чем в базовом? В этом случае мы можем быть уверены, что вызов
виртуальной функции из производного класса по указателю на тип базового не нарушит
// гарантируется, что исключения возбуждены не будут void compute( Base *pb ) throw()
{
try {
pb->f3( ); // может возбудить исключение типа int или string
}
// обработка исключений, возбужденных в Base::f3() catch ( const string & ) { }
catch ( int ) { }
спецификацию исключений функции-члена базового класса:
}
Объявление f3() в классе Base гарантирует, что эта функция возбуждает лишь исключения типа int или string. Следовательно, функция compute() включает catch- обработчики только для них. Поскольку спецификация исключений f3() в производном классе Derived накладывает больше ограничений, чем в базовом Base, то при
программировании в согласии с интерфейсом класса Base наши ожидания не будут обмануты.
В главе 11 мы говорили о том, что между типом возбужденного исключения и типом, заданным в спецификации исключений, не допускаются никакие преобразования. Однако если там указан тип класса, то функция может возбуждать исключения в виде объекта класса, открыто наследующего заданному. Аналогично, если имеется указатель на класс, то функции разрешено возбуждать исключения в виде указателя на объект класса, открыто наследующего заданному. Например:

С++ для начинающих |
1004 |
||
|
|
class stackExcp : public Excp { }; |
|
|
|
|
|
|
|
class popObEmpty : public stackExcp { }; |
|
|
|
class pushOnFull : public stackExcp { }; |
|
|
|
void stackManip() throw( stackExcp ) |
|
|
|
{ |
|
|
|
// ... |
|
|
|
} |
|
|
|
||
|
|
|
|
Спецификация исключений указывает, что stackManip() может возбуждать исключения |
|
||
не только типа stackExcp, но также popOnEmpty и pushOnFull. Напомним, что класс, |
|
||
открыто наследующий базовому, представляет собой пример отношения ЯВЛЯЕТСЯ, т.е. |
|
||
является частным случае более общего базового класса. Поскольку popOnEmpty и |
|
||
pushOnFull – частные случаи stackExcp, они не нарушают спецификации исключений |
|
||
функции stackManip(). |
|
19.2.7. Конструкторы и функциональные try-блоки
Можно объявить функцию так, что все ее тело будет заключено в try-блок. Такие try-
int main() { try {
// тело функции main()
}
catch ( pushOnFull ) { // ...
}
catch ( popOnEmpty ) { // ...
блоки называются функциональными. (Мы упоминали их в разделе 11.2.) Например:
}
Функциональный try-блок ассоциирует группу catch-обработчиков с телом функции. Если инструкция внутри тела возбуждает исключение, то поиск его обработчика ведется среди тех, что следуют за телом функции.
Функциональный try-блок необходим для конструкторов класса. Почему? Определение
имя_класса( список_параметров ) // список инициализации членов:
: член1(выражение1 ) , |
// инициализация член1 |
член2(выражение2 ) , |
// инициализация член2 |
// тело функции: |
|
конструктора имеет следующий вид:
{ /* ... */ }
выражение1 и выражение2 могут быть выражениями любого вида, в частности функциями, которые возбуждают исключения.

С++ для начинающих |
1005 |
Рассмотрим еще раз класс Account, описанный в главе 14. Его конструктор можно
inline Account::
Account( const char* name, double opening_bal )
: _balance( opening_bal - ServiceCharge() )
{
_name = new char[ strlen(name) + 1 ]; strcpy( _name, name );
_acct_nmbr = get_unique_acct_nmbr();
переопределить так:
}
Функция ServiceCharge(), вызываемая для инициализации члена _balance, может возбуждать исключение. Как нужно реализовать конструктор, если мы хотим обрабатывать все исключения, возбуждаемые функциями, которые вызываются при конструировании объекта типа Account?
inline Account::
Account( const char* name, double opening_bal )
: _balance( opening_bal - ServiceCharge() )
{
try {
_name = new char[ strlen(name) + 1 ]; strcpy( _name, name );
_acct_nmbr = get_unique_acct_nmbr();
}
catch (...) {
//специальная обработка
//не перехватывает исключения,
//возбужденные в списке инициализации членов
}
Помещать try-блок в тело функции нельзя:
}
Поскольку try-блок не охватывает список инициализации членов, то catch-обработчик, находящийся в конце конструктора, не рассматривается при поиске кандидатов, которые способны перехватить исключение, возбужденное в функции ServiceCharge().
Использование функционального try-блока – это единственное решение, гарантирующее, что все исключения, возбужденные при создании объекта, будут перехвачены в конструкторе. Для конструктора класса Account такой try-блок можно определить следующим образом:

С++ для начинающих |
1006 |
inline Account::
Account( const char* name, double opening_bal ) try
: _balance( opening_bal - ServiceCharge() )
{
_name = new char[ strlen(name) + 1 ]; strcpy( _name, name );
_acct_nmbr = get_unique_acct_nmbr();
catch (...) {
//теперь специальная обработка
//перехватывает исключения,
//возбужденные в ServiceCharge()
}
}
Обратите внимание, что ключевое слово try находится перед списком инициализации членов, а составная инструкция, образующая try-блок, охватывает тело конструктора.
Теперь предложение catch(...) принимается во внимание при поиске обработчика исключения, возбужденного как в списке инициализации членов, так и в теле конструктора.
19.2.8. Иерархия классов исключений в стандартной библиотеке C++
В начале этого раздела мы определили иерархию классов исключений, с помощью которой наша программа сообщает об аномальных ситуациях. В стандартной библиотеке C++ есть аналогичная иерархия, предназначенная для извещения о проблемах при выполнении функций из самой стандартной библиотеки. Эти классы исключений вы
можете использовать в своих программах непосредственно или создать производные от них классы для описания собственных специфических исключений.
Корневой класс исключения в стандартной иерархии называется exception. Он
определен в стандартном заголовочном файле <exception> и является базовым для всех исключений, возбуждаемых функциями из стандартной библиотеки. Класс exception
namespace std { class exception public:
exception() throw();
exception( const exception & ) throw(); exception& operator=( const exception & ) throw(); virtual ~exception() throw();
virtual const char* what() const throw();
};
имеет следующий интерфейс:
}
Как и всякий другой класс из стандартной библиотеки C++, exception помещен в пространство имен std, чтобы не засорять глобальное пространство имен программы.

С++ для начинающих |
1007 |
Первые четыре функции-члена в определении класса – это конструктор по умолчанию, копирующий конструктор, копирующий оператор присваивания и деструктор. Поскольку все они открыты, любая программа может свободно создавать и копировать объекты- исключения, а также присваивать им значения. Деструктор объявлен виртуальным, чтобы сделать возможным дальнейшее наследование классу exception.
Самой интересной в этом списке является виртуальная функция what(), которая возвращает C-строку с текстовым описанием возбужденного исключения. Классы, производные от exception, могут заместить what() собственной версией, которая лучше характеризует объект-исключение.
Отметим, что все функции в определении класса exception имеют пустую спецификацию throw(), т.е. не возбуждают никаких исключений. Программа может манипулировать объектами-исключениями (к примеру, внутри catch-обработчиков типа exception), не опасаясь, что функции создания, копирования и уничтожения этих объектов возбудят исключения.
Помимо корневого exception, в стандартной библиотеке есть и другие классы, которые допустимо использовать в программе для извещения об ошибках, обычно подразделяемых на две больших категории: логические ошибки и ошибки времени выполнения.
Логические ошибки обусловлены нарушением внутренней логики программы, например логических предусловий или инвариантов класса. Предполагается, что их можно найти и предотвратить еще до начала выполнения программы. В стандартной библиотеке
namespace std {
class logic_error : public exception { // логическая ошибка public:
explicit logic_error( const string &what_arg );
};
class invalid_argument : public logic_error { // неверный аргумент public:
explicit invalid_argument( const string &what_arg );
};
class out_of_range : public logic_error { // вне диапазона public:
explicit out_of_range( const string &what_arg );
};
class length_error : public logic_error { // неверная длина public:
explicit length_error( const string &what_arg );
};
class domain_error : public logic_error { // вне допустимой области public:
explicit domain_error( const string &what_arg );
};
определены следующие такие ошибки:
}
Функция может возбудить исключение invalid_argument, если получит аргумент с некорректным значением; в конкретной ситуации, когда значение аргумента выходит за пределы допустимого диапазона, разрешается возбудить исключение out_of_range, а length_error используется для оповещения о попытке создать объект, длина которого превышает максимально возможную.

С++ для начинающих |
1008 |
Ошибки времени выполнения, напротив, вызваны событием, с самой программой не связанным. Предполагается, что их нельзя обнаружить, пока программа не начала
namespace std {
class runtime_error : public exception { // ошибка времени выполнения public:
explicit runtime_error( const string &what_arg );
};
class range_error : public runtime_error { // ошибка диапазона public:
explicit range_error( const string &what_arg );
};
class overflow_error : public runtime_error { // переполнение public:
explicit overflow_error( const string &what_arg );
};
class underflow_error : public runtime_error { // потеря значимости public:
explicit underflow_error( const string &what_arg );
};
работать. В стандартной библиотеке определены следующие такие ошибки:
}
Функция может возбудить исключение range_error, чтобы сообщить об ошибке во внутренних вычислениях. Исключение overflow_error говорит об ошибке арифметического переполнения, а underflow_error – о потере значимости.
Класс exception является базовым и для класса исключения bad_alloc, которое возбуждает оператор new(), когда ему не удается выделить запрошенный объем памяти (см. раздел 8.4), и для класса исключения bad_cast, возбуждаемого в ситуации, когда ссылочный вариант оператора dynamic_cast не может быть выполнен (см. раздел 19.1).
Переопределим оператор operator[] в шаблоне Array из раздела 16.12 так, чтобы он возбуждал исключение типа range_error, если индекс массива Array выходит за границы:

С++ для начинающих |
1009 |
#include <stdexcept> #include <string>
template <class elemType> class Array {
public: // ...
elemType& operator[]( int ix ) const
{
if ( ix < 0 || ix >= _size )
{
string eObj =
"ошибка: вне диапазона в Array<elemType>::operator[]()";
throw out_of_range( eObj );
}
return _ia[ix];
}
// ...
private:
int _size; elemType *_ia;
};
Для использования предопределенных классов исключений в программу необходимо включить заголовочный файл <stdexcept>. Описание возбужденного исключения содержится в объекте eObj типа string. Эту информацию можно извлечь в обработчике
int main()
{
try {
// функция main() такая же, как в разделе 16.2
}
catch ( const out_of_range &excep ) {
//печатается:
//ошибка: вне диапазона в Array<elemType>::operator[]() cerr << excep.what() << "\n";
return -1;
}
с помощью функции-члена what():
}
В данной реализации выход индекса за пределы массива в функции try_array() приводит к тому, что оператор взятия индекса operator[]() класса Array возбуждает исключение типа out_of_range, которое перехватывается в main().
Упражнение 19.5
Какие исключения могут возбуждать следующие функции:

С++ для начинающих |
1010 |
#include <stdexcept>
(a)void operate() throw( logic_error );
(b)int mathErr( int ) throw( underflow_error, overflow_error );
(c)char manip( string ) throw( );
Упражнение 19.6
Объясните, как механизм обработки исключений в C++ поддерживает технику программирования “захват ресурса – это инициализация; освобождение ресурса – это уничтожение”.
Упражнение 19.7
#include <stdexcept>
int main() { try {
// использование функций из стандартной библиотеки
}
catch( exception ) {
}
catch( runtime_error &re ) {
}
catch( overflow_error eobj ) {
}
Исправьте ошибку в списке catch-обработчиков для данного try-блока:
}
Упражнение 19.8
int main() {
// использование стандартной библиотеки
Дана программа на C++:
}
Модифицируйте main() так, чтобы она перехватывала все исключения, возбуждаемые функциями стандартной библиотеки. Обработчики должны печатать сообщение об ошибке, ассоциированное с исключением, а затем вызывать функцию abort() (она определена в заголовочном файле <cstdlib>) для завершения main().
19.3. Разрешение перегрузки и наследование A
Наследование классов оказывает влияние на все аспекты разрешения перегрузки функций (см. раздел 9.2). Напомним, что эта процедура состоит из трех шагов:
1. Отбор функций-кандидатов.

С++ для начинающих |
1011 |
2.Отбор устоявших функций.
3.Выбор наилучшей из устоявших функции.
Отбор функций-кандидатов зависит от наследования потому, что на этом шаге принимаются во внимание функции, ассоциированные с базовыми классами, – как их функции-члены, так и функции, объявленные в тех же пространствах имен, где определены базовые классы. Отбор устоявших функций также зависит от наследования,
поскольку множество преобразований формальных параметров функции в фактические аргументы расширяется пользовательскими преобразованиями. Кроме того, наследование оказывает влияние на ранжирование последовательностей трансформаций аргументов, а значит, и на выбор наилучшей из устоявших функции. В данном разделе мы рассмотрим влияние наследования на эти три шага разрешения перегрузки более подробно.
19.3.1. Функции-кандидаты
Наследование влияет на первый шаг процедуры разрешения перегрузки функции – формирование множества кандидатов для данного вызова, причем это влияние может быть различным в зависимости от того, рассматривается ли вызов обычной функции
вида
func( args );
object.memfunc( args );
или функции-члена с помощью операторов доступа “точка” или “стрелка”: pointer->memfunc( args );
В данном разделе мы изучим оба случая.
Если аргумент обычной функции имеет тип класса, ссылки или указателя на тип класса, и класс определен в пространстве имен, то кандидатами будут все одноименные функции, объявленные в этом пространстве, даже если они невидимы в точке вызова (подробнее об этом говорилось в разделе 15.10). Если аргумент при наследовании имеет тип класса, ссылки или указателя на тип класса, и у этого класса есть базовые, то в множество кандидатов добавляются также функции, объявленные в тех пространствах имен, где
namespace NS {
class ZooAnimal { /* ... */ }; void display( const ZooAnimal& );
}
// базовый класс Bear объявлен в пространстве имен NS class Bear : public NS::ZooAnimal { };
int main() { Bear baloo;
display( baloo ); return 0;
определены базовые классы. Например:
}