|
void list::unique(); template <class BinaryPredicate> |
void list::unique( BinaryPredicate pred );
Операция unique()
удаляет соседние дубликаты. По умолчанию при сравнении используется оператор равенства, определенный для типа элементов контейнера. Например, если даны значения {0,2,4,6,4,2,0}, то после применения unique()
список останется таким же, поскольку в соседних позициях дубликатов нет. Но если мы сначала отсортируем список, что даст {0,0,2,2,4,4,6}, а потом применим unique(), то получим четыре различных значения {0,2,4,6}.
ilist.unique();
Вторая форма unique()
принимает альтернативный оператор сравнения. Например,
|
class EvenPair { public: bool operator()( int val1, val2 ) { return ! (val2 % val1 ); } }; |
ilist.unique( EvenPair() );
удаляет соседние элементы, если второй элемент без остатка делится на первый.
Эти операции, являющиеся членами класса, следует предпочесть соответствующим обобщенным алгоритмам при работе со списками. Остальные обобщенные алгоритмы, такие, как find(), transform(), for_each() и т.д., работают со списками так же эффективно, как и с другими контейнерами (еще раз напомним, что подробно все алгоритмы рассматриваются в Приложении).
Упражнение 12.8
Измените программу из раздела 12.2, используя список вместо вектора.
Часть IV
Объектное программирование
В части 4 мы сосредоточимся на объектном программировании, т.е. на применении классов C++ для определения новых типов, манипулировать которыми так же просто, как и встроенными. Создавая новые типы для описания предметной области, C++ помогает программисту писать более легкие для понимания приложения. Классы позволяют отделить детали, касающиеся реализации нового типа, от определения интерфейса и операций, предоставляемых пользователю. При этом уделяется меньше внимания мелочам, из-за чего программирование становится таким утомительным занятием. Значимые для приложения типы можно реализовать всего один раз, после чего использовать повторно. Средства, обеспечивающие инкапсуляцию данных и функций, необходимых для реализации типа, помогают значительно упростить последующее сопровождение и развитие приложения.
Присваивание одного объекта другому объекту того же класса выполняется с помощью копирующего оператора присваивания. (Этот специальный случай был рассмотрен в разделе 14.7.)
Для класса могут быть определены и другие операторы присваивания. Если объектам класса надо присваивать значения типа, отличного от этого класса, то разрешается определить такие операторы, принимающие подобные параметры. Например, чтобы поддержать присваивание C-строки объекту String:
|
String car ("Volks"); |
car = "Studebaker";
мы предоставляем оператор, принимающий параметр типа const char*. Эта операция уже была объявлена в нашем классе:
|
class String { public: // оператор присваивания для char* String& operator=( const char * ); // ... private: int _size; char *string; |
};
Такой оператор реализуется следующим образом. Если объекту String присваивается нулевой указатель, он становится “пустым”. В противном случае ему присваивается копия C-строки:
|
String& String::operator=( const char *sobj ) { // sobj - нулевой указатель if (! sobj ) { _size = 0; delete[] _string; _string = 0; } else { _size = strlen( sobj ); delete[] _string; _string = new char[ _size + 1 ]; strcpy( _string, sobj ); } return *this; |
}
_string
ссылается на копию той C-строки, на которую указывает sobj. Почему на копию? Потому что непосредственно присвоить sobj
члену _string
нельзя:
_string = sobj; // ошибка: несоответствие типов
sobj – это указатель на const и, следовательно, не может быть присвоен указателю на “не-const” (см. раздел 3.5). Изменим определение оператора присваивания:
String& String::operator=( const *sobj ) { // ... }
Теперь _string
прямо ссылается на C-строку, адресованную sobj. Однако при этом возникают другие проблемы. Напомним, что C-строка имеет тип const char*. Определение параметра как указателя на не-const делает присваивание невозможным:
car = "Studebaker"; // недопустимо с помощью operator=( char
char ia[] = { 'd', 'a', 'n', 'c', 'e', 'r' }; String trap = ia; // trap._string ссылается на ia |
// сцепление операторов присваивания int iobj, jobj; |
String ver, noun; |
// набор перегруженных операторов присваивания String& operator=( const String & ); |
Оператор dynamic_cast
можно применять для преобразования указателя, ссылающегося на объект типа класса в указатель на тип класса из той же иерархии. Его также используют для трансформации l-значения объекта типа класса в ссылку на тип класса из той же иерархии. Приведение типов с помощью оператора dynamic_cast, в отличие от других имеющихся в C++ способов, осуществляется во время выполнения программы. Если указатель или l-значение не могут быть преобразованы в целевой тип, то dynamic_cast
завершается неудачно. В случае приведения типа указателя признаком неудачи служит возврат нулевого значения. Если же l-значение нельзя трансформировать в ссылочный тип, возбуждается исключение. Ниже мы приведем примеры неудачного выполнения
этого оператора.
Прежде чем перейти к более детальному рассмотрению dynamic_cast, посмотрим, зачем его нужно применять. Предположим, что в программе используется библиотека классов для представления различных категорий служащих компании. Входящие в иерархию классы поддерживают функции-члены для вычисления зарплаты:
|
class employee { public: virtual int salary(); }; class manager : public employee { public: int salary(); }; class programmer : public employee { public: int salary(); }; void company::payroll( employee *pe ) { // используется pe->salary() |
}
В компании есть разные категории служащих. Параметром функции-члена payroll()
класса company
является указатель на объект employee, который может адресовать один из типов manager или programmer. Поскольку payroll()
обращается к виртуальной функции-члену salary(), то вызывается подходящая замещающая функция, определенная в классе manager или programmer, в зависимости от того, какой объект адресован указателем.
Допустим, класс employee
перестал удовлетворять нашим потребностям, и мы хотим его модифицировать, добавив еще одну функцию-член bonus(), используемую совместно с salary() при расчете платежной ведомости. Для этого нужно включить новую функцию-член в классы, составляющие иерархию employee:
class employee { public: virtual int salary(); // çàðïëàòà virtual int bonus(); // ïðåìèÿ }; class manager : public employee { public: int salary(); }; class programmer : public employee { public: int salary(); int bonus(); }; void company::payroll( employee *pe ) { // èñïîëüçóåòñÿ pe->salary() è pe->bonus() |
class employee { public: virtual int salary(); }; class manager : public employee { public: int salary(); }; class programmer : public employee { public: int salary(); int bonus(); |
void company::payroll( employee *pe ) { programmer *pm = dynamic_cast< programmer* >( pe ); // åñëè pe óêàçûâàåò íà îáúåêò òèïà programmer, // òî dynamic_cast âûïîëíèòñÿ óñïåøíî è pm áóäåò // óêàçûâàòü íà íà÷àëî îáúåêòà programmer if ( pm ) { // èñïîëüçîâàòü pm äëÿ âûçîâà programmer::bonus() } // åñëè pe íå óêàçûâàåò íà îáúåêò òèïà programmer, // òî dynamic_cast âûïîëíèòñÿ íåóäà÷íî // è pm áóäåò ñîäåðæàòü 0 else { // èñïîëüçîâàòü ôóíêöèè-÷ëåíû êëàññà employee } |
void company::payroll( employee *pe ) { programmer *pm = dynamic_cast< programmer* >( pe ); // ïîòåíöèàëüíàÿ îøèáêà: pm èñïîëüçóåòñÿ áåç ïðîâåðêè çíà÷åíèÿ static int variablePay = 0; variablePay += pm->bonus(); // ... |
void company::payroll( employee *pe ) { // âûïîëíèòü dynamic_cast è ïðîâåðèòü ðåçóëüòàò if ( programmer *pm = dynamic_cast< programmer* >( pe ) ) { // èñïîëüçîâàòü pm äëÿ âûçîâà programmer::bonus() } else { // èñïîëüçîâàòü ôóíêöèè-÷ëåíû êëàññà employee } |
#include <typeinfo> void company::payroll( employee &re ) { try { programmer &rm = dynamic_cast< programmer & >( re ); // èñïîëüçîâàòü rm äëÿ âûçîâà programmer::bonus() } catch ( std::bad_cast ) { // èñïîëüçîâàòü ôóíêöèè-÷ëåíû êëàññà employee } |
Существует третья форма оператора new, которая создает объект без отведения для него памяти, то есть в памяти, которая уже была выделена. Эту форму называют оператором размещения new. Программист указывает адрес области памяти, в которой размещается объект:
new (place_address) type-specifier
place_address
должен быть указателем. Такая форма (она включается заголовочным файлом <new>) позволяет программисту предварительно выделить большую область памяти, которая впоследствии будет содержать различные объекты. Например:
|
#include <iostream> #include <new> const int chunk = 16; class Foo { public: int val() { return _val; } FooQ(){ _val = 0; } private: int _val; }; // выделяем память, но не создаем объектов Foo char *buf = new char[ sizeof(Foo) * chunk ]; int main() { // создаем объект Foo в buf Foo *pb = new (buf) Foo; // проверим, что объект помещен в buf if ( pb.val() == 0 ) cout << "Оператор new сработал!" << endl; // здесь нельзя использовать pb delete[] buf; return 0; |
}
Результат работы программы:
Оператор new сработал!
Для оператора размещения new нет парного оператора delete: он не нужен, поскольку эта форма не выделяет память. В предыдущем примере необходимо освободить память, адресуемую указателем buf, а не pb. Это происходит в конце программы, когда буфер больше не нужен. Поскольку buf
ссылается на символьный массив, оператор delete имеет форму
delete[] buf;
При уничтожении buf
прекращают существование все объекты, созданные в нем. В нашем примере pb
больше не ссылается на существующий объект класса Foo.
Упражнение 8.5
Объясните, почему приведенные операторы new
ошибочны:
|
(a) const float *pf = new const float[100]; (b) double *pd = new doub1e[10] [getDim()]; (c) int (*pia2)[ 1024 ] = new int[ ][ 1024 ]; |
(d) const int *pci = new const int;
Упражнение 8.6
Как бы вы уничтожили pa?
|
typedef int arr[10]; |
int globalObj; char buf[1000]; void f() { int *pi = &global0bj; double *pd = 0; float *pf = new float(O); int *pa = new(buf)int[20]; delete pi; // (a) delete pd; // (b) delete pf; // (c) de1ete[] pa; // (d) |
int ix = 1024; int *pi = & ix; int *pi2 = new int ( 2048 ); (a) auto_ptr<int> p0(ix); (b) auto_ptr<int> pl(pi); (c) auto_ptr<int> p2(pi2); (d) auto_ptr<int> p3(&ix); (e) auto_ptr<int> p4(new int(2048)); (f) auto_ptr<int> p5(p2.get()); (9) auto_ptr<int> p6(p2.release()); |
int *pi0 = p2.get(); |
ps.get()->assign( "Danny" ); |
Оператор-член new()
может быть перегружен при условии, что все объявления имеют разные списки параметров. Первый параметр должен иметь тип size_t:
|
class Screen { public: void *operator new( size_t ); void *operator new( size_t, Screen * ); // ... |
};
Остальные параметры инициализируются аргументами размещения, заданными при вызове new:
|
void func( Screen *start ) { Screen *ps = new (start) Screen; // ... |
}
Та часть выражения, которая находится после ключевого слова new и заключена в круглые скобки, представляет аргументы размещения. В примере выше вызывается оператор new(), принимающий два параметра. Первый автоматически инициализируется значением, равным размеру класса Screen в байтах, а второй– значением аргумента размещения start.
Можно также перегружать и оператор-член delete(). Однако такой оператор никогда не вызывается из выражения delete. Перегруженный delete()
неявно вызывается компилятором, если конструктор, вызванный при выполнении оператора new
(это не опечатка, мы действительно имеем в виду new), возбуждает исключение. Рассмотрим использование delete()
более внимательно.
Последовательность действий при вычислении выражения
Screen *ps = new ( start ) Screen;
такова:
1. Вызывается определенный в классе оператор new(size_t, Screen*).
2. Вызывается конструктор по умолчанию класса Screen для инициализации созданного объекта.
Переменная ps
инициализируется адресом нового объекта Screen.
Предположим, что оператор класса new(size_t, Screen*) выделяет память с помощью глобального new(). Как разработчик может гарантировать, что память будет освобождена, если вызванный на шаге 2 конструктор возбуждает исключение? Чтобы защитить пользовательский код от утечки памяти, следует предоставить перегруженный оператор delete(), который вызывается только в подобной ситуации.
Если в классе имеется перегруженный оператор с параметрами, типы которых соответствуют типам параметров new(), то компилятор автоматически вызывает его для освобождения памяти. Предположим, есть следующее выражение с оператором размещения new:
Имя члена пользовательского пространства дополняется поставленным спереди именем этого пространства и оператором разрешения области видимости (::). Использование неквалифицированного члена, например matrix, является ошибкой. Компилятор не знает, к какому объявлению относится это имя:
|
// определение интерфейса библиотеки #include "primer.h" // ошибка: нет объявления для matrix |
void func( matrix &m );
Объявление члена пространства имен скрыто в своем пространстве. Если мы не укажем компилятору, где именно искать объявление, он произведет поиск только в текущей области видимости и в областях, включающих текущую. Допустим, если переписать предыдущую программу так:
|
// определение интерфейса библиотеки #include "primer.h" class matrix { /* пользовательское определение */ }; // правильно: глобальный тип matrix найден |
void func( matrix &m );
то определение класса matrix
компилятор находит в глобальной области видимости и программа компилируется без ошибок. Поскольку объявление matrix как члена пространства имен cplusplus_primer
скрыто в этом пространстве, оно не конфликтует с классом, объявленным в глобальной области видимости.
Именно поэтому мы говорим, что пространства имен решают проблему засорения глобального пространства: имена их членов невидимы, если имя пространства не указано явно, с помощью оператора разрешения области видимости. Существуют и другие механизмы, позволяющие сделать объявление члена пространства имен видимым вне его. Это using-объявления и using-директивы. Мы рассмотрим их в следующем разделе.
Отметим, что оператор области видимости может быть использован и для того, чтобы сослаться на элемент глобального пространства имен. Поскольку это пространство не имеет имени, запись
::member_name
относится к его элементу. Такой способ полезен для указания членов глобального пространства, если их имена оказываются скрыты именами, объявленными во вложенных локальных областях видимости.
#include <iostream> const int max = 65000; const int lineLength = 12; void fibonacci( int max ) { if ( max < 2 ) return; cout << "0 1 "; int v1 = 0, v2 = 1, cur; for ( int ix = 3; ix <= max; ++ix ) { cur = v1 + v2; if ( cur > ::max ) break; cout << cur << " "; vl = v2; v2 = cur; if (ix % "lineLength == 0) cout << end"!; } |
#include <iostream> void fibonacci( int ); int main() { cout << "Числа Фибоначчи: 16\n"; fibonacci( 16 ); return 0; |
Оператор sizeof
возвращает размер в байтах объекта или типа данных. Синтаксис его таков:
|
sizeof ( type name ); sizeof ( object ); |
sizeof object;
Результат имеет специальный тип size_t, который определен как typedef в заголовочном файле cstddef. Вот пример использования обеих форм оператора sizeof:
|
#include <cstddef> int ia[] = { 0, 1, 2 }; // sizeof возвращает размер всего массива size_t array_size = sizeof ia; // sizeof возвращает размер типа int |
size_t element_size = array_size / sizeof( int );
Применение sizeof к массиву дает количество байтов, занимаемых массивом, а не количество его элементов и не размер в байтах каждого из них. Так, например, в системах, где int
хранится в 4 байтах, значением array_size будет 12. Применение sizeof к указателю дает размер самого указателя, а не объекта, на который он указывает:
|
int *pi = new int[ 3 ]; |
size_t pointer_size = sizeof ( pi );
Здесь значением pointer_size
будет память под указатель в байтах (4 в 32-битных системах), а не массива ia.
Вот пример программы, использующей оператор sizeof:
|
#include <string> #include <iostream> #include <cstddef> int main() { size_t ia; ia = sizeof( ia ); // правильно ia = sizeof ia; // правильно // ia = sizeof int; // ошибка ia = sizeof( int ); // правильно int *pi = new int[ 12 ]; cout << "pi: " << sizeof( pi ) << " *pi: " << sizeof( pi ) << endl; // sizeof строки не зависит от // ее реальной длины string stl( "foobar" ); string st2( "a mighty oak" ); string *ps = &stl; cout << " st1: " << sizeof( st1 ) << " st2: " << sizeof( st2 ) << " ps: sizeof( ps ) << " *ps: " << sizeof( *ps ) << endl; cout << "short :\t" << sizeof(short) << endl; cout << "shorf" :\t" << sizeof(short*) << endl; cout << "short& :\t" << sizeof(short&) << endl; cout << "short[3] :\t" << sizeof(short[3]) << endl; |
// char_size == 1 |
// правильно: константное выражение |
Оператор “стрелка”, разрешающий доступ к членам, может перегружаться для объектов класса. Он должен быть определен как функция-член и обеспечивать семантику указателя. Чаще всего этот оператор используется в классах, которые предоставляют “интеллектуальный указатель” (smart pointer), ведущий себя аналогично встроенным, но поддерживают и некоторую дополнительную функциональность.
Допустим, мы хотим определить тип класса для представления указателя на объект Screen (см. главу 13):
|
class ScreenPtr { // ... private: Screen *ptr; |
};
Определение ScreenPtr
должно быть таким, чтобы объект этого класса гарантировано указывал на объект Screen: в отличие от встроенного указателя, он не может быть нулевым. Тогда приложение сможет пользоваться объектами типа ScreenPtr, не проверяя, указывают ли они на какой-нибудь объект Screen. Для этого нужно определить класс ScreenPtr с конструктором, но без конструктора по умолчанию (детально конструкторы рассматривались в разделе 14.2):
|
class ScreenPtr { public: ScreenPtr( const Screen &s ) : ptr( &s ) { } // ... |
};
В любом определении объекта класса ScreenPtr
должен присутствовать инициализатор– объект класса Screen, на который будет ссылаться объект ScreenPtr:
|
ScreenPtr p1; // ошибка: у класса ScreenPtr нет конструктора по умолчанию Screen myScreen( 4, 4 ); |
ScreenPtr ps( myScreen ); // правильно
Чтобы класс ScreenPtr вел себя как встроенный указатель, необходимо определить некоторые перегруженные операторы – разыменования (*) и “стрелку” для доступа к членам:
|
// перегруженные операторы для поддержки поведения указателя class ScreenPtr { public: Screen& operator*() { return *ptr; } Screen* operator->() { return ptr; } // ... |
};
Оператор доступа к членам унарный, поэтому параметры ему не передаются. При использовании в составе выражения его результат зависит только от типа левого операнда. Например, в инструкции
point->action();
исследуется тип point. Если это указатель на некоторый тип класса, то применяется семантика встроенного оператора доступа к члену. Если же это объект или ссылка на объект, то проверяется, есть ли в этом классе перегруженный оператор доступа. Когда перегруженный оператор “стрелка” определен, он вызывается для объекта point, иначе инструкция неверна, поскольку для обращения к членам самого объекта (в том числе по ссылке) следует использовать оператор “точка”.
#include <iostream> #include <string> #include "Screen.h" void printScreen( const ScreenPtr &ps ) { cout << "Screen Object ( " << ps->height() << ", " << ps->width() << " )\n\n"; for ( int ix = 1; ix <= ps->height(); ++ix ) { for ( int iy = 1; iy <= ps->width(); ++iy ) cout << ps->get( ix, iy ); cout << "\n"; } } int main() { Screen sobj( 2, 5 ); string init( "HelloWorld" ); ScreenPtr ps( sobj ); // Установить содержимое экрана string::size_type initpos = 0; for ( int ix = 1; ix <= ps->height(); ++ix ) for ( int iy = 1; iy <= ps->width(); ++iy ) { ps->move( ix, iy ); ps->set( init[ initpos++ ] ); } // Вывести содержимое экрана printScreen( ps ); return 0; |
Второй оператор, входящий в состав RTTI, – это typeid, который позволяет выяснить фактический тип выражения. Если оно принадлежит типу класса и этот класс содержит хотя бы одну виртуальную функцию-член, то ответ может и не совпадать с типом самого выражения. Так, если выражение является ссылкой на базовый класс, то typeid
сообщает тип производного класса объекта:
|
#include <typeinfo> programmer pobj; employee &re = pobj; // ñ ôóíêöèåé name() ìû ïîçíàêîìèìñÿ â ïîäðàçäåëå, ïîñâÿùåííîì type_info // îíà âîçâðàùàåò C-ñòðîêó "programmer" |
coiut << typeid( re ).name() << endl;
Операнд re
оператора typeid
имеет тип employee. Но так как re – это ссылка на тип класса с виртуальными функциями, то typeid
говорит, что тип адресуемого объекта – programmer (а не employee, на который ссылается re). Программа, использующая такой оператор, должна включать заголовочный файл <typeinfo>, что мы и сделали в этом примере.
Где применяется typeid? В сложных системах разработки, например при построении отладчиков, а также при использовании устойчивых объектов, извлеченных из базы данных. В таких системах необходимо знать фактический тип объекта, которым программа манипулирует с помощью указателя или ссылки на базовый класс, например для получения списка его свойств во время сеанса работы с отладчиком или для правильного сохранения или извлечения объекта из базы данных. Оператор typeid
допустимо использовать с выражениями и именами любых типов. Например, его операндами могут быть выражения встроенных типов и константы. Если операнд не принадлежит к типу класса, то typeid просто возвращает его тип:
int iobj; cout << typeid( iobj ).name() << endl; // ïå÷àòàåòñÿ: int |
class Base { /* нет виртуальных функций */ }; class Derived : public Base { /* íåò âèðòóàëüíûõ ôóíêöèé */ }; Derived dobj; Base *pb = &dobj; |
#include <typeinfo> employee *pe = new manager; employee& re = *pe; if ( typeid( pe ) == typeid( employee* ) ) // èñòèííî // ÷òî-òî ñäåëàòü /* if ( typeid( pe ) == typeid( manager* ) ) // ëîæíî if ( typeid( pe ) == typeid( employee ) ) // ëîæíî if ( typeid( pe ) == typeid( manager ) ) // ëîæíî |
// вызов виртуальной функции |
typeid( *pe ) == typeid( manager ) // истинно |
typeid( re ) == typeid( manager ) // истинно typeid( re ) == typeid( employee ) // ложно typeid( &re ) == typeid( employee* ) // истинно |
Оператор вывода обычно применяется для записи на стандартный вывод cout. Например, программа
|
#include <iostream> int main() { cout << "сплетница Анна Ливия\n"; |
}
печатает на терминале строку:
сплетница Анна Ливия
Имеются операторы, принимающие аргументы любого встроенного типа данных, включая const char*, а также типов string и complex из стандартной библиотеки. Любое выражение, включая вызов функции, может быть аргументом оператора вывода при условии, что результатом его вычисления будет тип, принимаемый каким-либо вариантом этого оператора. Например, программа
|
#include <iostream> #include <string.h> int main() { cout << "Длина 'Улисс' равна:\t"; cout << strlen( "Улисс" ); cout << '\n'; cout << "Размер 'Улисс' равен:\t"; cout << sizeof( "Улисс" ); cout << endl; |
}
выводит на терминал следующее:
Длина 'Улисс' равна:7
Размер 'Улисс' равен:8
endl – это манипулятор вывода, который вставляет в выходной поток символ перехода на новую строку, а затем сбрасывает буфер объекта ostream. (С буферизацией мы познакомимся в разделе 20.9.)
Операторы вывода, как правило, удобнее сцеплять в одну инструкцию. Например, предыдущую программу можно записать таким образом:
|
#include <iostream> #include <string.h> int main() { // операторы вывода можно сцеплять cout << "Длина 'Улисс' равна:\t"; << strlen( "Улисс" ) << '\n'; cout << "Размер 'Улисс' равен:\t" << sizeof( "Улисс" ) << endl; |
}
Сцепление операторов вывода (и ввода тоже) возможно потому, что результатом выражения
cout << "некоторая строка";
служит левый операнд оператора вывода, т.е. сам объект cout. Затем этот же объект передается следующему оператору и далее по цепочке (мы говорим, что оператор << левоассоциативен).
#include <iostream> int main() { int i = 1024; int *pi = &i; cout << "i: " << i << "\t&i:\t" << &i << '\n'; cout << "*pi: " << *pi << "\tpi:\t" << pi << endl << "\t\t&pi:\t" << &pi << endl; |
#include <iostream> const char *str = "vermeer"; int main() { const char *pstr = str; cout << "Адрес pstr равен: " << pstr << endl; |
#include <iostream> inline void max_out( int val1, int val2 ) { cout << ( val1 > val2 ) ? val1 : val2; } int main() { int ix = 10, jx = 20; cout << "Большее из " << ix << ", " << jx << " равно "; max_out( ix, jx ); cout << endl; |
int main() { cout << "печать значений типа bool по умолчанию: " << true << " " << false << "\nи в виде строк: " << boolalpha() << true << " " << false << endl; |
#include <iostream> #include <vector> #include <string> string pooh_pals[] = { "Тигра", "Пятачок", "Иа-Иа", "Кролик" }; int main() { vector<string> ppals( pooh_pals, pooh_pals+4 ); vector<string>::iterator iter = ppals.begin(); vector<string>::iterator iter_end = ppals.end(); cout << "Это друзья Пуха: "; for ( ; iter != iter_end; iter++ ) cout << *iter << " "; cout << endl; |
#include <iostream> #include <algorithm> #include <vector> #include <string> string pooh_pals[] = { "Тигра", "Пятачок", "Иа-Иа", "Кролик" }; int main() { vector<string> ppals( pooh_pals, pooh_pals+4 ); vector<string>::iterator iter = ppals.begin(); vector<string>::iterator iter_end = ppals.end(); cout << "Это друзья Пуха: "; // копируем каждый элемент в cout ... ostream_iterator< string > output( cout, " " ); copy( iter, iter_end, output ); cout << endl; |
string sa[4] = { "пух", "тигра", "пятачок", "иа-иа" }; vector< string > svec( sa, sa+4 ); string robin( "кристофер робин" ); const char *pc = robin.c_str(); int ival = 1024; char blank = ' '; double dval = 3.14159; |
Оператор вызова функции может быть перегружен для объектов типа класса. (Мы уже видели, как он используется, при рассмотрении объектов-функций в разделе 12.3.) Если определен класс, представляющий некоторую операцию, то для ее вызова перегружается соответствующий оператор. Например, для взятия абсолютного значения числа типа int
можно определить класс absInt:
|
class absInt { public: int operator()( int val ) { int result = val < 0 ? -val : val; return result; } |
};
Перегруженный оператор operator()
должен быть объявлен как функция-член с произвольным числом параметров. Параметры и возвращаемое значение могут иметь любые типы, допустимые для функций (см. разделы 7.2, 7.3 и 7.4). operator() вызывается путем применения списка аргументов к объекту того класса, в котором он определен. Мы рассмотрим, как он используется в одном из обобщенных алгоритмов, описанных в главе 12. В следующем примере обобщенный алгоритм transform() вызывается для применения определенной в absInt
операции к каждому элементу вектора ivec, т.е. для замены элемента его абсолютным значением.
|
#include <vector> #include <algoritm> int main() { int ia[] = { -0, 1, -1, -2, 3, 5, -5, 8 }; vector< int > ivec( ia, ia+8 ); // заменить каждый элемент его абсолютным значением transform( ivec.begin(), ivec.end(), ivec.begin(), absInt() ); // ... |
}
Первый и второй аргументы transform()
ограничивают диапазон элементов, к которым применяется операция absInt. Третий указывает на начало вектора, где будет сохранен результат применения операции.
Четвертый аргумент – это временный объект класса absInt, создаваемый с помощью конструктора по умолчанию. Конкретизация обобщенного алгоритма transform(), вызываемого из main(), могла бы выглядеть так:
|
typedef vector< int >::iterator iter_type; // конкретизация transform() // операция absInt применяется к элементу вектора int iter_type transform( iter_type iter, iter_type last, iter_type result, absInt func ) { while ( iter != last ) *result++ = func( *iter++ ); // вызывается absInt::operator() return iter; |
}
func – это объект класса, который предоставляет операцию absInt, заменяющую число типа int его абсолютным значением. Он используется для вызова перегруженного оператора operator()
класса absInt. Этому оператору передается аргумент *iter, указывающий на тот элемент вектора, для которого мы хотим получить абсолютное значение.
Оператор взятия индекса operator[]()
можно определять для классов, представляющих абстракцию контейнера, из которого извлекаются отдельные элементы. Примерами таких контейнеров могут служить наш класс String, класс IntArray, представленный в главе 2, или шаблон класса vector, определенный в стандартной библиотеке C++. Оператор взятия индекса обязан быть функцией-членом класса.
У пользователей String
должна иметься возможность чтения и записи отдельных символов члена _string. Мы хотим поддержать следующий способ применения объектов данного класса:
|
String entry( "extravagant" ); String mycopy; for ( int ix = 0; ix < entry.size(); ++ix ) |
mycopy[ ix ] = entry[ ix ];
Оператор взятия индекса может появляться как слева, так и справа от оператора присваивания. Чтобы быть в левой части, он должен возвращать l-значение индексируемого элемента. Для этого мы возвращаем ссылку:
|
#include <cassert> inine char& String::operator[]( int elem ) const { assert( elem >= 0 && elem < _size ); return _string[ elem ]; |
}
В следующем фрагменте нулевому элементу массива color присваивается символ 'V':
|
String color( "violet" ); |
color[ 0 ] = 'V';
Обратите внимание, что в определении оператора проверяется выход индекса за границы массива. Для этого используется библиотечная C-функция assert(). Можно также возбудить исключение, показывающее, что значение elem меньше 0 или больше длины C-строки, на которую ссылается _string. (Возбуждение и обработка исключений обсуждались в главе 11.)
Одно выражение может состоять из набора подвыражений, разделенных запятыми; такие подвыражения вычисляются слева направо. Конечным результатом будет результат самого правого из них. В следующем примере каждое из подвыражений условного оператора представляет собой список. Результатом первого подвыражения условного оператора является ix, второго– 0.
|
int main() { // примеры оператора "запятая" // переменные ia, sz и index определены в другом месте ... int ival = (ia != 0) ? ix=get_va1ue(), ia[index]=ix : ia=new int[sz], ia[index]=0; // ... |
}
Операторная функция является кандидатом, если она имеет то же имя, что и вызванная. При использовании следующего оператора сложения
|
SmallInt si(98); int iobj = 65; |
int res = si + iobj;
операторной функцией-кандидатом является operator+. Какие объявления operator+
принимаются во внимание?
Потенциально в случае применения операторного синтаксиса с операндами, имеющими тип класса, строится пять множеств кандидатов. Первые три– те же, что и при вызове обычных функций с аргументами типа класса:
· множество операторов, видимых в точке вызова. Объявления функции operator+(), видимые в точке использования оператора, являются кандидатами. Например, operator+(), объявленный в глобальной области
видимости, – кандидат в случае применения operator+() внутри main():
|
SmallInt operator+ ( const SmallInt &, const SmallInt & ); int main() { SmallInt si(98); int iobj = 65; int res = si + iobj; // ::operator+() - функция-кандидат |
}
· множество операторов, объявленных в пространстве имен, в котором определен тип операнда. Если операнд имеет тип класса и этот тип объявлен в пользовательском пространстве имен, то операторные функции, объявленные в том же пространстве и имеющие то же имя, что и использованный оператор, считаются кандидатами:
|
namespace NS { class SmallInt { /* ... */ }; SmallInt operator+ ( const SmallInt&, double ); } int main() { // si имеет тип SmallInt: // этот класс объявлен в пространстве имен NS NS::SmallInt si(15); // NS::operator+() - функция-кандидат int res = si + 566; return 0; |
}
Операнд si
имеет тип класса SmallInt, объявленного в пространстве имен NS. Поэтому перегруженный operator+(const SmallInt, double), объявленный в том же пространстве, добавляется к множеству кандидатов;
namespace NS { class SmallInt { friend SmallInt operator+( const SmallInt&, int ) { /* ... */ } }; } int main() { NS::SmallInt si(15); // функция-друг operator+() - кандидат int res = si + 566; return 0; |
class myFloat { myFloat( double ); }; class SmallInt { public: SmallInt( int ); SmallInt operator+ ( const myFloat & ); }; int main() { SmallInt si(15); int res = si + 5.66; // оператор-член operator+() - кандидат |
int operator+( int, int ); double operator+( double, double ); T* operator+( T*, I ); |
namespace NS { class myFloat { myFloat( double ); }; class SmallInt { friend SmallInt operator+( const SmallInt &, int ) { /* ... */ } public: SmallInt( int ); operator int(); SmallInt operator+ ( const myFloat & ); // ... }; SmallInt operator+ ( const SmallInt &, double ); } int main() { // тип si - class SmallInt: // Этот класс объявлен в пространстве имен NS NS::SmallInt si(15); int res = si + 5.66; // какой operator+()? return 0; |
int operator+( int, int ); double operator+( double, double ); T* operator+( T*, I ); |
Продолжая развивать реализацию класса ScreenPtr, введенного в предыдущем разделе, рассмотрим еще два оператора, которые поддерживаются для встроенных указателей и которые желательно иметь и для нашего интеллектуального указателя: инкремент (++) и декремент (--).
Чтобы использовать класс ScreenPtr для ссылки на элементы массива объектов Screen, туда придется добавить несколько дополнительных членов.
Сначала мы определим новый член size, который содержит либо нуль (это говорит о том, что объект ScreenPtr
указывает на единственный объект), либо размер массива, адресуемого объектом ScreenPtr. Нам также понадобится член offset, запоминающий смещение от начала данного массива:
|
class ScreenPtr { public: // ... private: int size; // размер массива: 0, если единственный объект int offset; // смещение ptr от начала массива Screen *ptr; |
};
Модифицируем конструктор класса ScreenPtr с учетом его новой функциональности и дополнительных членов,. Пользователь нашего класса должен передать конструктору дополнительный аргумент, если создаваемый объект указывает на массив:
|
class ScreenPtr { public: ScreenPtr( Screen &s , int arraySize = 0 ) : ptr( &s ), size ( arraySize ), offset( 0 ) { } private: int size; int offset; Screen *ptr; |
};
С помощью этого аргумента задается размер массива. Чтобы сохранить прежнюю функциональность, предусмотрим для него значение по умолчанию, равное нулю. Таким образом, если второй аргумент конструктора опущен, то член size окажется равен 0 и, следовательно, такой объект будет указывать на единственный объект Screen. Объекты нового класса ScreenPtr
можно определять следующим образом:
|
Screen myScreen( 4, 4 ); ScreenPtr pobj( myScreen ); // правильно: указывает на один объект const int arrSize = 10; Screen *parray = new Screen[ arrSize ]; |
ScreenPtr parr( *parray, arrSize ); // правильно: указывает на массив
Теперь мы готовы определить в ScreenPtr
class ScreenPtr { public: Screen& operator++(); Screen& operator--(); // ... |
const int arrSize = 10; Screen *parray = new Screen[ arrSize ]; ScreenPtr parr( *parray, arrSize ); for ( int ix = 0; ix < arrSize; ++ix, ++parr ) // эквивалентно parr.operator++() } |
Screen& ScreenPtr::operator++() { if ( size == 0 ) { cerr << "не могу инкрементировать указатель для одного объекта\n"; return *ptr; } if ( offset >= size - 1 ) { cerr << "уже в конце массива\n"; return *ptr; } ++offset; return *++ptr; } Screen& ScreenPtr::operator--() { if ( size == 0 ) { cerr << "не могу декрементировать указатель для одного объекта\n"; return *ptr; } if ( offset <= 0 ) { cerr << "уже в начале массива\n"; return *ptr; } --offset; return *--ptr; |
class ScreenPtr { public: Screen& operator++(); // префиксные операторы Screen& operator--(); Screen& operator++(int); // постфиксные операторы Screen& operator--(int); // ... |
Screen& ScreenPtr::operator++(int) { if ( size == 0 ) { cerr << "не могу инкрементировать указатель для одного объекта\n"; return *ptr; } if ( offset == size ) { cerr << "уже на один элемент дальше конца массива\n"; return *ptr; } ++offset; return *ptr++; } Screen& ScreenPtr::operator--(int) { if ( size == 0 ) { cerr << "не могу декрементировать указатель для одного объекта\n"; return *ptr; } if ( offset == -1 ) { cerr << "уже на один элемент раньше начала массива\n"; return *ptr; } --offset; return *ptr--; |
const int arrSize = 10; Screen *parray = new Screen[ arrSize ]; ScreenPtr parr( *parray, arrSize ); for ( int ix = 0; ix < arrSize; ++ix) |
class ScreenPtr { // объявления не членов friend Screen& operator++( Screen & ); // префиксные операторы friend Screen& operator--( Screen & ); friend Screen& operator++( Screen &, int); // постфиксные операторы friend Screen& operator--( Screen &, int); public: // определения членов |
Каждая программа во время работы получает определенное количество памяти, которую можно использовать. Такое выделение памяти под объекты во время выполнения называется динамическим, а сама память выделяется из хипа (heap). (Мы уже касались вопроса о динамическом выделении памяти в главе 1.) Напомним, что выделение памяти объекту производится с помощью оператора new, возвращающего указатель на вновь созданный объект того типа, который был ему задан. Например:
int *pi =
new int;
размещает объект типа int в памяти и инициализирует указатель pi адресом этого объекта. Сам объект в таком случае не инициализируется, но это легко изменить:
int *pi = new int( 1024 );
Можно динамически выделить память под массив:
int *pia = new int[ 10 ];
Такая инструкция размещает в памяти массив встроенного типа из десяти элементов типа int. Для подобного массива нельзя задать список начальных значений его элементов при динамическом размещении. (Однако если размещается массив объектов типа класса, то для каждого из элементов вызывается конструктор по умолчанию.) Например:
string *ps = new string;
размещает в памяти один объект типа string, инициализирует ps его адресом и вызывает конструктор по умолчанию для вновь созданного объекта типа string. Аналогично
string *psa = new string[10];
размещает в памяти массив из десяти элементов типа string, инициализирует psa его адресом и вызывает конструктор по умолчанию для каждого элемента массива.
Объекты, размещаемые в памяти с помощью оператора new, не имеют собственного имени. Вместо этого возвращается указатель на безымянный объект, и все действия с этим объектом производятся посредством косвенной адресации.
После использования объекта, созданного таким образом, мы должны явно освободить память, применив оператор delete к указателю на этот объект. (Попытка применить оператор delete к указателю, не содержащему адрес объекта, полученного описанным способом, вызовет ошибку времени выполнения.) Например:
delete pi;
освобождает память, на которую указывает объект типа int, на который указывает pi. Аналогично
delete ps;
освобождает память, на которую указывает объект класса string, адрес которого содержится в ps. Перед уничтожением этого объекта вызывается деструктор. Выражение
delete [] pia;
освобождает память, отведенную под массив pia. При выполнении такой операции необходимо придерживаться указанного синтаксиса.
(Об операциях new и delete мы еще поговорим в главе 8.)
Упражнение 4.11
Какие из следующих выражений ошибочны?
(a) vector<string> svec( 10 );
(b) vector<string> *pvecl = new vector<string>(10);
(c) vector<string> **pvec2 = new vector<string>[10];
(d) vector<string> *pvl = &svec;
(e) vector<string> *pv2 = pvecl;
(f) delete svec;
(g) delete pvecl;
(h) delete [] pvec2;
(i) delete pvl;
(j) delete pv2;
class Screen { public: void operator delete( void * ); |
class Screen { public: // заменяет // void operator delete( void * ); void operator delete( void *, size_t ); |
class Screen { public: void *operator new( size_t ); void operator delete( void *, size_t ); // ... private: Screen *next; static Screen *freeStore; static const int screenChunk; |
#include "Screen.h" #include <cstddef> // статические члены инициализируются // в исходных файлах программы, а не в заголовочных файлах Screen *Screen::freeStore = 0; const int Screen::screenChunk = 24; void *Screen::operator new( size_t size ) { Screen *p; if ( !freeStore ) { // связанный список пуст: получить новый блок // вызывается глобальный оператор new size_t chunk = screenChunk * size; freeStore = p = reinterpret_cast< Screen* >( new char[ chunk ] ); // включить полученный блок в список for ( ; p != &freeStore[ screenChunk - 1 ]; ++p ) p->next = p+1; p->next = 0; } p = freeStore; freeStore = freeStore->next; return p; |
void Screen::operator delete( void *p, size_t ) { // вставить "удаленный" объект назад, // в список свободных ( static_cast< Screen* >( p ) )->next = freeStore; freeStore = static_cast< Screen* >( p ); |
// Псевдокод на C++ ptr = Screen::operator new( sizeof( Screen ) ); |
// Псевдокод на C++ Screen::~Screen( ptr ); |
Оператор new(), определенный в предыдущем подразделе, вызывается только при выделении памяти для единичного объекта. Так, в данной инструкции вызывается new()
класса Screen:
|
// вызывается Screen::operator new() |
Screen *ps = new Screen( 24, 80 );
тогда как ниже вызывается глобальный оператор new[]() для выделения из хипа памяти под массив объектов типа Screen:
|
// вызывается Screen::operator new[]() |
Screen *psa = new Screen[10];
В классе можно объявить также операторы new[]() и delete[]() для работы с массивами.
Оператор-член new[]()
должен возвращать значение типа void* и принимать в качестве первого параметра значение типа size_t. Вот его объявление для Screen:
|
class Screen { public: void *operator new[]( size_t ); // ... |
};
Когда с помощью new
создается массив объектов типа класса, компилятор проверяет, определен ли в классе оператор new[](). Если да, то для выделения памяти под массив вызывается именно он, в противном случае – глобальный new[](). В следующей инструкции в хипе создается массив из десяти объектов Screen:
Screen *ps = new Screen[10];
В этом классе есть оператор new[](), поэтому он и вызывается для выделения памяти. Его параметр size_t
автоматически инициализируется значением, равным объему памяти в байтах, необходимому для размещения десяти объектов Screen.
Даже если в классе имеется оператор-член new[](), программист может вызвать для создания массива глобальный new[](), воспользовавшись оператором разрешения глобальной области видимости:
Screen *ps = ::new Screen[10];
Оператор delete(), являющийся членом класса, должен иметь тип void, а в качестве первого параметра принимать void*. Вот как выглядит его объявление для Screen:
|
class Screen { public: void operator delete[]( void * ); |
};
Чтобы удалить массив объектов класса, delete должен вызываться следующим образом:
delete[] ps;
Когда операндом delete
является указатель на объект типа класса, компилятор проверяет, определен ли в этом классе оператор delete[](). Если да, то для освобождения памяти вызывается именно он, в противном случае – его глобальная версия. Параметр типа void* автоматически инициализируется значением адреса начала области памяти, в которой размещен массив.
class Screen { public: // заменяет // void operator delete[]( void* ); void operator delete[]( void*, size_t ); |
Члены Query
представляют:
· множество операций, поддерживаемых всеми производными от него классами запросов. Сюда входят как виртуальные операции, переопределяемые в производных классах, так и невиртуальные, разделяемые всеми производными классами (мы приведем примеры тех и других);
· множество данных-членов, общих для всех производных классов. Если вынести такие члены в абстрактный базовый класс Query, мы сможем обращаться к ним вне зависимости от того, с объектом какого производного класса мы работаем.
Если имеется запрос вида:
fiery || untamed
то двумя основными операциями для него будут: нахождение строк текста, удовлетворяющих условиям запроса, и представление найденных строк пользователю. Назовем эти операции соответственно eval() и display().
Алгоритм работы eval()
свой для каждого производного класса, поэтому эту функцию следует объявить виртуальной в определении Query. Всякий производный класс должен предоставить собственную реализацию для нее. Сам же Query
лишь включает ее в свой открытый интерфейс.
Алгоритм работы функции display(), выводящей найденные строки текста, не зависит от типа производного класса. Нам необходимо лишь иметь доступ к представлению самого текста и списку строк, удовлетворяющих запросу. Вместо того чтобы дублировать реализацию алгоритма и необходимые для него данные в каждом производном классе, определим единственный наследуемый экземпляр в Query.
Такое проектное решение позволит нам вызывать любую операцию, не зная фактического типа объекта, которым мы манипулируем:
|
void doit( Query *pq ) { // виртуальный вызов pq->eval(); // статический вызов Query::display() pq->display(); |
}
Как следует представить найденные строки текста? Каждому упомянутому в запросе слову будет соответствовать вектор позиций, построенный во время поиска. Позиция – это пара (строка, колонка), в которой каждый член – это значение типа short int. Отображение слов на векторы позиций, построенное функцией build_text_map(), содержит такие векторы для каждого встречающегося в тексте слова, распознанного нашей системой. Ключами для этого отображения служат значения типа string, представляющие слова. Например, для текста
#include <vector> #include <set> #include <string> #include <utility> typedef pair< short, short > location; class Query { public: // конструкторы и деструктор обсуждаются в разделе 17.4 // копирующий конструктор и копирующий оператор присваивания // обсуждаются в разделе 17.6 // операции для поддержки открытого интерфейса virtual void eval() = 0; virtual void display () const; // функции доступа для чтения const set<short> *solution() const; const vector<location> *locations() const { return &_loc; } static const vector<string> *text_file() {return _text_file;} protected: set<short>* _vec2set( const vector<location>* ); static vector<string> *_text_file; set<short> *_solution; vector<location> _loc; }; inline const set<short> Query:: solution() { return _solution ? _solution : _solution = _vec2set( &_loc ); |
Мы видели, что определение члена пространства имен может появиться внутри определения самого пространства. Например, класс matrix и константа pi
появляются внутри вложенного пространства имен MatrixLib, а определения функций operator+() и inverse()
приводятся где-то в другом месте текста программы:
|
// ---- primer.h ---- namespace cplusplus_primer { // первое вложенное пространство имен: // матричная часть библиотеки namespace MatrixLib { class matrix { /* ... */ }; const double pi = 3.1416; matrix operators+ ( const matrix &ml, const matrix &m2 ); void inverse( matrix & ); // ... } |
}
Член пространства имен можно определить и вне соответствующего пространства. В таком случае имя члена должно быть квалифицировано именами пространств, к которым он принадлежит. Например, если определение функции operator+()
помещено в глобальную область видимости, то оно должно выглядеть следующим образом:
|
// ---- primer.C ---- #include "primer.h" // определение в глобальной области видимости cplusplus_primer::MatrixLib::matrix cplusplus_primer::MatrixLib::operator+ ( const matrix& ml, const matrix &m2 ) |
{ /* ... */ }
Имя operator+()
квалифицировано в данном случае именами пространств cplusplus_primer и MatrixLib.
Однако обратите внимание на тип matrix в списке параметров operator+(): употреблено неквалифицированное имя. Как такое может быть?
В определении функции operator+()
можно использовать неквалифицированные имена для членов своего пространства, поскольку определение принадлежит к его области видимости. При разрешении имен внутри функции operator+()
используется MatrixLib. Заметим, однако, что в типе возвращаемого значения все же нужно указывать квалифицированное имя, поскольку он расположен вне области видимости, заданной определением функции:
cplusplus_primer::MatrixLib::operator+
В определении operator+()
неквалифицированные имена могут встречаться в любом объявлении или выражении внутри списка параметров или тела функции. Например, локальное объявление внутри operator+()
// ---- primer.C ---- #include "primer.h" cplusplus_primer::MatrixLib::matrix cplusplus_primer::MatrixLib::operator+ ( const matrix &ml, const matrix &m2 ) { // объявление локальной переменной типа // cplusplus_primer::MatrixLib::matrix matrix res; // вычислим сумму двух объектов matrix return res; |
// ---- primer.C -- #include "primer.h" namespace cplusplus_primer { MatrixLib::matrix MatrixLib::operator+ ( const matrix &ml, const matrix &m2 ) { /* ... */ } |
namespace cplusplus_primer { namespace MatrixLib { class matrix { /*...*/ }; // следующее объявление не может быть пропущено matrix operator+ ( const matrix &ml, const matrix &m2 ); // ... } |
В этой главе мы построим иерархию классов для представления запроса пользователя. Сначала реализуем каждую операцию в виде отдельного класса:
|
NameQuery // Shakespeare NotQuery // ! Shakespeare OrQuery // Shakespeare || Marlowe |
AndQuery // William && Shakespeare
В каждом классе определим функцию-член eval(), которая выполняет соответствующую операцию. К примеру, для NameQuery она возвращает вектор позиций, содержащий координаты (номера строки и колонки) начала каждого вхождения слова (см. раздел 6.8); для OrQuery
строит объединение векторов позиций обоих своих операндов и т.д.
Таким образом, запрос
untamed || fiery
состоит из объекта класса OrQuery, который содержит два объекта NameQuery в качестве операндов. Для простых запросов этого достаточно, но при обработке составных запросов типа
Alice || Emma && Weeks
возникает проблема. Данный запрос состоит из двух подзапросов: объекта OrQuery, содержащего объекты NameQuery для представления слов Alice и Emma, и объекта AndQuery. Правым операндом AndQuery
является объект NameQuery для слова Weeks.
|
AndQuery OrQuery NameQuery ("Alice") NameQuery ("Emma") |
NameQuery ("Weeks")
Но левый операнд – это объект OrQuery, предшествующий оператору &&. На его месте мог бы быть объект NotQuery или другой объект AndQuery. Как же следует представить операнд, если он может принадлежать к типу любого из четырех классов? Эта проблема имеет две стороны:
· необходимо уметь объявлять тип операнда в классах OrQuery, AndQuery и NotQuery так, чтобы с его помощью можно было представить тип любого из четырех классов запросов;
· какое бы решение мы ни выбрали в предыдущем случае, мы должны иметь возможность вызывать соответствующий классу каждого операнда вариант функции-члена eval().
// не объектно-ориентированное решение union op_type { // объединение не может содержать объекты классов с // ассоциированными конструкторами NotQuery *nq; OrQuery *oq; AndQuery *aq; string *word; }; enum opTypes { Not_query=1, O_query, And_query, Name_query }; class AndQuery { public: // ... private: /* * opTypes хранит информацию о фактических типах операндов запроса * op_type - это сами операнды */ op_type _lop, _rop; opTypes _lop_type, _rop_type; |
class AndQuery { public: // ... private: void * _lop, _rop; opTypes _lop_type, _rop_type; |
void AndQuery:: eval() { // не объектно-ориентированный подход // ответственность за разрешение типа ложится на программиста // определить фактический тип левого операнда switch( _lop_type ) { case And_query: AndQuery *paq = static_cast<AndQuery*>(_lop); paq->eval(); break; case Or_query: OrQuery *pqq = static_cast<OrQuery*>(_lop); poq->eval(); break; case Not_query: NotQuery *pnotq = static_cast<NotQuery*>(_lop); pnotq->eval(); break; case Name_query: AndQuery *pnmq = static_cast<NameQuery*>(_lop); pnmq->eval(); break; } // то же для правого операнда |
// объектно-ориентированное решение // ответственность за разрешение типов перекладывается на компилятор // примечание: теперь _lop и _rop - объекты типа класса // их определения будут приведены ниже void AndQuery:: eval() { _lop->eval(); _rop->eval(); |
Определение класса состоит из двух частей: заголовка, включающего ключевое слово class, за которым следует имя класса, и тела, заключенного в фигурные скобки. После такого определения должны стоять точка с запятой или список объявлений:
|
class Screen { /* ... */ }; |
class Screen { /* ... */ } myScreen, yourScreen;
Внутри тела объявляются данные-члены и функции-члены и указываются уровни доступа к ним. Таким образом, тело класса определяет список его членов.
Каждое определение вводит новый тип данных. Даже если два класса имеют одинаковые списки членов, они все равно считаются разными типами:
|
class First { int memi; double memd; }; class Second { int memi; double memd; }; class First obj1; |
Second obj2 = obj1; // ошибка: obj1 и obj2 имеют разные типы
Тело класса определяет отдельную область видимости. Объявление членов внутри тела помещает их имена в область видимости класса. Наличие в двух разных классах членов с одинаковыми именами – не ошибка, эти имена относятся к разным объектам. (Подробнее об областях видимости классов мы поговорим в разделе 13.9.)
После того как тип класса определен, на него можно ссылаться двумя способами:
· написать ключевое слово class, а после него – имя класса. В предыдущем примере объект obj1
класса First
объявлен именно таким образом;
· указать только имя класса. Так объявлен объект obj2
класса Second из приведенного примера.
Оба способа сослаться на тип класса эквивалентны. Первый заимствован из языка C и остается корректным методом задания типа класса; второй способ введен в C++ для упрощения объявлений.
Объект класса UserQuery
можно инициализировать указателем на вектор строк, представляющий запрос пользователя, или передать ему адрес этого вектора позже, с помощью функции-члена query(). Это позволяет использовать один объект для нескольких запросов. Фактическое построение иерархии классов Query выполняется функцией eval_query():
|
// определить объект, не имея запроса пользователя UserQuery user_query; string text; vector<string> query_text; // обработать запросы пользователя do { while( cin >> text ) query_text.push_back( text ); // передать запрос объекту UserQuery user_query.query( &query_text ); // вычислить результат запроса и вернуть // корень иерархии Query* Query *query = user_query.eval_query(); } |
while ( /* пользователь продолжает формулировать запросы */ );
Вот определение нашего класса UserQuery:
|
#ifndef USER_QUERY_H #define USER_QUERY_H #include <string> #include <vector> #include <map> #include <stack> typedef pair<short,short> location; typedef vector<location,allocator> loc; #include "Query.h" class UserQuery { public: UserQuery( vector< string,allocator > *pquery = 0 ) : _query( pquery ), _eval( 0 ), _paren( 0 ) {} Query *eval_query(); // строит иерархию void query( vector< string,allocator > *pq ); void displayQuery(); static void word_map( map<string,loc*,less<string>,allocator> *pwm ) { if ( !_word_map ) _word_map = pwm; } private: enum QueryType { WORD = 1, AND, OR, NOT, RPAREN, LPAREN }; QueryType evalQueryString( const string &query ); void evalWord( const string &query ); void evalAnd(); void evalOr(); void evalNot(); void evalRParen(); bool integrity_check();
int _paren; Query *_eval; vector<string> *_query; stack<Query*, vector<Query*> > _query_stack; stack<Query*, vector<Query*> > _current_op; static short _lparenOn, _rparenOn; static map<string,loc*,less<string>,allocator> *_word_map; }; |
inline void UserQuery:: evalAnd() { Query *pop = _query_stack.top(); _query_stack.pop(); AndQuery *pq = new AndQuery( pop ); if ( _lparenOn ) { pq->lparen( _lparenOn ); _lparenOn = 0; } if ( _rparenOn ) { pq->rparen( _rparenOn ); _rparenOn = 0; } _current_op.push( pq ); } inline void UserQuery:: evalOr() { Query *pop = _query_stack.top(); _query_stack.pop(); OrQuery *pq = new OrQuery( pop ); if ( _lparenOn ) { pq->lparen( _lparenOn ); _lparenOn = 0; } if ( _rparenOn ) { pq->rparen( _rparenOn ); _rparenOn = 0; } _current_op.push( pq ); |
inline void UserQuery:: evalNot() { NotQuery *pq = new NotQuery; if ( _lparenOn ) { pq->lparen( _lparenOn ); _lparenOn = 0; } if ( _rparenOn ) { pq->rparen( _rparenOn ); _rparenOn = 0; } _current_op.push( pq ); |
inline void UserQuery:: evalRParen() { if ( _paren < _current_op.size() ) { Query *poperand = _query_stack.top(); _query_stack.pop(); Query *pop = _current_op.top(); _current_op.pop(); pop->add_op( poperand ); _query_stack.push( pop ); } |
inline void UserQuery:: evalWord( const string &query ) { NameQuery *pq; loc *ploc; if ( ! _word_map->count( query )) pq = new NameQuery( query ); else { ploc = ( *_word_map )[ query ]; pq = new NameQuery( query, *ploc ); } if ( _current_op.size() <= _paren ) _query_stack.push( pq ); else { Query *pop = _current_op.top(); _current_op.pop(); pop->add_op( pq ); _query_stack.push( pop ); } |
В самом простом случае оператор определения объекта состоит из спецификатора типа и имени объекта и заканчивается точкой с запятой. Например:
|
double salary; double wage; int month; int day; int year; |
unsigned long distance;
В одном операторе можно определить несколько объектов одного типа. В этом случае их имена перечисляются через запятую:
double salary, wage;
int month,
day, year;
unsigned long distance;
Простое определение переменной не задает ее начального значения. Если объект определен как глобальный, спецификация С++ гарантирует, что он будет инициализирован нулевым значением. Если же переменная локальная либо динамически размещаемая (с помощью оператора new), ее начальное значение не определено, то есть она может содержать некоторое случайное значение.
Использование подобных переменных– очень распространенная ошибка, которую к тому же трудно обнаружить. Рекомендуется явно указывать начальное значение объекта, по крайней мере в тех случаях, когда неизвестно, может ли объект инициализировать сам себя. Механизм классов вводит понятие конструктора по умолчанию, который служит для присвоения значений по умолчанию. (Мы уже сказали об этом в разделе 2.3. Разговор о конструкторах по умолчанию будет продолжен немного позже, в разделах 3.11 и 3.15, где мы будем разбирать классы string и complex из стандартной библиотеки.)
|
int main() { |
// неинициализированный локальный объект
int ival;
// объект типа string инициализирован
// конструктором по умолчанию
string project;
// ...
}
Начальное значение может быть задано прямо в операторе определения переменной. В С++ допустимы две формы инициализации переменной – явная, с использованием оператора присваивания:
|
int ival = 1024; |
string project = "Fantasia 2000";
и неявная, с заданием начального значения в скобках:
|
int ival( 1024 ); |
string project( "Fantasia 2000" );
Оба варианта эквивалентны и задают начальные значения для целой переменной ival как 1024 и для строки project как "Fantasia 2000".
double salary = 9999.99, wage = salary + 0.01; int month = 08; |
// ival получает значение 0, а dval - 0.0 |
// int() применяется к каждому из 10 элементов |
#include <cmath> |
(a) int car = 1024, auto = 2048; (b) int ival = ival; (c) int ival( int() ); (d) double salary = wage = 9999.99; |
(a) extern string name; string name( "exercise 3.5a" ); (b) extern vector<string> students; |
(a) int double = 3.14159; (b) vector< int > _; (c) string namespase; (d) string catch-22; |
string global_class; int global_int; |
Чтобы определить объект класса map, мы должны указать, как минимум, типы ключа и значения. Например:
map<string,int> word_count;
Здесь задается объект word_count
типа map, для которого ключом служит объект типа string, а ассоциированным с ним значением – объект типа int. Аналогично
|
class employee; |
map<int,employee*>
personnel;
определяет personnel как отображение ключа типа int (уникальный номер служащего) на указатель, адресующий объект класса employee.
Для нашей поисковой системы полезно такое отображение:
|
typedef pair<short,short> location; typedef vector<location> loc; |
map<string,loc*> text_map;
Поскольку имевшийся в нашем распоряжении компилятор не поддерживал аргументы по умолчанию для параметров шаблона, нам пришлось написать более развернутое определение:
|
map<string,loc*, // ключ, значение less<string>, // оператор сравнения allocator> // распределитель памяти по умолчанию |
text_map;
По умолчанию сортировка ассоциативных контейнеров производится с помощью операции “меньше”. Однако можно указать и другой оператор сравнения (см. раздел 12.3 об объектах-функциях).
После того как отображение определено, необходимо заполнить его парами ключ/значение. Интуитивно хочется написать примерно так:
|
#include <map> #include <string> map<string,int> word_count; word_count[ string("Anna") ] = 1; word_count[ string("Danny") ] = 1; word_count[ string("Beth") ] = 1; |
// и так далее ...
Когда мы пишем:
word_count[ string("Anna") ] = 1;
на самом деле происходит следующее:
1. Безымянный временный объект типа string
инициализируется значением "Anna" и передается оператору взятия индекса, определенному в классе map.
2. Производится поиск элемента с ключом "Anna" в массиве word_count. Такого элемента нет.
3. В word_count вставляется новая пара ключ/значение. Ключом является, естественно, строка "Anna". Значением – 0, а не 1.
// предпочтительный метод вставки одного элемента word_count.insert( map<string,i nt>:: value_type( string("Anna"), 1 ) |
map< string,int >:: |
map< string, int > word_count; // ... заполнить map< string,int > word_count_two; // скопируем все пары ключ/значение |
// инициализируем копией всех пар ключ/значение |
(*word_map)[(*text_words)[ix]]-> |
// возьмем слово, которое надо обновить string word = (*text_words) [ix]; // возьмем значение из вектора позиций vector<location> *ploc = (*word_map) [ word ]; // возьмем позицию - пару координат loc = (*text_locs)[ix]; // вставим новую позицию |
int main() { // считываем файл и выделяем слова vector<string, allocator> *text_file = retrieve_text(); text_loc *text_locations = separate_words( text_file ); // обработаем слова // ... // построим отображение слов на векторы позиций map<string,lос*,less<string>,allocator> *text_map = build_word_map( text_locatons ); // ... |
// typedef для удобства чтения typedef pair< short,short > location; typedef vector< location > loc; typedef vector< string > text; typedef pair< text*,loc* > text_loc; extern map< string, loc* >* |
map<string,loc*> *word_map = new map< string, loc* >; vector<string> *text_words = text_locations->first; |
register int elem_cnt = text_words->size(); for ( int ix=0; ix < elem_cnt; ++ix ) { string textword = ( *text_words )[ ix ]; // игнорируем слова короче трех букв // или присутствующие в списке стоп-слов if ( textword.size() < 3 || exclusion_set.count( textword )) continue; // определяем, занесено ли слово в отображение // если count() возвращает 0 - нет: добавим его if ( ! word_map->count((*text_words)[-ix] )) { loc *ploc = new vector<location>; ploc->push_back( (*text_locs) [ix] ); word_map->insert(value_type((*text_words)[ix],ploc)); } else // добавим дополнительные координаты (*word_map)[(*text_words)[ix]]-> push_back((*text_locs)[ix]); |
Перед использованием класса set
необходимо включить соответствующий заголовочный файл:
#include <set>
Вот определение нашего множества стоп-слов:
set<string> exclusion_set;
Отдельные элементы могут добавляться туда с помощью операции insert(). Например:
|
exclusion_set.insert( "the" ); |
exclusion_set.insert( "and" );
Передавая insert() пару итераторов, можно добавить целый диапазон элементов. Скажем, наша поисковая система позволяет указать файл со стоп-словами. Если такой файл не задан, берется некоторый набор слов по умолчанию:
|
typedef set< string >::difference_type diff_type; set< string > exclusion_set; ifstream infile( "exclusion_set" ); if ( ! infile ) { static string default_excluded_words[25] = { "the","and","but","that","then","are","been", "can"."can't","cannot","could","did","for", "had","have","him","his","her","its","into", "were","which","when","with","would" }; cerr << "предупреждение! невозможно открыть файл стоп-слов! -- " << "используется стандартный набор слов \n"; copy( default_excluded_words, default_excluded_words+25, inserter( exclusion_set, exclusion_set.begin() )); } else { istream_iterator<string,diff_type> input_set(infile),eos; copy( input_set, eos, inserter( exclusion_set, exclusion_set.begin() )); |
}
В этом фрагменте кода встречаются два элемента, которые мы до сих пор не рассматривали: тип difference_type и класс inserter. difference_type – это тип результата вычитания двух итераторов для нашего множества строк. Он передается в качестве одного из параметров шаблона istream_iterator.
copy() –один из обобщенных алгоритмов. (Мы рассмотрим их в главе 12 и в Приложении.) Первые два параметра – пара итераторов или указателей – задают диапазон. Третий параметр является либо итератором, либо указателем на начало контейнера, в который элементы копируются.
Проблема с этой функцией вызвана ограничением, вытекающим из ее реализации: количество копируемых элементов не может превосходить числа элементов в контейнере-адресате. Дело в том, что copy() не вставляет элементы, она только присваивает каждому элементу новое значение. Однако ассоциативные контейнеры не позволяют явно задать размер. Чтобы скопировать элементы в наше множество, мы должны заставить copy()
вставлять элементы. Именно для этого служит класс inserter
(детально он рассматривается в разделе 12.4).
Каждый производный класс наследует данные и функции-члены своего базового класса, и программировать приходится лишь те аспекты, которые изменяют или расширяют его поведение. К примеру, в классе NameQuery необходимо определить реализацию eval(). Кроме того, нужна поддержка для хранения слова-операнда, представленного объектом класса типа string.
Наконец, для получения ассоциированного вектора позиций должно быть доступно отображение слов на векторы. Поскольку один такой объект разделяется всеми объектами класса NameQuery, мы объявляем его статическим членом. Первая попытка определения NameQuery
(рассмотрение конструкторов, деструктора и копирующего оператора присваивания мы снова отложим) выглядит так:
|
typedef vector<location> loc; class NameQuery : public Query { public: // ... // переопределяет виртуальную функцию Query::eval()2 virtual void eval(); // функция чтения string name() const { return _name; } static const map<string,loc*> *word_map() { return _word_map; } protected: string _name; static map<string,loc*> *_word_map; |
};
Класс NotQuery в дополнение к предоставлению реализации виртуальной функции eval() должен обеспечить поддержку своего единственного операнда. Поскольку им может быть объект любого из производных классов, определим его как указатель на тип Query. Результат запроса NotQuery, напомним, обязан содержать не только строки текста, где нет указанного слова, но также и номера колонок внутри каждой строки. Например, если есть запрос:
! daddy
то операнд запроса NotQuery
включает следующий вектор позиций:
daddy ((0,8),(3,3),(5,5))
Вектор позиций, возвращаемый в ответ на исходный запрос, должен включать все номера колонок в строках (1,2,4). Кроме того, он должен включать все номера колонок в строке (0), кроме колонки (8), все номера колонок в строке (3), кроме колонки (3), и все номера колонок в строке (5), кроме колонки (5).
class NotQuery : public Query { public: // ... // альтернативный синтаксис: явно употреблено ключевое слово virtual // переопределение Query::eval() virtual void eval(); // функция доступа для чтения const Query *op() const { return _op; } static const vector< location > * all_locs() { return _all_locs; } protected: Query *_op; static const vector< location > *_all_locs; |
class OrQuery : public Query { public: // ... virtual void eval(); const Query *rop() const { return _rop; } const Query *lop() const { return _lop; } protected: Query *_lop; Query *_rop; |
class AndQuery : public Query { public: // конструкторы обсуждаются в разделе 17.4 virtual void eval(); const Query *rop() const { return _rop; } const Query *lop() const { return _lop; } static void max_col( const vector< int > *pcol ) { if ( !_max_col ) _max_col = pcol; } protected: Query *_lop; Query *_rop; static const vector< int > *_max_col; |
Иногда может показаться, что сильно типизированный язык создает препятствия для реализации совсем простых функций. Например, хотя следующий алгоритм функции min()
тривиален, сильная типизация требует, чтобы его разновидности были реализованы для всех типов, которые мы собираемся сравнивать:
|
int min( int a, int b ) { return a < b ? a : b; } double min( double a, double b ) { return a < b ? a : b; |
}
Заманчивую альтернативу явному определению каждого экземпляра функции min()
представляет использование макросов, расширяемых препроцессором:
#define min(a, b) ((a) < (b) ? (a) : (b))
Но этот подход таит в себе потенциальную опасность. Определенный выше макрос правильно работает при простых обращениях к min(), например:
|
min( 10, 20 ); |
min( 10.0, 20.0 );
но может преподнести сюрпризы в более сложных случаях: такой механизм ведет себя не как вызов функции, он лишь выполняет текстовую подстановку аргументов. В результате значения обоих аргументов оцениваются дважды: один раз при сравнении a и b, а второй – при вычислении возвращаемого макросом результата:
|
#include <iostream> #define min(a,b) ((a) < (b) ? (a) : (b)) const int size = 10; int ia[size]; int main() { int elem_cnt = 0; int *p = &ia[0]; // подсчитать число элементов массива while ( min(p++,&ia[size]) != &ia[size] ) ++elem_cnt; cout << "elem_cnt : " << elem_cnt << "\texpecting: " << size << endl; return 0; |
}
На первый взгляд, эта программа подсчитывает количество элементов в массиве ia
целых чисел. Но в этом случае макрос min() расширяется неверно, поскольку операция постинкремента применяется к аргументу-указателю дважды при каждой подстановке. В результате программа печатает строку, свидетельствующую о неправильных вычислениях:
elem_cnt : 5 expecting: 10
Шаблоны функций предоставляют в наше распоряжение механизм, с помощью которого можно сохранить семантику определений и вызовов функций (инкапсуляция фрагмента кода в одном месте программы и гарантированно однократное вычисление аргументов), не принося в жертву сильную типизацию языка C++, как в случае применения макросов.
template <class Type> Type min2( Type a, Type b ) { return a < b ? a : b; } int main() { // правильно: min( int, int ); min( 10, 20 ); // правильно: min( double, double ); min( 10.0, 20.0 ); return 0; |
template <class Glorp> Glorp min2( Glorp a, Glorp b ) { return a < b ? a : b; |
template <class Type, int size> |
template <class Type, int size> Type min( const Type (&r_array)[size] ) { /* параметризованная функция для отыскания * минимального значения в массиве */ Type min_val = r_array[0]; for ( int i = 1; i < size; ++i ) if ( r_array[i] < min_val ) min_val = r_array[i]; return min_val; |
// size определяет размер параметра-массива и инициализирует // переменную типа const int template <class Type, int size> Type min( const Type (&r_array)[size] ) { const int loc_size = size; Type loc_array[loc_size]; // ... |
typedef double Type; template <class Type> Type min( Type a, Type b ) { // tmp имеет тот же тип, что параметр шаблона Type, а не заданный // глобальным typedef Type tm = a < b ? a : b; return tmp; |
template <class Type> Type min( Type a, Type b ) { // ошибка: повторное объявление имени Type, совпадающего с именем // параметра шаблона typedef double Type; Type tmp = a < b ? a : b; return tmp; |
// правильно: T1 представляет тип значения, возвращаемого min(), // а T2 и T3 – параметры-типы этой функции template <class T1, class T2, class T3> |
// ошибка: неправильное повторное использование имени параметра Type template <class Type, class Type> |
// правильно: повторное использование имени Type внутри шаблона template <class Type> |
template <class Type> |
// все три объявления min() относятся к одному и тому же шаблону функции // опережающие объявления шаблона template <class T> T min( T, T ); template <class U> U min( U, U ); // фактическое определение шаблона template <class Type> |
#include <vector> // правильно: Type используется неоднократно в списке параметров шаблона template <class Type> |
// правильно: ключевые слова typename и class могут перемежаться template <typename T, class U> T minus( T*, U ); // ошибка: должно быть <typename T, class U> или // <typename T, typename U> template <typename T, U> |
template <class Parm, class U> Parm minus( Parm* array, U value ) { Parm::name * p; // это объявление указателя или умножение? // На самом деле умножение |
template <class Parm, class U> Parm minus( Parm* array, U value ) { typename Parm::name * p; // теперь это объявление указателя |
// правильно: спецификатор после списка параметров template <typename Type> inline Type min( Type, Type ); // ошибка: спецификатор inline не на месте inline template <typename Type> |
(a) template <class T, U, class V> void foo( T, U, V ); (b) template <class T> T foo( int *T ); (c) template <class T1, typename T2, class T3> T1 foo( T2, T3 ); (d) inline template <typename T> T foo( T, unsigned int* ); (e) template <class myT, class myT> void foo( myT, myT ); (f) template <class T> foo( T, T ); (g) typedef char Ctype; template <class Ctype> |
(a) template <class Type> Type bar( Type, Type ); template <class Type> Type bar( Type, Type ); (b) template <class T1, class T2> void bar( T1, T2 ); template <typename C1, typename C2> |
Предположим, что нам нужно определить класс, поддерживающий механизм очереди. Очередь– это структура данных для хранения коллекции объектов; они помещаются в конец очереди, а извлекаются из ее начала. Поведение очереди описывают аббревиатурой FIFO – “первым пришел, первым ушел”. (Определенный в стандартной библиотеке C++ тип, реализующий очередь,
упоминался в разделе 6.17. В этой главе мы создадим упрощенный тип для знакомства с шаблонами классов.)
Необходимо, чтобы наш класс Queue
поддерживал следующие операции:
· добавить элемент в конец очереди:
void add( item );
· удалить элемент из начала очереди:
item remove();
· определить, пуста ли очередь:
bool is_empty();
· определить, заполнена ли очередь:
bool is_full();
Определение Queue
могло бы выглядеть так:
template <class Type> class Queue { public: Queue(); ~Queue(); Type& remove(); void add( const Type & ); bool is_empty(); bool is_full(); private: // ... |
Queue<int> qi; Queue< complex<double> > qc; |
template <class T> |
template <class T1, class T2, class T3> |
// ошибка: должно быть <typename T, class U> или // <typename T, typename U> template <typename T, U> |
template <class Type, int size> |
template <class Type> class QueueItem { public: // ... private: // Type представляет тип члена Type item; QueueItem *next; |
typedef double Type; template <class Type> class QueueItem { public: // ... private: // тип Item - не double Type item; QueueItem *next; |
template <class Type> class QueueItem { public: // ... private: // ошибка: член не может иметь то же имя, что и // параметр шаблона Type typedef double Type; Type item; QueueItem *next; |
// ошибка: неправильное использование имени параметра шаблона Type template <class Type, class Type> |
// правильно: повторное использование имени Type в разных шаблонах template <class Type> class QueueItem; template <class Type> |
// все три объявления QueueItem // относятся к одному и тому же шаблону класса // объявления шаблона template <class T> class QueueItem; template <class U> class QueueItem; // фактическое определение шаблона template <class Type> |
template <class Type, size = 1024> |
template <class Type, size = 1024> |
// правильно: рассматриваются аргументы по умолчанию из обоих объявлений template <class Type=string, int size> |
template <class Type> class QueueItem { public: QueueItem( const Type & ); private: Type item; QueueItem *next; |
template <class Type> void display( QueueItem<Type> &qi ) { QueueItem<Type> *pqi = &qi; // ... |
class Queue {
public:
Queue();
~Queue();
Type& remove();
void add( const Type & );
bool is_empty();
bool is_full();
private:
// ...
};
Вопрос в том, какой тип использовать вместо Type? Предположим, что мы решили реализовать класс Queue, заменив Type на int. Тогда Queue
может управлять коллекциями объектов типа int. Если бы понадобилось поместить в очередь объект другого типа, то его пришлось бы преобразовать в тип int, если же это невозможно, компилятор выдаст сообщение об ошибке:
|
Queue qObj; string str( "vivisection" ); qObj.add( 3.14159 ); // правильно: в очередь помещен объект 3 |
qObj.add( str ); // ошибка: нет преобразования из string в int
Поскольку любой объект в коллекции имеет тип int, то язык C++ гарантирует, что в очередь можно поместить либо значение типа int, либо значение, преобразуемое в такой тип. Это подходит, если предстоит работа с очередями объектов только типа int. Если же класс Queue
Определение пользовательского пространства имен начинается с ключевого слова namespace, за которым следует идентификатор. Он должен быть уникальным в той области видимости, в которой определяется данное пространство; наличие другой сущности с тем же именем является ошибкой. Конечно, это не означает, что проблема засорения глобального пространства решена полностью, но существенно помогает в ее решении.
За идентификатором пространства имен следует блок в фигурных скобках, содержащий различные объявления. Любое объявление, допустимое в области видимости глобального пространства, может встречаться и в пользовательском: классы, переменные (вместе с инициализацией), функции (вместе со своими определениями), шаблоны.
Помещая объявление в пользовательское пространство, мы не меняем его семантики. Единственное отличие состоит в том, что имена, вводимые такими объявлениями, включают в себя имя пространства, внутри которого они объявлены. Например:
|
namespace cplusplus_primer { class matrix { /* ... */ }; void inverse ( matrix & ); matrix operator+ ( const matrix &ml, const matrix &m2 ) {/* ... */ } const double pi = 3.1416; |
}
Именем класса, объявленного в пространстве cplusplus_primer, будет
cplusplus_primer::matrix
Именем функции
cplusplus_primer::inverse()
Именем константы
cplusplus_primer::pi
Имя класса, функции или константы расширяется именем пространства, в котором они объявлены. Такие имена называют квалифицированными.
Определение пространства имен не обязательно должно быть непрерывным. Например, предыдущее пространство могло быть определено таким образом:
|
namespace cplusplus_primer { class matrix { /* ... */ }; const double pi = 3.1416; } namespace cplusplus_primer { void inverse ( matrix & ); matrix operator+ ( const matrix &ml, const matrix &m2 ) {/* ... */ } |
}
Два приведенных примера эквивалентны: оба задают пространство имен cplusplus_primer, содержащее класс matrix, функцию inverse(), константу pi и operator+(). Определение пространства имен может состоять из нескольких соединенных частей.
// Эта часть пространства имен // определяет интерфейс библиотеки namespace cplusplus_primer { class matrix { /* ... */ }; const double pi = 3.1416; matrix operator+ ( const matrix &ml, const matrix &m2 ); void inverse ( matrix & ); } // Эта часть пространства имен // определяет реализацию библиотеки namespace cplusplus_primer { void inverse ( matrix &m ) { /* ... */ } matrix operator+ ( const matrix &ml, const matrix &m2 ) { /* ... */ } |
// ---- primer.h ---- namespace cplusplus_primer { class matrix { /*... */ }; const double pi = 3.1416; matrix operator+ ( const matrix &m1, const matrix &m2 ); void inverse( matrix & ); } // ---- primer.C ---- #include "primer.h" namespace cplusplus_primer { void inverse( matrix &m ) { /* ... */ } matrix operator+ ( const matrix &m1, const matrix &m2 ) { /* ... */ } |
// ---- user.C ---- // определение интерфейса библиотеки #include "primer.h" void func( cplusplus_primer::matrix &m ) { //... cplusplus_primer: :inverse( m ); return m; |
По умолчанию любой объект, функция, тип или шаблон, объявленный в глобальной области видимости, также называемой областью видимости глобального пространства имен, вводит глобальную сущность. Каждая такая сущность обязана иметь уникальное имя. Например, функция и объект не могут быть одноименными, даже если они объявлены в разных исходных файлах.
Таким образом, используя в своей программе некоторую библиотеку, мы должны быть уверены, что имена глобальных сущностей нашей программы не совпадают с именами из библиотеки. Это нелегко, если мы работаем с библиотеками разных производителей, где определено много глобальных имен. Собирая программу с такими библиотеками, нельзя гарантировать, что имена глобальных сущностей не будут вступать в конфликт.
Обойти эту проблему, названную проблемой засорения области видимости глобального пространства имен, можно посредством очень длинных имен. Часто в качестве их префикса употребляется определенная последовательность символов. Например:
|
class cplusplus_primer_matrix { ... }; |
void inverse( cplusplus_primer_matrix & );
Однако у этого решения есть недостаток. Программа, написанная на С++, может содержать множество глобальных классов, функций и шаблонов, видимых в любой точке кода. Работать со слишком длинными идентификаторами для программистов утомительно.
Пространства имен помогают справиться с проблемой засорения более удобным способом. Автор библиотеки может задать собственное пространство и таким образом вынести используемые в библиотеке имена из глобальной области видимости:
|
namespace cplusplus_primer { class matrix { /*...*/ }; void inverse ( matrix & ); |
}
cplusplus_primer
является пользовательским пространством имен
(в отличие от глобального пространства, которое неявно подразумевается и существует в любой программе).
Каждое такое пространство представляет собой отдельную область видимости. Оно может содержать вложенные определения пространств имен, а также объявления или определения функций, объектов, шаблонов и типов. Все сущности, объявленные внутри некоторого пространства имен, называются его членами. Каждое имя в пользовательском пространстве, как и в глобальном, должно быть уникальным в пределах этого пространства.
void func( cplusplus_primer::matrix &m ) { // ... cplusplus_primer::inverse(m); return m; |
void func( DisneyFeatureAnimation::matrix &m ) { // ... DisneyFeatureAnimation::inverse(m); return m; |
Ниже представлено определение шаблона класса Queue. Оно помещено в заголовочный файл Queue.h
вместе с определением шаблона QueueItem:
|
#ifndef QUEUE_H #define QUEUE_H // объявление QueueItem template <class T> class QueueItem; template <class Type> class Queue { public: Queue() : front( 0 ), back ( 0 ) { } ~Queue(); Type& remove(); void add( const Type & ); bool is_empty() const { return front == 0; } private: QueueItem<Type> *front; QueueItem<Type> *back; }; |
#endif
При использовании имени Queue
внутри определения шаблона класса Queue список параметров <Type>
можно опускать. Однако пропуск списка параметров шаблона QueueItem в определении шаблона Queue
недопустим. Так, объявление члена front является ошибкой:
|
template <class Type> class Queue { public: // ... private: // ошибка: список параметров для QueueItem неизвестен QueueItem<Type> *front; |
}
Упражнение 16.1
Найдите ошибочные объявления (или пары объявлений) шаблонов классов:
|
(a) template <class Type> class Container1; template <class Type, int size> |
class Container1;
|
(b) template <class T, U, class V> |
class Container2;
|
(c) template <class C1, typename C2> |
class Container3 {};
|
(d) template <typename myT, class myT> |
class Container4 {};
|
(e) template <class Type, int *pi> |
class Container5;
|
(f) template <class Type, int val = 0> class Container6; template <class T = complex<double>, int v> |
class Container6;
Упражнение 16.2
Следующее определение шаблона List
некорректно. Как исправить ошибку?
|
template <class elemenType> class ListItem; template <class elemType> class List { public: List<elemType>() : _at_front( 0 ), _at_end( 0 ), _current( 0 ), _size( 0 ) {} List<elemType>( const List<elemType> & ); List<elemType>& operator=( const List<elemType> & ); ~List(); void insert( ListItem *ptr, elemType value ); int remove( elemType value ); ListItem *find( elemType value ); void display( ostream &os = cout ); int size() { return _size; } private: ListItem *_at_front; ListItem *_at_end; ListItem *_current; int _size |
};
Мы уже видели, как преобразования типов применяются к операндам встроенных типов: в разделе 4.14 этот вопрос рассматривался на примере операндов встроенных операторов, а в разделе 9.3 – на примере фактических аргументов вызванной функции для приведения их к типам формальных параметров. Рассмотрим с этой точки зрения следующие шесть операций сложения:
|
char ch; short sh;, int ival; /* в каждой операции один операнд * требует преобразования типа */ ch + ival; ival + ch; ch + sh; ch + ch; |
ival + sh; sh + ival;
Операнды ch и sh
расширяются до типа int. При выполнении операции складываются два значения типа int. Расширение типа неявно выполняется компилятором и для пользователя прозрачно.
В этом разделе мы рассмотрим, как разработчик может определить собственные преобразования для объектов типа класса. Такие определенные пользователем преобразования также автоматически вызываются компилятором по мере необходимости. Чтобы показать, зачем они нужны, обратимся снова к классу SmallInt, введенному в разделе 10.9.
Напомним, что SmallInt
позволяет определять объекты, способные хранить значения из того же диапазона, что unsigned char, т.е. от 0 до 255, и перехватывает ошибки выхода за его границы. Во всех остальных отношениях этот класс ведет себя точно так же, как unsigned char.
Чтобы иметь возможность складывать объекты SmallInt с другими объектами того же класса или со значениями встроенных типов, а также вычитать их, реализуем шесть операторных функций:
|
class SmallInt { friend operator+( const SmallInt &, int ); friend operator-( const SmallInt &, int ); friend operator-( int, const SmallInt & ); friend operator+( int, const SmallInt & ); public: SmallInt( int ival ) : value( ival ) { } operator+( const SmallInt & ); operator-( const SmallInt & ); // ... private: int value; |
};
Операторы-члены дают возможность складывать и вычитать два объекта SmallInt. Глобальные же операторы-друзья позволяют производить эти операции над объектами данного класса и объектами встроенных арифметических типов. Необходимо только шесть операторов, поскольку любой встроенный арифметический тип может быть приведен к типу int. Например, выражение
SmallInt si( 3 ); |
class SmallInt { public: SmallInt( int ival ) : value( ival ) { } // конвертер // SmallInt ==> int operator int() { return value; } // перегруженные операторы не нужны private: int value; |
SmallInt si( 3 ); |
#include <iostream> #include "SmallInt.h" int main() { cout << "Введите SmallInt, пожалуйста: "; while ( cin >> si1 ) { cout << "Прочитано значение " << si1 << "\nОно "; // SmallInt::operator int() вызывается дважды cout << ( ( si1 > 127 ) ? "больше, чем " : ( ( si1 < 127 ) ? "меньше, чем " : "равно ") ) << "127\n"; cout << "\Введите SmallInt, пожалуйста \ (ctrl-d для выхода): "; } cout <<"До встречи\n"; |
#include <iostream> class SmallInt { friend istream& operator>>( istream &is, SmallInt &s ); friend ostream& operator<<( ostream &is, const SmallInt &s ) { return os << s.value; } public: SmallInt( int i=0 ) : value( rangeCheck( i ) ){} int operator=( int i ) { return( value = rangeCheck( i ) ); } operator int() { return value; } private: int rangeCheck( int ); int value; |
istream& operator>>( istream &is, SmallInt &si ) { int ix; is >> ix; si = ix; // SmallInt::operator=(int) return is; } int SmallInt::rangeCheck( int i ) { /* если установлен хотя бы один бит, кроме первых восьми, * то значение слишком велико; сообщить и сразу выйти */ if ( i & ~0377 ) { cerr << "\n*** Ошибка диапазона SmallInt: " << i << " ***" << endl; exit( -1 ); } return i; |
Когда мы применили закрытое наследование класса PeekbackStack от IntArray, то все защищенные и открытые члены IntArray стали закрытыми членами PeekbackStack. Было бы полезно, если бы пользователи PeekbackStack могли узнать размер стека с помощью такой инструкции:
is.size();
Разработчик способен оградить некоторые члены базового класса от эффектов неоткрытого наследования. Вот как, к примеру, открывается функция-член size() класса IntArray:
|
class PeekbackStack : private IntArray { public: // сохранить открытый уровень доступа using IntArray::size; // ... |
};
Еще одна причина для открытия отдельных членов заключается в том, что иногда необходимо разрешить доступ к защищенным членам закрыто унаследованного базового класса при последующем наследовании. Предположим, что пользователям нужен подтип стека PeekbackStack, который может динамически расти. Для этого классу, производному от PeekbackStack, понадобится доступ к защищенным элементам ia и _size класса IntArray:
|
template <class Type> class PeekbackStack : private IntArray { public: using intArray::size; // ... protected: using intArray::size; using intArray::ia; // ... |
};
Производный класс может лишь вернуть унаследованному члену исходный уровень доступа, но не повысить или понизить его по сравнению с указанным в базовом классе.
На практике множественное наследование очень часто применяется для того, чтобы унаследовать открытый интерфейс одного класса и закрытую реализацию другого. Например, в библиотеку классов Booch Components включена следующая реализация растущей очереди Queue
(см. также статью Майкла Вило (Michaeel Vilot) и Грейди Буча (Grady Booch) в [LIPPMAN96b]):
|
template < class item, class container > class Unbounded_Queue: private Simple_List< item >, // ðåàëèçàöèÿ public Queue< item > // èíòåðôåéñ |
{ ... }
Открытое наследование называется еще наследованием типа. Производный класс в этом случае является подтипом базового; он замещает реализации всех функций-членов, специфичных для типа базового класса, и наследует общие для типа и подтипа функции. Можно сказать, что производный класс служит примером отношения “ЯВЛЯЕТСЯ”, т.е. предоставляет специализацию более общего базового класса. Медведь (Bear) является животным из зоопарка (ZooAnimal); аудиокнига (AudioBook) является предметом, выдаваемым читателям (LibraryLendingMaterial). Мы говорим, что Bear– это подтип ZooAnimal, равно как и Panda. Аналогично AudioBook – подтип LibBook (библиотечная книга), а оба они – подтипы LibraryLendingMaterial. В любом месте программы, где ожидается базовый тип, можно вместо него подставить открыто унаследованный от него подтип, и программа будет продолжать работать правильно (при условии, конечно, что подтип реализован корректно). Во всех приведенных выше примерах демонстрировалось именно наследование типа.
Закрытое наследование называют также наследованием реализации. Производный класс напрямую не поддерживает открытый интерфейс базового, но пользуется его реализацией, предоставляя свой собственный открытый интерфейс.
Чтобы показать, какие здесь возникают вопросы, реализуем класс PeekbackStack, который поддерживает выборку из стека с помощью метода peekback():
|
bool PeekbackStack:: |
peekback( int index, type &value ) { ... }
где value
содержит элемент в позиции index, если peekback()
вернула true. Если же peekback()
возвращает false, то заданная аргументом index
позиция некорректна и в value помещается элемент из вершины стека.
В реализации PeekbackStack
возможны два типа ошибок:
· реализация абстракции PeekbackStack: некорректная реализация поведения класса;
· реализация представления данных: неправильное управление выделением и освобождением памяти, копированием объектов из стека и т.п.
#include "IntArray.h" class PeekbackStack : public IntArray { private: const int static bos = -1; public: explicit PeekbackStack( int size ) : IntArray( size ), _top( bos ) {} bool empty() const { return _top == bos; } bool full() const { return _top == size()-1; } int top() const { return _top; } int pop() { if ( empty() ) /* îáðàáîòàòü îøèáêó */ ; return _ia[ _top-- ]; } void push( int value ) { if ( full() ) /* îáðàáîòàòü îøèáêó */ ; _ia[ ++_top ] = value; } bool peekback( int index, int &value ) const; private: int _top; }; inline bool PeekbackStack:: peekback( int index, int &value ) const { if ( empty() ) /* îáðàáîòàòü îøèáêó */ ; if ( index < 0 || index > _top ) { value = _ia[ _top ]; return false; } value = _ia[ index ]; return true; |
extern void swap( IntArray&, int, int ); PeekbackStack is( 1024 ); // íåïðåäâèäåííîå îøèáî÷íîå èñïîëüçîâàíèå PeekbackStack swap(is, i, j); is.sort(); |
Начинающие программисты часто удивляются, почему некорректные определения классов AndQuery и OrQuery (в которых отсутствуют необходимые объявления конструкторов) компилируются без ошибок. Если бы мы не попытались определить фактический объект класса AndQuery, в этой модифицированной иерархии так и осталась бы ненайденная ошибка. Дело в том, что:
·
если ошибка обнаруживается в точке объявления, то мы не можем продолжать компиляцию приложения, пока не исправим ее. Если же конфликтующее объявление – это часть библиотеки, для которой у нас нет исходного текста, то разрешение конфликта может оказаться нетривиальной задачей. Более того, возможно, в нашем коде никогда и не возникнет ситуации, когда эта ошибка проявляется, так что для нас она останется лишь потенциальной угрозой;
· с другой стороны, если ошибка не найдена вплоть до момента использования, то код может оказаться замусоренным ошибками, проявляющимися в самый неподходящий момент к удивлению программиста. При такой стратегии успешная компиляция говорит не об отсутствии семантических ошибок, а лишь о том, что программа не исполняет код, нарушающий семантические правила языка.
Выдача сообщения об ошибке в точке использования – это одна из форм отложенного вычисления, распространенного метода повышения производительности программ. Он часто применяется для того, чтобы отложить потенциально дорогую операцию выделения или инициализации ресурса до момента, когда в нем возникнет реальная необходимость. Если ресурс так и не понадобится, мы сэкономим на ненужных подготовительных операциях. Если же он потребуется, но не сразу, мы растянем инициализацию программы на более длительный период.
В C++ потенциальные ошибки “комбинирования”, связанные с перегруженными функциями, шаблонами и наследованием классов, обнаруживаются в точке использования, а не в точке объявления. (Мы полагаем, что это правильно, поскольку необходимость выявлять все возможные ошибки, которые можно допустить в результате комбинирования многочисленных компонентов, – пустая трата времени). Следовательно, для обнаружения и устранения латентных ошибок необходимо тщательно тестировать код. Подобные ошибки, возникающие при комбинировании двух или более больших компонентов, допустимы; однако в пределах одного компонента, такого, как иерархия классов Query, их быть не должно.
Вернемся к задаче, сформулированной в начале данного раздела. Как использовать указатели на функции для сортировки элементов? Мы можем передать в алгоритм сортировки указатель на функцию, которая выполняет сравнение:
|
int sort( string*, string*, |
int (*)( const string &, const string & ) );
И в этом случае директива typedef
помогает сделать объявление sort() более понятным:
|
// Использование директивы typedef делает // объявление sort() более понятным typedef int ( *PFI2S )( const string &, const string & ); |
int sort( string*, string*, PFI2S );
Поскольку в большинстве случаев употребляется функция lexicoCompare, можно использовать значение параметра по умолчанию:
|
// значение по умолчанию для третьего параметра int lexicoCompare( const string &, const string & ); |
int sort( string*, string*, PFI2S = lexicoCompare );
Определение sort() выглядит следующим образом:
|
1 void sort( string *sl, string *s2, 2 PFI2S compare = lexicoCompare ) 3 { 4 // условие окончания рекурсии 5 if ( si < s2 ) { 6 string elem = *s1; 7 string *1ow = s1; 8 string *high = s2 + 1; 9 10 for (;;) { 11 while ( compare ( *++1ow, elem ) < 0 && low < s2) ; 12 while ( compare( elem, *--high ) < 0 && high > s1) 14 if ( low < high ) 15 1ow->swap(*high); 16 else break; 17 } // end, for(;;) 18 19 s1->swap(*high); 20 sort( s1, high - 1 ); 21 sort( high +1, s2 ); 22 } // end, if ( si < s2 ) |
23 }
sort()
реализует алгоритм быстрой сортировки Хоара
(C.A.R.Hoare). Рассмотрим ее определение детально. Она сортирует элементы массива от s1 до s2. Это рекурсивная функция, которая вызывает сама себя для последовательно уменьшающихся подмассивов. Рекурсия окончится тогда, когда s1 и s2
укажут на один и тот же элемент или s1 будет располагаться после s2
#include <iostream> #include <string> // это должно бы находиться в заголовочном файле int lexicoCompare( const string &, const string & ); int sizeCompare( const string &, const string & ); typedef int (*PFI)( const string &, const string & ); void sort( string *, string *, PFI=lexicoCompare ); string as[10] = { "a", "light", "drizzle", "was", "falling", "when", "they", "left", "the", "museum" }; int main() { // вызов sort() с значением по умолчанию параметра compare sort( as, as + sizeof(as)/sizeof(as[0]) - 1 ); // выводим результат сортировки for ( int i = 0; i < sizeof(as)/sizeof(as[0]); ++i ) cout << as[ i ].c_str() << "\n\t"; |
// typedef представляет собой тип функции typedef int functype( const string &, const string & ); |
void sort( string *, string *, |
// Использование директивы typedef делает // объявления более понятными typedef int (*PF)( int*, int ); |
// typedef представляет собой тип функции typedef int func( int*, int ); |
Массив в С++ никогда не передается по значению, а только как указатель на его первый, точнее нулевой, элемент. Например, объявление
void putValues( int[ 10 ] );
рассматривается компилятором так, как будто оно имеет вид
void putValues( int* );
Размер массива неважен при объявлении параметра. Все три приведенные записи эквивалентны:
|
// три эквивалентных объявления putValues() void putValues( int* ); void putValues( int[] ); |
void putValues( int[ 10 ] );
Передача массивов как указателей имеет следующие особенности:
·
изменение значения аргумента внутри функции затрагивает сам переданный объект, а не его локальную копию. Если такое поведение нежелательно, программист должен позаботиться о сохранении исходного значения. Можно также при объявлении функции указать, что она не должна изменять значение параметра, объявив этот параметр константой:
void putValues( const int[ 10 ] );
· размер массива не является частью типа параметра. Поэтому функция не знает реального размера передаваемого массива. Компилятор тоже не может это проверить. Рассмотрим пример:
|
void putValues( int[ 10 ] ); // рассматривается как int* int main() { int i, j [ 2 ]; putValues( &i ); // правильно: &i is int*; // однако при выполнении возможна ошибка putValues( j ); // правильно: j - адрес 0-го элемента - int*; |
// однако при выполнении возможна ошибка
При проверке типов параметров компилятор способен распознать, что в обоих случаях тип аргумента int*
соответствует объявлению функции. Однако контроль за тем, не является ли аргумент массивом, не производится.
По принятому соглашению C-строка является массивом символов, последний элемент которого равен нулю. Во всех остальных случаях при передаче массива в качестве параметра необходимо указывать его размер. Это относится и к массивам символов, внутри которых встречается 0. Обычно для такого указания используют дополнительный параметр функции. Например:
void putValues( int[], int size ); int main() { int i, j[ 2 ]; putValues( &i, 1 ); putValues( j, 2 ); return 0; |
#include <iostream> const lineLength =12; // количество элементов в строке void putValues( int *ia, int sz ) { cout << "( " << sz << " )< "; for (int i=0;i<sz; ++i ) { if ( i % lineLength == 0 && i ) cout << "\n\t"; // строка заполнена cout << ia[ i ]; // разделитель, печатаемый после каждого элемента, // кроме последнего if ( i % lineLength != lineLength-1 && i != sz-1 ) cout << ", "; } cout << " >\n"; |
// параметр - ссылка на массив из 10 целых void putValues( int (&arr)[10] ); int main() { int i, j [ 2 ]; putValues(i); // ошибка: // аргумент не является массивом из 10 целых putValues(j); // ошибка: // аргумент не является массивом из 10 целых return 0; |
#include <iostream> void putValues( int (&ia)[10] ) { cout << "( 10 )< "; for ( int 1 =0; i < 10; ++i ) { cout << ia[ i ]; // разделитель, печатаемый после каждого элемента, // кроме последнего if ( i != 9 ) cout << ", "; } cout << " >\n"; |
template <class Type> void putValues( Type *ia, int sz ) { // так же, как и раньше |
Использование ссылок в качестве параметров модифицирует стандартный механизм передачи по значению. При такой передаче функция манипулирует локальными копиями аргументов. Используя параметры-ссылки, она получает l-значения своих аргументов и может изменять их.
В каких случаях применение параметров-ссылок оправданно? Во-первых, тогда, когда без использования ссылок пришлось бы менять типы параметров на указатели (см. приведенную выше функцию swap()). Во-вторых, при необходимости вернуть из функции несколько значений. В-третьих, для передачи большого объекта типа класса. Рассмотрим два последних случая подробнее.
Как пример функции, использующей параметр-ссылку для возврата дополнительного значения, возьмем look_up(), которая будет искать заданную величину в векторе целых чисел. В случае успеха look_up()
вернет итератор, указывающий на найденный элемент, иначе– на элемент, расположенный за конечным. Если величина содержится в векторе несколько раз, итератор будет указывать на первое вхождение. Кроме того, дополнительный параметр-ссылка occurs
возвращает количество найденных элементов.
|
#include <vector> // параметр-ссылка 'occurs' // содержит второе возвращаемое значение vector<int>::const_iterator look_up( const vector<int> &vec, int value, // искомое значение int &occurs ) // количество вхождений { // res_iter инициализируется значением // следующего за конечным элемента vector<int>::const_iterator res_iter = vec.end(); occurs = 0; for ( vector<int>::const_iterator iter = vec.begin(); iter != vec.end(); ++iter ) if ( *iter == value ) { if ( res_iter == vec.end() ) res_iter = iter; ++occurs; } return res_iter; |
}
Третий случай, когда использование параметра-ссылки может быть полезно, – это большой объект типа класса в качестве аргумента. При передаче по значению объект будет копироваться целиком при каждом вызове функции, что для больших объектов может привести к потере эффективности. Используя параметр-ссылку, функция получает доступ к той области памяти, где размещен сам объект, без создания дополнительной копии.
class Huge { public: double stuff[1000]; }; extern int calc( const Huge & ); int main() { Huge table[ 1000 ]; // ... инициализация table int sum = 0; for ( int ix=0; ix < 1000; ++ix ) // calc() ссылается на элемент массива // типа Huge sum += calc( tab1e[ix] ); // ... |
class X; extern int foo_bar( X& ); int foo( const X& xx ) { // ошибка: константа передается // функции с параметром неконстантного типа return foo_bar( xx ); |
extern int foo_bar( const X& ); |
int foo( const X &xx ) { // ... X x2 = xx; // создать копию значения // foo_bar() может поменять x2, // xx останется нетронутым return foo_bar( x2 ); // правильно |
void ptrswap( int *&vl, int *&v2 ) { int *trnp = v2; v2 = vl; vl = tmp; |
#include <iostream> void ptrswap( int *&vl, int *&v2 ); int main() { int i = 10; int j = 20; int *pi = &i; int *pj = &j; cout << "Перед ptrswap():\tpi: " << *pi << "\tpj: " << *pj << endl; ptrswap( pi, pj ); cout << "После ptrswap():\tpi: " << *pi << "\tpj: " << pj << endl; return 0; |
Когда же лучше использовать параметры-ссылки, а когда – параметры-указатели? В конце концов, и те и другие позволяют функции модифицировать объекты, эффективно передавать в функцию большие объекты типа класса. Что выбрать: объявить параметр ссылкой или указателем?
Как было сказано в разделе 3.6, ссылка может быть один раз инициализирована значением объекта, и впоследствии изменить ее нельзя. Указатель же в течение своей жизни способен адресовать разные объекты или не адресовать вообще.
Поскольку указатель может содержать, а может и не содержать адрес какого-либо объекта, перед его использованием функция должна проверить, не равен ли он нулю:
|
class X; void manip( X *px ) { // проверим на 0 перед использованием if ( px != 0 ) // обратимся к объекту по адресу... |
}
Параметр-ссылка не нуждается в этой проверке, так как всегда существует именуемый ею объект. Например:
|
class Type { }; void operate( const Type& p1, const Type& p2 ); int main() { Type obj1; // присвоим objl некоторое значение // ошибка: ссылка не может быть равной 0 Type obj2 = operate( objl, 0 ); |
}
Если параметр должен ссылаться на разные объекты во время выполнения функции или принимать нулевое значение (ни на что не ссылаться), нам следует использовать указатель.
Одна из важнейших сфер применения параметров-ссылок – эффективная реализация перегруженных операций. При этом использование операций остается простым и интуитивно понятным. (Подробнее данный вопрос рассматривается в главе 15.) Разберем маленький пример. Представим себе класс Matrix (матрица). Хорошо бы реализовать операции сложения и присваивания “привычным” способом:
|
Matrix a, b, c; |
c = a + b;
Эти операции реализуются с помощью перегруженных операторов – функций с немного необычным именем. Для оператора сложения такая функция будет называться operator+. Посмотрим, как ее определить:
|
Matrix // тип возврата - Matrix operator+( // имя перегруженного оператора Matrix m1, // тип левого операнда Matrix m2 // тип правого операнда ) { Matrix result; // необходимые действия return result; |
// реализация с параметрами-указателями operator+( Matrix *ml, Matrix *m2 ) { Matrix result; // необходимые действия return result; |
// а вот это не работает // &a + &b возвращает объект типа Matrix |
// правильно: работает, однако ... |
// реализация с параметрами-ссылками operator+( const Matrix &m1, const Matrix &m2 ) { Matrix result; // необходимые действия return result; |
Нередко приходится определять переменную, которая принимает значения из некоего набора. Скажем, файл открывают в любом из трех режимов: для чтения, для записи, для добавления.
Конечно, можно определить три константы для обозначения этих режимов:
|
const int input = 1; const int output = 2; |
const int append = 3;
и пользоваться этими константами:
|
bool open_file( string file_name, int open_mode); // ... |
open_file( "Phoenix_and_the_Crane", append );
Подобное решение допустимо, но не вполне приемлемо, поскольку мы не можем гарантировать, что аргумент, передаваемый в функцию open_file() равен только 1, 2 или 3.
Использование перечислимого типа решает данную проблему. Когда мы пишем:
enum open_modes{ input = 1, output, append };
мы определяем новый тип open_modes. Допустимые значения для объекта этого типа ограничены набором 1, 2 и 3, причем каждое из указанных значений имеет мнемоническое имя. Мы можем использовать имя этого нового типа для определения как объекта данного типа, так и типа формальных параметров функции:
void open_file( string file_name, open_modes om );
input, output и append являются элементами перечисления. Набор элементов перечисления задает допустимое множество значений для объекта данного типа. Переменная типа open_modes (в нашем примере) инициализируется одним из этих значений, ей также может быть присвоено любое из них. Например:
open_file( "Phoenix and the Crane", append );
Попытка присвоить переменной данного типа значение, отличное от одного из элементов перечисления (или передать его параметром в функцию), вызовет ошибку компиляции. Даже если попробовать передать целое значение, соответствующее одному из элементов перечисления, мы все равно получим ошибку:
|
// ошибка: 1 не является элементом перечисления open_modes |
open_file( "Jonah", 1 );
Есть способ определить переменную типа open_modes, присвоить ей значение одного из элементов перечисления и передать параметром в функцию:
|
open_modes om = input; // ... |
cout << open_modes_table[ input ] << " " |
// не поддерживается for ( open_modes iter = input; iter != append; ++inter ) |
// shape == 0, sphere == 1, cylinder == 2, polygon == 3 |
// point2d == 2, point2w == 3, point3d == 3, point3w == 4 |
void mumble() { Points pt3d = point3d; // правильно: pt2d == 3 // ошибка: pt3w инициализируется типом int Points pt3w = 3; // ошибка: polygon не входит в перечисление Points pt3w = polygon; // правильно: оба объекта типа Points pt3w = pt3d; |
const int array_size = 1024; // правильно: pt2w преобразуется int |
Функции используют память из стека программы. Некоторая область стека отводится функции и остается связанной с ней до окончания ее работы, по завершении которой отведенная ей память освобождается и может быть занята другой функцией. Иногда эту часть стека называют областью активации.
Каждому параметру функции отводится место в данной области, причем его размер определяется типом параметра. При вызове функции память инициализируется значениями фактических аргументов.
Стандартным способом передачи аргументов является копирование их значений, т.е. передача по значению. При этом способе функция не получает доступа к реальным объектам, являющихся ее аргументами. Вместо этого она получает в стеке локальные копии этих объектов. Изменение значений копий никак не отражается на значениях самих объектов. Локальные копии теряются при выходе из функции.
Значения аргументов при передаче по значению не меняются. Следовательно, программист не должен заботиться о сохранении и восстановлении их значений при вызове функции. Без этого механизма любой вызов мог бы привести к нежелательному изменению аргументов, не объявленных константными явно. Передача по значению освобождает человека от лишних забот в наиболее типичной ситуации.
Однако такой способ передачи аргументов может не устраивать нас в следующих случаях:
· передача большого объекта типа класса. Временные и пространственные расходы на размещение и копирование такого объекта могут оказаться неприемлемыми для реальной программы;
· иногда значения аргументов должны быть модифицированы внутри функции. Например, swap()
должна обменять значения своих аргументов, что невозможно при передаче по значению:
|
// swap() не меняет значений своих аргументов! void swap( int vl, int v2 ) { int tmp = v2; v2 = vl; vl = tmp; |
#include <iostream> void swap( int, int ); int main() { int i = 10; int j = 20; cout << "Перед swap():\ti: " << i << "\tj: " << j << endl; swap( i, j ); cout << "После swap():\ti: " << i << "\tj: " << j << endl; return 0; |
// pswap() обменивает значения объектов, // адресуемых указателями vl и v2 void pswap( int *vl, int *v2 ) { int tmp = *v2; *v2 = *vl; *vl = tmp; |
// rswap() обменивает значения объектов, // на которые ссылаются vl и v2 void rswap( int &vl, int &v2 ) { int tmp = v2; v2 = vl; vl = tmp; |
Различные функции программы могут общаться между собой с помощью двух механизмов. (Под словом “общаться” мы подразумеваем обмен данными.) В одном случае используются глобальные объекты, в другом – передача параметров и возврат значений.
Глобальный объект определен вне функции. Например:
|
int glob; int main() { // что угодно |
}
Объект glob
является глобальным. (В главе 8 рассмотрение глобальных объектов и глобальной области видимости будет продолжено.) Главное достоинство и одновременно один из наиболее заметных недостатков такого объекта – доступность из любого места программы, поэтому его обычно используют для общения между разными модулями. Обратная сторона медали такова:
· функции, использующие глобальные объекты, зависят от этих объектов и их типов. Использовать такую функцию в другом контексте затруднительно;
· при модификации такой программы повышается вероятность ошибок. Даже для внесения локальных изменений необходимо понимание всей программы в целом;
· если глобальный объект получает неверное значение, ошибку нужно искать по всей программе. Отсутствует локализация;
· используя глобальные объекты, труднее писать рекурсивные функции (Рекурсия возникает тогда, когда функция вызывает сама себя. Мы рассмотрим это в разделе 7.5.);
· если используются потоки (threads), то для синхронизации доступа к глобальным объектам требуется писать дополнительный код. Отсутствие синхронизации – одна из распространенных ошибок при использовании потоков. (Пример использования потоков при программировании на С++ см. в статье “Distributing Object Computing in C++” (Steve Vinoski and Doug Schmidt) в [LIPPMAN96b].)
vector<string> &readText( ) { vector<string> text; string word; while ( cin >> word ) { text.push_back( word ); // ... } // .... return text; |
Итак, мы уже знаем, как объявлять, определять и использовать функции в программах. В этой главе речь пойдет об их специальном виде– перегруженных функциях. Две функции называются перегруженными, если они имеют одинаковое имя, объявлены в одной и той же области видимости, но имеют разные списки формальных параметров. Мы расскажем, как объявляются такие функции и почему они полезны. Затем мы рассмотрим вопрос об их разрешении, т.е. о том, какая именно из нескольких перегруженных функций вызывается во время выполнения программы. Эта проблема является одной из наиболее сложных в C++. Тем, кто хочет разобраться в деталях, будет интересно прочитать два раздела в конце главы, где тема преобразования типов аргументов и разрешения перегруженных функций раскрывается более подробно.
В главе 15 мы рассмотрим два вида специальных функций: перегруженные операторы и определенные пользователем преобразования. Они дают возможность употреблять объекты классов в выражениях так же интуитивно, как и объекты встроенных типов. В этой главе мы сначала изложим общие концепции проектирования перегруженных операторов. Затем представим понятие друзей класса со специальными правами доступа и обсудим, зачем они применяются, обратив особое внимание на то, как реализуются некоторые перегруженные операторы: присваивание, взятие индекса, вызов, стрелка для доступа к члену класса, инкремент и декремент, а также специализированные для класса операторы new и delete. Другая категория специальных функций, которая рассматривается в этой главе, – это функции преобразования членов (конвертеры), составляющие набор стандартных преобразований для типа класса. Они неявно применяются компилятором, когда объекты классов используются в качестве фактических аргументов функции или операндов встроенных или перегруженных операторов. Завершается глава развернутым изложением правил разрешения перегрузки функций с учетом передачи объектов в качестве аргументов, функций-членов класса и перегруженных операторов.
Все перегруженные функции объявляются в одной и той же области видимости. К примеру, локально объявленная функция не перегружает, а просто скрывает глобальную:
|
#include <string> void print( const string & ); void print( double ); // перегружает print() void fooBar( int ival ) { // отдельная область видимости: скрывает обе реализации print() extern void print( int ); // ошибка: print( const string & ) не видна в этой области print( "Value: "); print( ival ); // правильно: print( int ) видна |
}
Поскольку каждый класс определяет собственную область видимости, функции, являющиеся членами двух разных классов, не перегружают друг друга. (Функции-члены класса описываются в главе 13. Разрешение перегрузки для функций-членов класса рассматривается в главе 15.)
Объявлять такие функции разрешается и внутри пространства имен. С каждым из них также связана отдельная область видимости, так что функции, объявленные в разных пространствах, не перегружают друг друга. Например:
|
#include <string> namespace IBM { extern void print( const string & ); extern void print( double ); // перегружает print() } namespace Disney { // отдельная область видимости: // не перегружает функцию print() из пространства имен IBM extern void print( int ); |
}
Использование using-объявлений и using-директив помогает сделать члены пространства имен доступными в других областях видимости. Эти механизмы оказывают определенное влияние на объявления перегруженных функций. (Using-объявления и using-директивы рассматривались в разделе 8.6.)
Каким образом using-объявление сказывается на перегрузке функций? Напомним, что оно вводит псевдоним для члена пространства имен в ту область видимости, в которой это объявление встречается. Что делают такие объявления в следующей программе?
|
namespace libs_R_us { int max( int, int ); int max( double, double ); extern void print( int ); extern void print( double ); } // using-объявления using libs_R_us::max; using libs_R_us::print( double ); // ошибка void func() { max( 87, 65 ); // вызывает libs_R_us::max( int, int ) |
#include <string> namespace libs_R_us { extern void print( int ); extern void print( double ); } extern void print( const string & ); // libs_R_us::print( int ) и libs_R_us::print( double ) // перегружают print( const string & ) using libs_R_us::print; void fooBar( int ival ) { print( "Value: "); // вызывает глобальную функцию // print( const string & ) print( ival ); // вызывает libs_R_us::print( int ) |
namespace libs_R_us { void print( int ); void print( double ); } void print( int ); using libs_R_us::print; // ошибка: повторное объявление print(int) void fooBar( int ival ) { print( ival ); // какая print? ::print или libs_R_us::print |
#include <string> namespace libs_R_us { extern void print( int ); extern void print( double ); } extern void print( const string & ); // using-директива // print(int), print(double) и print(const string &) - элементы // одного и того же множества перегруженных функций using namespace libs_R_us; void fooBar( int ival ) { print( "Value: "); // вызывает глобальную функцию // print( const string & ) print( ival ); // вызывает libs_R_us::print( int ) |
namespace IBM { int print( int ); } namespace Disney { double print( double ); |
// using-директива // формируется множество перегруженных функций из различных // пространств имен using namespace IBM; using namespace Disney; long double print(long double); int main() { print(1); // вызывается IBM::print(int) print(3.1); // вызывается Disney::print(double) return 0; |
Перегрузка оператора ввода (>>) похожа на перегрузку оператора вывода, но, к сожалению, возможностей для ошибок гораздо больше. Вот, например, его реализация для класса WordCount:
|
#include <iostream> #include "WordCount.h" /* необходимо модифицировать определение класса WordCount, чтобы оператор ввода был другом class WordCount { friend ostream& operator<<( ostream&, const WordCount& ); friend istream& operator>>( istream&, const WordCount& ); */ istream& operator >>( istream &is, WordCount &wd ) { /* формат хранения объекта WordCount: * <2> строка * <7,3> <12,36> */ int ch; /* прочитать знак '<'. Если его нет, * перевести поток в ошибочное состояние и выйти */ if ((ch = is.get()) != '<' ) { // is.setstate( ios_base::badbit ); return is; } // прочитать длину int occurs; is >> occurs; // читать до обнаружения >; ошибки не контролируются while ( is && (ch = is.get()) != '>' ) ; is >> wd._word; // прочитать позиции вхождений; // каждая позиция имеет формат: < строка, колонка > for ( int ix = 0; ix < occurs; ++ix ) { int line, col; // извлечь значения while (is && (ch = is.get())!= '<' ) ; is >> line; while (is && (ch = is.get())!= ',' ) ; is >> col; while (is && (ch = is.get())!= '>' ) ; wd._occurList.push_back( Location( line, col )); } return is; |
}
На этом примере показан целый ряд проблем, имеющих отношение к возможным ошибочным состояниям входного потока:
· поток, чтение из которого невозможно из-за неправильного формата, переводится в состояние fail:
// проверить, находится ли поток "is" в "хорошем" состоянии |
#include <iostream> #include "WordCount.h" int main() { WordCount readIn; // operator>>( cin, readIn ) cin >> readIn; if ( !cin ) { cerr << "Ошибка ввода WordCount" << endl; return -1; } // operator<<( cout, readIn ) cout << readIn << endl; |
Если мы хотим, чтобы наш тип класса поддерживал операции ввода/вывода, то необходимо перегрузить оба соответствующих оператора. В этом разделе мы рассмотрим, как перегружается оператор вывода. (Перегрузка оператора ввода – тема следующего раздела.) Например, для класса WordCount он выглядит так:
|
class WordCount { friend ostream& operator<<( ostream&, const WordCount& ); public: WordCount( string word, int cnt=1 ); // ... private: string word; int occurs; }; ostream& operator <<( ostream& os, const WordCount& wd ) { // формат: <счетчик> слово os << "< " << " > " > " << wd.word; return os; |
}
Проектировщик должен решить, следует ли выводить завершающий символ новой строки. Лучше этого не делать: поскольку операторы вывода для встроенных типов такой символ не печатают, пользователь ожидает аналогичного поведения и от операторов в других классах. Определенный нами в классе WordCount оператор вывода можно использовать вместе с любыми другими операторами:
|
#include <iostream> #include "WordCount.h" int main() { WordCount wd( "sadness", 12 ); cout << "wd:\n" << wd << endl; return 0; |
}
Программа печатает на терминале строки:
wd:
<12> sadness
Оператор вывода – это бинарный оператор, который возвращает ссылку на объект класса ostream. В общем случае структура определения перегруженного оператора вывода выглядит так:
|
// структура перегруженного оператора вывода ostream& operator <<( ostream& os, const ClassType &object ) { // произвольный код для подготовки объекта // фактическое число членов os << // ... // возвращается объект ostream return os; |
}
Первый его аргумент – это ссылка на объект ostream, а второй – ссылка (обычно константная) на объект некоторого класса. Возвращается ссылка на ostream. Значением всегда является объект ostream, для которого оператор вызывался.
#include <iostream> class Location { friend ostream& operator<<( ostream&, const Location& ); private: short _line; short _col; }; ostream& operator <<( ostream& os, const Location& lc ) { // объект Loc выводится в виде: < 10,37 > os << "<" << lc._line << "," << lc._col << "> "; return os; |
#include <vector> #include <string> #include <iostream> #include "Location.h" class WordCount { friend ostream& operator<<( ostream&, const WordCount& ); public: WordCount() {} WordCount( const string &word ) : _word( word ) {} WordCount( const string &word, int ln, int col ) : _word( word ){ insert_location( ln, col ); } string word() const { return _word; } int occurs() const { return _occurList.size(); } void found( int ln, int col ) { insert_location( ln, col ); } private: void insert_location( int ln, int col ) { _occurList.push_back( Location( ln, col )); } string _word; vector< Location > _occurList; |
ostream& operator <<( ostream& os, const WordCount& wd ) { os << "<" << wd._occurList.size() << "> " << wd._word << endl; int cnt = 0, onLine = 6; vector< Location >::const_iterator first = wd._occurList.begin(); vector< Location >::const_iterator last = wd._occurList.end(); for ( ; first != last; ++first ) { // os << Location os << *first << " "; // форматирование: по 6 в строке if ( ++cnt >= onLine ) { os << "\n"; cnt = 0; } } return os; |
int main() { WordCount search( "rosebud" ); // для простоты явно введем 8 вхождений search.found(11,3); search.found(11,8); search.found(14,2); search.found(34,6); search.found(49,7); search.found(67,5); search.found(81,2); search.found(82,3); search.found(91,4); search.found(97,8); cout << "Вхождения: " << "\n" << search << endl; return 0; |
class Date { public: // ... private: int month, day, year; |
class CheckoutRecord { // запись о выдаче public: // ... private: double book_id; // идентификатор книги string title; // название Date date_borrowed; // дата выдачи Date date_due; // дата возврата pair<string,string> borrower; // кому выдана vector pair<string,string> wait_list; // очередь на книгу |
В предыдущих главах мы уже показывали, что перегрузка операторов позволяет программисту вводить собственные версии предопределенных операторов (см. главу 4) для операндов типа классов. Например, в классе String из раздела 3.15 задано много перегруженных операторов. Ниже приведено его определение:
|
#include <iostream> class String; istream& operator>>( istream &, const String & ); ostream& operator<<( ostream &, const String & ); class String { public: // набор перегруженных конструкторов // для автоматической инициализации String( const char* = 0 ); String( const String & ); // деструктор: автоматическое уничтожение ~String(); // набор перегруженных операторов присваивания String& operator=( const String & ); String& operator=( const char * ); // перегруженный оператор взятия индекса char& operator[]( int ); // набор перегруженных операторов равенства // str1 == str2; bool operator==( const char * ); bool operator==( const String & ); // функции доступа к членам int size() { return _size; }; char * c_str() { return _string; } private: int _size; char *_string; |
};
В классе String
есть три набора перегруженных операторов. Первый – это набор операторов присваивания:
|
// набор перегруженных операторов присваивания String& operator=( const String & ); |
String& operator=( const char * );
Сначала идет копирующий оператор присваивания. (Подробно они обсуждались в разделе 14.7.) Следующий оператор поддерживает присваивание C-строки символов объекту типа String:
|
String name; |
name = "Sherlock"; // использование оператора operator=( char * )
(Операторы присваивания, отличные от копирующих, мы рассмотрим в разделе 15.3.)
// перегруженный оператор взятия индекса |
if ( name[0] != 'S' ) |
// набор перегруженных операторов равенства // str1 == str2; bool operator==( const char * ); |
#include "String.h" int main() { String name1 "Sherlock"; String name2 "Holmes"; name1 += " "; name1 += name2; if (! ( name1 == "Sherlock Holmes" ) ) cout << "конкатенация не сработала\n"; |
class String { public: // набор перегруженных операторов += String& operator+=( const String & ); String& operator+=( const char * ); // ... private: // ... |
#include <cstring> inline String& String::operator+=( const String &rhs ) { // Если строка, на которую ссылается rhs, непуста if ( rhs._string ) { String tmp( *this ); // выделить область памяти, достаточную // для хранения конкатенированных строк _size += rhs._size; delete [] _string; _string = new char[ _size + 1 ]; // сначала скопировать в выделенную область исходную строку // затем дописать в конец строку, на которую ссылается rhs strcpy( _string, tmp._string ); strcpy( _string + tmp._size, rhs._string ); } return *this; } inline String& String::operator+=( const char *s ) { // Если указатель s ненулевой if ( s ) { String tmp( *this ); // выделить область памяти, достаточную // для хранения конкатенированных строк _size += strlen( s ); delete [] _string; _string = new char[ _size + 1 ]; // сначала скопировать в выделенную область исходную строку // затем дописать в конец C-строку, на которую ссылается s strcpy( _string, tmp._string ); strcpy( _string + tmp._size, s ); } return *this; |
Шаблон функции может быть перегружен. В следующем примере есть три перегруженных объявления для шаблона min():
|
// определение шаблона класса Array // (см. раздел 2.4) template <typename Type> class Array( /* ... */ }; // три объявления шаблона функции min() template <typename Type> Type min( const Array<Type>&, int ); // #1 template <typename Type> Type min( const Type*, int ); // #2 template <typename Type> |
Type min( Type, Type ); // #3
Следующее определение main()
иллюстрирует, как могут вызываться три объявленных таким образом функции:
|
#include <cmath> int main() { Array<int> iA(1024); // конкретизация класса int ia[1024]; // Type == int; min( const Array<int>&, int ) int ival0 = min( iA, 1024 ); // Type == int; min( const int*, int ) int ival1 = min( ia, 1024 ); // Type == double; min( double, double ) double dval0 = min( sqrt( iA[0] ), sqrt( ia[0] ) ); return 0; |
}
Разумеется, тот факт, что три перегруженных шаблона функции успешно объявлены, не означает, что они могут быть также успешно вызваны. Такие шаблоны могут приводить к неоднозначности при вызове конкретизированного шаблона. Например, для следующего определения шаблона min5()
|
template <typename T> |
int min5( T, T ) { /* ... */ }
функция не конкретизируется по шаблону, если min5()
вызывается с аргументами разных типов; при этом процесс вывода заканчивается с ошибкой, поскольку из фактических аргументов функции выводятся два разных типа для T.
|
int i; unsigned int ui; // правильно: для T выведен тип int min5( 1024, i ); // вывод аргументов шаблона заканчивается с ошибкой: // для T можно вывести два разных типа |
min5 ( i, ui );
Для разрешения второго вызова можно было бы перегрузить min5(), допустив два различных типа аргументов:
|
template <typename T, typename U> |
// правильно: int min5( int, usigned int ) |
// ошибка: неоднозначность: две возможных конкретизации // из min5( T, T ) и min5( T, U ) |
// правильно: конкретизация из min5( T, U ) |
template <typename Type> Type sum( Type*, int ); template <typename Type> Type sum( Type, int ); int ia[1024]; // Type == int ; sum<int>( int*, int ); или // Type == int*; sum<int*>( int*, int ); ?? |
В языке 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. Для обработки исключения выбирается тот обработчик, для которого типы в объявлении исключения и в возбужденном исключении совпадают. (В главе 19 мы увидим, что типы не обязаны совпадать точно: обработчик для базового класса подходит и для исключений с производными классами.) Например, когда функция-член pop()
класса 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(), возбудившую исключение.
Иногда функции нужно выполнить определенное действие до того, как она завершит обработку исключения, даже несмотря на то, что обработать его она не может. К примеру, функция захватила некоторый ресурс, скажем открыла файл или выделила память из хипа, и этот ресурс необходимо освободить перед выходом:
|
void manip() { resource res; res.lock(); // захват ресурса // использование ресурса // действие, в результате которого возбуждено исключение res.release(); // не выполняется, если возбуждено исключение |
}
Если исключение возбуждено, то управление не попадет на инструкцию, где ресурс освобождается. Чтобы освободить ресурс, не пытаясь перехватить все возможные исключения (тем более, что мы не всегда знаем, какие именно исключения могут возникнуть), воспользуемся специальной конструкцией, позволяющей перехватывать любые исключения. Это не что иное, как предложение catch, в котором объявление исключения имеет вид (...) и куда управление попадает при любом исключении. Например:
|
// управление попадает сюда при любом возбужденном исключении catch (...) { // здесь размещаем наш код |
}
Конструкция catch(...)
используется в сочетании с повторным возбуждением исключения. Захваченный ресурс освобождается внутри составной инструкции в catch-обработчике перед тем, как передать исключение по цепочке вложенных вызовов в результате повторного возбуждения:
|
void manip() { resource res; res.lock(); try { // использование ресурса // действие, в результате которого возбуждено исключение } catch (...) { res.release(); throw; } res.release(); // не выполняется, если возбуждено исключение |
}
Чтобы гарантировать освобождение ресурса в случае, когда выход из manip()
происходит в результате исключения, мы освобождаем его внутри catch(...) до того, как исключение будет передано дальше. Можно также управлять захватом и освобождением ресурса путем инкапсуляции в класс всей работы с ним. Тогда захват будет реализован в конструкторе, а освобождение – в автоматически вызываемом деструкторе. (С этим подходом мы познакомимся в главе 19.)
try { stack.display(); for ( int ix = 1; ix < 51; ++x ) { // то же, что и выше } } catch ( pushOnFull ) { } catch ( popOnEmpty ) { } |
(a) class exceptionType { }; catch( exceptionType *pet ) { } (b) catch(...) { } (c) enum mathErr { overflow, underflow, zeroDivide }; catch( mathErr &ref ) { } (d) typedef int EXCPTYPE; |