Механизм классов позволяет создавать новые типы данных; с его помощью введены типы string, vector, complex и pair, рассмотренные выше. В главе 2 мы рассказывали о концепциях и механизмах, поддерживающих объектный и объектно-ориентированный подход, на примере реализации класса Array. Здесь мы, основываясь на объектном подходе, создадим простой класс String, реализация которого поможет понять, в частности, перегрузку операций – мы говорили о ней в разделе 2.3. (Классы подробно рассматриваются в главах 13, 14 и 15. Мы дали краткое описание класса для того, чтобы приводить более интересные примеры. Читатель, только начинающий изучение С++, может пропустить этот раздел и подождать более систематического описания классов в следующих главах.)
Наш класс String
должен поддерживать инициализацию объектом класса String, строковым литералом и встроенным строковым типом, равно как и операцию присваивания ему значений этих типов. Мы используем для этого конструкторы класса и перегруженную операцию присваивания. Доступ к отдельным символам String
будет реализован как перегруженная операция взятия индекса. Кроме того, нам понадобятся: функция size() для получения информации о длине строки; операция сравнения объектов типа String и объекта String со строкой встроенного типа; а также операции ввода/вывода нашего объекта. В заключение мы реализуем возможность доступа к внутреннему представлению нашей строки в виде строки встроенного типа.
Определение класса начинается ключевым словом class, за которым следует идентификатор – имя класса, или типа. В общем случае класс состоит из секций, предваряемых словами public (открытая) и private
(закрытая). Открытая секция, как правило, содержит набор операций, поддерживаемых классом и называемых методами или функциями-членами класса. Эти функции-члены определяют открытый интерфейс класса, другими словами, набор действий, которые можно совершать с объектами данного класса. В закрытую секцию обычно включают данные-члены, обеспечивающие внутреннюю реализацию. В нашем случае к внутренним членам относятся _string – указатель на char, а также _size
#inc1ude <iostream> class String; istream& operator>>( istream&, String& ); ostream& operator<<( ostream&, const String& ); class String { public: // набор конструкторов // для автоматической инициализации // String strl; // String() // String str2( "literal" ); // String( const char* ); // String str3( str2 ); // String( const String& ); String(); String( const char* ); String( const String& ); // деструктор ~String(); // операторы присваивания // strl = str2 // str3 = "a string literal" String& operator=( const String& ); String& operator=( const char* ); // операторы проверки на равенство // strl == str2; // str3 == "a string literal"; bool operator==( const String& ); bool operator==( const char* ); // перегрузка оператора доступа по индексу // strl[ 0 ] = str2[ 0 ]; char& operator[]( int ); // доступ к членам класса int size() { return _size; } char* c_str() { return _string; } private: int _size; char *_string; |
String object("Danny"); String *ptr = new String ("Anna"); |
vector<int> sizes( 3 ); // доступ к члену для objects (.); // objects имеет размер 5 sizes[ 0 ] = object.size(); // доступ к члену для pointers (->) // ptr имеет размер 4 sizes[ 1 ] = ptr->size(); // доступ к члену (.) // array[0] имеет размер 0 |
String namel( "Yadie" ); String name2( "Yodie" ); // bool operator==(const String&) if ( namel == name2 ) return; else // String& operator=( const String& ) |
// содержимое исходного файла: String.С // включение определения класса String #inc1ude "String.h" // включение определения функции strcmp() #inc1ude <cstring> bool // тип возвращаемого значения String:: // класс, которому принадлежит функция operator== // имя функции: оператор равенства (const String &rhs) // список параметров { if ( _size != rhs._size ) return false; return strcmp( _strinq, rhs._string ) ? false : true; |
inline bool String::operator==(const String &rhs) { // то же самое |
inline bool String::operator==(const char *s) { return strcmp( _string, s ) ? false : true; |
#include <cstring> // default constructor inline String::String() { _size = 0; _string = 0; } inline String::String( const char *str ) { if ( ! str ) { _size = 0; _string = 0; } else { _size = str1en( str ); _string = new char[ _size + 1 ]; strcpy( _string, str ); } // copy constructor inline String::String( const String &rhs ) { size = rhs._size; if ( ! rhs._string ) _string = 0; else { _string = new char[ _size + 1 ]; strcpy( _string, rhs._string ); } |
String namel( "orville" ), name2( "wilbur" ); |
ptr->size(); |
inline String& String::operator=( const char *s ) { if ( ! s ) { _size = 0; delete [] _string; _string = 0; } else { _size = str1en( s ); delete [] _string; _string = new char[ _size + 1 ]; strcpy( _string, s ); } |
inline String& String::operator=( const String &rhs ) { // в выражении // namel = *pointer_to_string // this представляет собой name1, // rhs - *pointer_to_string. |
inline String& String::operator=( const String &rhs ) { if ( this != &rhs ) { delete [] _string; _size = rhs._size; if ( ! rhs._string ) _string = 0; else { _string = new char[ _size + 1 ]; strcpy( _string, rhs._string ); } } return *this; |
#include <cassert> inline char& String::operator[] ( int elem ) { assert( elem >= 0 && elem < _size ); return _string[ elem ]; |
#include <iomanip> inline istream& operator>>( istream &io, String &s ) { // искусственное ограничение: 4096 символов const int 1imit_string_size = 4096; char inBuf[ limit_string_size ]; // setw() входит в библиотеку iostream // он ограничивает размер читаемого блока до 1imit_string_size-l io >> setw( 1imit_string_size ) >> inBuf; s = mBuf; // String::operator=( const char* ); return io; |
inline ostream& operator<<( ostream& os, const String &s ) { return os << s.c_str(); |
#include <iostream> #inc1ude "String.h" int main() { int aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0, theCnt = 0, itCnt = 0, wdCnt = 0, notVowel = 0; // Слова "The" и "It" // будем проверять с помощью operator==( const char* ) String but, the( "the" ), it( "it" ); // operator>>( ostream&, String& ) while ( cin >> buf ) { ++wdCnt; // operator<<( ostream&, const String& ) cout << buf << ' '; if ( wdCnt % 12 == 0 ) cout << endl; // String::operator==( const String& ) and // String::operator==( const char* ); if ( buf == the | | buf == "The" ) ++theCnt; else if ( buf == it || buf == "It" ) ++itCnt; // invokes String::s-ize() for ( int ix =0; ix < buf.sizeO; ++ix ) { // invokes String:: operator [] (int) switch( buf[ ix ] ) { case 'a': case 'A': ++aCnt; break; case 'e': case 'E': ++eCnt; break; case 'i': case 'I': ++iCnt; break; case 'o': case '0': ++oCnt; break; case 'u': case 'U': ++uCnt; break; default: ++notVowe1; break; } } } // operator<<( ostream&, const String& ) cout << "\n\n" << "Слов: " << wdCnt << "\n\n" << "the/The: " << theCnt << '\n' << "it/It: " << itCnt << "\n\n" << "согласных: " < <notVowel << "\n\n" << "a: " << aCnt << '\n' << "e: " << eCnt << '\n' << "i: " << ICnt << '\n' << "o: " << oCnt << '\n' << "u: " << uCnt << endl; |
class String { public: // ... int count( char ch ) const; // ... |
class String { public: // ... String operator+( const String &rhs ) const; // ... |
Разрешением перегрузки функции называется процесс выбора той функции из множества перегруженных, которую следует вызвать. Этот процесс основывается на указанных при вызове аргументах. Рассмотрим пример:
|
T t1, t2; void f( int, int ); void f( float, float ); int main() { f( t1, t2 ); return 0; |
}
Здесь в ходе процесса разрешения перегрузки в зависимости от типа T
определяется, будет ли при обработке выражения f(t1,t2)
вызвана функция f(int,int) или f(float,float) или зафиксируется ошибка.
Разрешение перегрузки функции – один и самых сложных аспектов языка C++. Пытаясь разобраться во всех деталях, начинающие программисты столкнутся с серьезными трудностями. Поэтому в данном разделе мы представим лишь краткий обзор того, как происходит разрешение перегрузки, чтобы у вас составилось хоть какое-то впечатление об этом процессе. Для тех, кто хочет узнать больше, в следующих двух разделах приводится более подробное описание.
Процесс разрешения перегрузки функции состоит из трех шагов, которые мы покажем на следующем примере:
|
void f(); void f( int ); void f( double, double = 3.4 ); void f( char *, char * ); void main() { f( 5.6 ); return 0; |
}
При разрешении перегрузки функции выполняются следующие шаги:
1. Выделяется множество перегруженных функций для данного вызова, а также свойства списка аргументов, переданных функции.
2. Выбираются те из перегруженных функций, которые могут быть вызваны с данными аргументами, с учетом их количества и типов.
3. Находится функция, которая лучше всего соответствует вызову.
Рассмотрим последовательно каждый пункт.
На первом шаге необходимо идентифицировать множество перегруженных функций, которые будут рассматриваться при данном вызове. Вошедшие в это множество функции называются кандидатами. Функция-кандидат – это функция с тем же именем, что и вызванная, причем ее объявление видимо в точке вызова. В нашем примере есть четыре таких кандидата: f(), f(int), f(double, double) и f(char*, char*).
В нашей программе тестируется определенный в предыдущем разделе класс iStack и его функции-члены pop() и push(). Выполняется 50 итераций цикла for. На каждой итерации в стек помещается значение, кратное 3: 3, 6, 9 и т.д. Если значение кратно 4 (4, 8, 12...), то выводится текущее содержимое стека, а если кратно 10 (10, 20, 30...), то с вершины снимается один элемент, после чего содержимое стека выводится снова. Как нужно изменить функцию main(), чтобы она обрабатывала исключения, возбуждаемые функциями-членами класса iStack?
|
#include <iostream> #include "iStack.h" int main() { iStack stack( 32 ); stack.display(); for ( int ix = 1; ix < 51; ++ix ) { if ( ix % 3 == 0 ) stack.push( ix ); if ( ix % 4 == 0 ) stack.display(); if ( ix % 10 == 0 ) { int dummy; stack.pop( dummy ); stack.display(); } } return 0; |
}
Инструкции, которые могут возбуждать исключения, должны быть заключены в try-блок. Такой блок начинается с ключевого слова try, за которым идет последовательность инструкций, заключенная в фигурные скобки, а после этого – список обработчиков, называемых catch-предложениями. Try-блок группирует инструкции программы и ассоциирует с ними обработчики исключений. Куда нужно поместить try-блоки в функции main(), чтобы были обработаны исключения popOnEmpty и pushOnFull?
|
for ( int ix = 1; ix < 51; ++ix ) { try { // try-блок для исключений pushOnFull if ( ix % 3 == 0 ) stack.push( ix ); } catch ( pusOnFull ) { ... } if ( ix % 4 == 0 ) stack.display(); try { // try-блок для исключений popOnEmpty if ( ix % 10 == 0 ) { int dummy; stack.pop( dummy ); stack.display(); } } catch ( popOnEmpty ) { ... } |
}
try { for ( int ix = 1; ix < 51; ++ix ) { if ( ix % 3 == 0 ) stack.push( ix ); if ( ix % 4 == 0 ) stack.display(); if ( ix % 10 == 0 ) { int dummy; stack.pop( dummy ); stack.display(); } } } catch ( pushOnFull ) { ... } |
int main() { try { iStack stack( 32 ); // правильно: объявление внутри try-блока stack.display(); for ( int ix = 1; ix < 51; ++ix ) { // то же, что и раньше } } catch ( pushOnFull ) { // здесь к переменной stack обращаться нельзя } catch ( popOnEmpty ) { // здесь к переменной stack обращаться нельзя } // и здесь к переменной stack обращаться нельзя return 0; |
int main() try { iStack stack( 32 ); // правильно: объявление внутри try-блока stack.display(); for ( int ix = 1; ix < 51; ++ix ) { // то же, что и раньше } return 0; } catch ( pushOnFull ) { // здесь к переменной stack обращаться нельзя } catch ( popOnEmpty ) { // здесь к переменной stack обращаться нельзя |
В общем случае удаление осуществляется двумя формами функции-члена erase(). Первая форма удаляет единственный элемент, вторая– диапазон, отмеченный парой итераторов. Для последнего элемента можно воспользоваться функцией-членом pop_back().
При вызове erase() параметром является итератор, указывающий на нужный элемент. В следующем фрагменте кода мы воспользуемся обобщенным алгоритмом find() для нахождения элемента и, если он найден, передадим его адрес функции-члену erase().
|
string searchValue( "Quasimodo" ); list< string >::iterator iter = find( slist.begin(), slist.end(), searchValue ); if ( iter != slist.end() ) |
slist.erase( iter );
Для удаления всех элементов контейнера или некоторого диапазона можно написать следующее:
|
// удаляем все элементы контейнера slist.erase( slist.begin(), slist.end() ); // удаляем элементы, помеченные итераторами list< string >::iterator first, last; first = find( slist. begin(), slist.end(), vail ); last = find( slist.begin(), slist.end(), va12 ); // ... проверка first и last |
slist.erase( first, last );
Парной по отношению к push_back()
является функция-член pop_back(), удаляющая из контейнера последний элемент, не возвращая его значения:
|
vector< string >::iterator iter = buffer.begin(); for ( ; iter != buffer.end(), iter++ ) { slist.push_back( *iter ); if ( ! do_something( slist )) slist.pop_back(); |
}
Существуют три формы функции-члена erase() для удаления элементов отображения. Для единственного элемента используется erase() с ключом или итератором в качестве аргумента, а для последовательности эта функция вызывается с двумя итераторами. Например, мы могли бы позволить удалять элементы из text_map
таким образом:
|
string removal_word; cout << "введите удаляемое слово: "; cin >> removal_word; if ( text_map->erase( remova1_word )) cout << "ok: " << remova1_word << " удалено\n"; |
else cout << "увы: " << remova1_word << " не найдено!\n";
Альтернативой является проверка: действительно ли слово содержится в text_map?
|
map<string,loc*>::iterator where; where = text_map.find( remova1_word ); if ( where == text_map->end() ) cout << "увы: " << remova1_word << " не найдено!\n"; else { text_map->erase( where ); cout << "ok: " << remova1_word << " удалено!\n"; |
}
В нашей реализации text_map с каждым словом сопоставляется множество позиций, что несколько усложняет их хранение и извлечение. Вместо этого можно было бы иметь по одной позиции на слово. Но контейнер map не допускает дублирующиеся ключи. Нам следовало бы воспользоваться классом multimap, который рассматривается в разделе 6.15.
Упражнение 6.20
Определите отображение, где ключом является фамилия, а значением – вектор с именами детей. Поместите туда как минимум шесть элементов. Реализуйте возможность делать запрос по фамилии, добавлять имена и распечатывать содержимое.
Упражнение 6.21
Измените программу из предыдущего упражнения так, чтобы вместе с именем ребенка записывалась дата его рождения: пусть вектор-значение хранит пары строк – имя и дата.
Упражнение 6.22
Приведите хотя бы три примера, в которых нужно использовать отображение. Напишите определение объекта map для каждого примера и укажите наиболее вероятный способ вставки и извлечения элементов.
Предположим, что в нашем классе Screen
определены четыре новых функции-члена: forward(), back(), up() и down(), которые перемещают курсор соответственно вправо, влево, вверх и вниз. Сначала мы должны объявить их в теле класса:
|
class Screen { public: inline Screen& forward(); inline Screen& back(); inline Screen& end(); inline Screen& up(); inline Screen& down(); // другие функции-члены не изменяются private: inline int row(); // другие функции-члены не изменяются |
};
Функции-члены forward() и back()
перемещают курсор на один символ. По достижении правого нижнего или левого верхнего угла экрана курсор переходит в противоположный угол.
|
inline Screen& Screen::forward() { // переместить _cursor вперед на одну экранную позицию ++_cursor; // если достигли конца экрана, перепрыгнуть в противоположный угол if ( _cursor == _screen.size() ) home(); return *this; } inline Screen& Screen::back() { // переместить _cursor назад на одну экранную позицию // если достигли начала экрана, перепрыгнуть в противоположный угол if ( _cursor == 0 ) end(); else --_cursor; return *this; |
}
end()
перемещает курсор в правый нижний угол экрана и является парной по отношению к функции-члену home():
|
inline Screen& Screen::end() { _cursor = _width * _height - 1; return *this; |
}
Функции up() и down()
перемещают курсор вверх и вниз на одну строку. По достижении верхней или нижней строки курсор остается на месте и подается звуковой сигнал:
|
const char BELL = '\007'; inline Screen& Screen::up() { // переместить _cursor на одну строку вверх // если уже наверху, остаться на месте и подать сигнал if ( row() == 1 ) // наверху? cout << BELL << endl; else _cursor -= _width; return *this; } inline Screen& Screen::down() { if ( row() == _height ) //внизу? cout << BELL << endl; else _cursor += _width; return *this; |
inline int Screen::row() { // вернуть текущую строку return ( _cursor + _width ) / height; |
Screen &repeat( char op, int times ) { switch( op ) { case DOWN: // n раз вызвать Screen::down() break; case DOWN: // n раз вызвать Screen::up() break; // ... } |
Указатели и динамическое выделение памяти были вкратце представлены в разделе 2.2. Указатель – это объект, содержащий адрес другого объекта и позволяющий косвенно манипулировать этим объектом. Обычно указатели используются для работы с динамически созданными объектами, для построения связанных структур данных, таких, как связанные списки и иерархические деревья, и для передачи в функции больших объектов – массивов и объектов классов – в качестве параметров.
Каждый указатель ассоциируется с некоторым типом данных, причем их внутреннее представление не зависит от внутреннего типа: и размер памяти, занимаемый объектом типа указатель, и диапазон значений у них одинаков[5]. Разница состоит в том, как компилятор воспринимает адресуемый объект. Указатели на разные типы могут иметь одно и то же значение, но область памяти, где размещаются соответствующие типы, может быть различной:
· указатель на int, содержащий значение адреса 1000, направлен на область памяти 1000-1003 (в 32-битной системе);
· указатель на double, содержащий значение адреса 1000, направлен на область памяти 1000-1007 (в 32-битной системе).
Вот несколько примеров:
|
int *ip1, *ip2; complex<double> *cp; string *pstring; vector<int> *pvec; |
double *dp;
Указатель обозначается звездочкой перед именем. В определении переменных списком звездочка должна стоять перед каждым указателем (см. выше: ip1 и ip2). В примере ниже lp – указатель на объект типа long, а lp2 – объект типа long:
long *lp, lp2;
В следующем случае fp
интерпретируется как объект типа float, а fp2 – указатель на него:
float fp, *fp2;
Оператор разыменования (*) может отделяться пробелами от имени и даже непосредственно примыкать к ключевому слову типа. Поэтому приведенные определения синтаксически правильны и совершенно эквивалентны:
string *ps; |
//pi инициализирован нулевым адресом int *pi = 0; // pi2 инициализирован адресом ival int *pi2 = &ival; // правильно: pi и pi2 содержат адрес ival pi = pi2; // pi2 содержит нулевой адрес |
// ошибка: pi не может принимать значение int |
double dval; |
// ошибки компиляции |
// правильно: void* может содержать |
int ival = 1024;, ival2 = 2048; |
// косвенное присваивание переменной ival значения ival2 *pi = ival2; // косвенное использование переменной ival как rvalue и lvalue *pi = abs(*pi); // ival = abs(ival); |
int **ppi = π int *pi2 = *ppi; cout << "Значение ival\n" << "явное значение: " << ival << "\n" << "косвенная адресация: " << *pi << "\n" << "дважды косвенная адресация: " << **ppi << "\n" |
int ival = 1024, ival2 = 2048; |
(a) ival = *pi3; (e) pi1 = *pi3; (b) *pi2 = *pi3; (f) ival = *pi1; (c) ival = pi2; (g) pi1 = ival; |
pi = &ival; |
int foobar(int *pi) { *pi = 1024; return *pi; } int main() { int *pi2 = 0; int ival = foobar(pi2); return 0; |
Предположим, что нам нужно написать функцию сортировки, вызов которой выглядит так:
sort( start, end, compare );
где start и end
являются указателями на элементы массива строк. Функция sort()
сортирует элементы между start и end, а аргумент compare
задает операцию сравнения двух строк этого массива.
Какую реализацию выбрать для compare? Мы можем сортировать строки лексикографически, т.е. в том порядке, в котором слова располагаются в словаре, или по длине – более короткие идут раньше более длинных. Нам нужен механизм для задания альтернативных операций сравнения.
(Заметим, что в главе 12 описан алгоритм sort() и другие обобщенные алгоритмы из стандартной библиотеки С++. В этом разделе мы покажем свою собственную версию sort() как пример употребления указателей на функции. Наша функция будет упрощенным вариантом стандартного алгоритма.)
Один из способов удовлетворить наши потребности – использовать в качестве третьего аргумента compare
указатель на функцию, применяемую для сравнения.
Для того чтобы упростить использование функции sort(), не жертвуя гибкостью, можно задать операцию сравнению по умолчанию, подходящую для большинства случаев. Предположим, что чаще всего нам требуется лексикографическая сортировка, поэтому в качестве такой операции возьмем функцию compare() для строк (эта функция впервые встретилась в разделе 6.10).
Можно объявлять указатели на функции, написанные на других языках программирования. Это делается с помощью директивы связывания. Например, указатель pf
ссылается на С-функцию:
extern "C" void (*pf)(int);
Через pf
вызывается функция, написанная на языке С.
|
extern "C" void exit(int); // pf ссылается на C-функцию exit() extern "C" void (*pf)(int) = exit; int main() { // ... // вызов С-функции, а именно exit() (*pf)(99); |
}
Вспомним, что присваивание и инициализация указателя на функцию возможны лишь тогда, когда тип в левой части оператора присваивания в точности соответствует типу в правой его части. Следовательно, указатель на С-функцию не может адресовать функцию С++ (и инициализация его таким адресом не допускается), и наоборот. Подобная попытка вызывает ошибку компиляции:
|
void (*pfl)(int); extern "C" void (*pf2)(int); int main() { pfl = pf2; // ошибка: pfl и pf2 имеют разные типы // ... |
}
Отметим, что в некоторых реализациях С++ характеристики указателей на функции С и С++ одинаковы. Отдельные компиляторы могут допустить подобное присваивание, рассматривая это как расширение языка.
Если директива связывания применяется к объявлению, она затрагивает все функции, участвующие в данном объявлении.
В следующем примере параметр pfParm
также служит указателем на С-функцию. Директива связывания применяется к объявлению функции, к которой этот параметр относится:
|
// pfParm - указатель на С-функцию |
extern "C" void f1( void(*pfParm)(int) );
Следовательно, f1()
является С-функцией с одним параметром – указателем на С-функцию. Значит, передаваемый ей аргумент должен быть либо такой же функцией, либо указателем на нее, поскольку считается, что указатели на функции, написанные на разных языках, имеют разные типы. (Снова заметим, что в тех реализациях С++, где указатели на функции С и С++ имеют одинаковые характеристики, компилятор может поддерживать расширение языка, позволяющее не различать эти два типа указателей.)
// FC представляет собой тип: // С-функция с параметром типа int, не возвращающая никакого значения extern "C" typedef void FC( int ); // f2() - C++ функция с параметром - // указателем на С-функцию |
(a) int (*mpf)(vector<int>&); (b) void (*apf[20])(doub1e); |
double abs(double); double sin(double); double cos(double); |
Можно объявить указатель на одну из множества перегруженных функций. Например:
|
extern void ff( vector<double> ); extern void ff( unsigned int ); // на какую функцию указывает pf1? |
void ( *pf1 )( unsigned int ) = &ff;
Поскольку функция ff()
перегружена, одного инициализатора &ff недостаточно для выбора правильного варианта. Чтобы понять, какая именно функция инициализирует указатель, компилятор ищет в множестве всех перегруженных функций ту, которая имеет тот же тип возвращаемого значения и список параметров, что и функция, на которую ссылается указатель. В нашем случае будет выбрана функция ff(unsigned int).
А что если не найдется функции, в точности соответствующей типу указателя? Тогда компилятор выдаст сообщение об ошибке:
|
extern void ff( vector<double> ); extern void ff( unsigned int ); // ошибка: соответствие не найдено: неверный список параметров |
void ( *pf2 )( int ) = &ff;
|
// ошибка: соответствие не найдено: неверный тип возвращаемого значения |
double ( *pf3 )( vector<double> ) = &ff;
Присваивание работает аналогично. Если значением указателя должен стать адрес перегруженной функции , то для выбора операнда в правой части оператора присваивания используется тип указателя на функцию. И если компилятор не находит функции, в точности соответствующей нужному типу, он выдает сообщение об ошибке. Таким образом, преобразование типов между указателями на функции никогда не производится.
|
matrix calc( const matrix & ); int calc( int, int ); int ( *pc1 )( int, int ) = 0; int ( *pc2 )( int, double ) = 0; // ... // правильно: выбирается функция calc( int, int ) pc1 = &calc; // ошибка: нет соответствия: неверный тип второго параметра |
pc2 = &calc;
Между указателями на статические и нестатические члены класса есть разница. Синтаксис указателя на член класса не используется для обращения к статическому члену. Статические члены – это глобальные объекты и функции, принадлежащие классу. Указатели на них – это обычные указатели. (Напомним, что статической функции-члену не передается указатель this.)
Объявление указателя на статический член класса выглядит так же, как и для указателя на объект, не являющийся членом класса. Для разыменования указателя никакой объект не требуется. Рассмотрим класс Account:
|
class Account { public: static void raiseInterest( double incr ); static double interest() { return _interestRate ; } double amount() { return _amount; } private: static double _interestRate; double _amount; string _owner; }; inline void Account::raiseInterest( double incr ) { _interestRate += incr; |
}
Тип &_interestRate – это double*:
|
// это неправильный тип для &_interestRate |
double Account::*
Определение указателя на &_interestRate
имеет вид:
|
// правильно: double*, а не double Account::* |
double *pd = &Account::_interestRate;
Этот указатель разыменовывается так же, как и обычный, объект класса для этого не требуется:
|
Account unit; // используется обычный оператор разыменования |
double daily = *pd / 365 * unit._amount;
Однако, поскольку _interestRate и _amount – закрытые члены, необходимо иметь статическую функцию-член interest() и нестатическую amount().
Указатель на interest() – это обычный указатель на функцию:
|
// правильно |
double (*)()
а не на функцию-член класса Account:
|
// неправильно |
double (Account::*)()
Определение указателя и косвенный вызов interest() реализуются так же, как и для обычных указателей:
|
// правильно: double(*pf)(), а не double(Account::*pf)() double(*pf)() = &Account::interest; |
double daily = pf() / 365 * unit.amount();
Упражнение 13.11
Если имеется запрос такого типа:
fiery && ( bird || potato )
то в нашу задачу входит построение эквивалентной иерархии классов:
|
AndQuery NameQuery( "fiery" ) OrQuery NameQuery( "bird" ) |
NameQuery( "potato" )
Как лучше всего это сделать? Процедура вычисления ответа на запрос напоминает функционирование конечного автомата. Мы начинаем с пустого состояния и при обработке каждого элемента запроса переходим в новое состояние, пока весь запрос не будет разобран. В основе нашей реализации лежит одна инструкция switch
внутри операции, которую мы назвали eval_query(). Слова запроса считываются одно за другим из вектора строк и сравниваются с каждым из возможных значений:
|
vector<string>::iterator it = _query->begin(), end_it = _query->end(); for ( ; it != end_it; ++it ) switch( evalQueryString( *it )) { case WORD: evalWord( *it ); break; case AND: evalAnd(); break; case OR: evalOr(); break; case NOT: evalNot(); break; case LPAREN: ++_paren; ++_lparenOn; break; case RPAREN: --_paren; ++_rparenOn; evalRParen(); break; |
}
Пять операций eval: evalWord(), evalAnd(), evalOr(), evalNot и evalRParen() – как раз и строят иерархию классов Query. Прежде чем обратиться к деталям их реализации, рассмотрим общую организацию программы.
Нам нужно определить каждую операцию в виде отдельной функции, как это было сделано в главе 6 при построении процедур обработки запроса. Пользовательский запрос и производные от Query
OrQuery NameQuery( "fiery" ) |
evalNot() : incomplete! push on _current_op ( size == 1 ) evalWord() : daddy pop _current_op : NotQuery add operand: WordQuery : NotQuery complete! |
Пространства имен появились в стандартном С++. Предыдущие версии С++ их не поддерживали, и, следовательно, поставляемые библиотеки не помещали глобальные объявления в пространства имен. Множество программ на С++ было написано еще до того, как компиляторы стали поддерживать такую опцию. Заключая содержимое библиотеки в пространство имен, мы можем испортить старое приложение, использующее ее предыдущие версии: все имена из этой библиотеки становятся квалифицированными, т.е. должны включать имя пространства вместе с оператором разрешения области видимости. Те приложения, в которых эти имена употребляются в неквалифицированной форме, перестают компилироваться.
Сделать видимыми имена из библиотеки, используемой в нашей программе, можно с помощью using-объявления. Предположим, что файл primer.h содержит интерфейс новой версии библиотеки, в котором глобальные объявления помещены в пространство имен cplusplus_primer. Нужно заставить нашу программу работать с новой библиотекой. Два using-объявления сделают видимыми имена класса matrix и функции inverse() из пространства cplusplus_primer:
|
#include "primer.h" using cplusplus_primer::matrix; using cplusplus_primer::inverse; // using-объявления позволяют использовать // имена matrix и inverse без спецификации void func( matrix &m ) { // ... inverse( m ); return m; |
}
Но если библиотека достаточно велика и приложение часто использует имена из нее, то для подгонки имеющегося кода к новой библиотеке может потребоваться много using-объявлений. Добавлять их все только для того, чтобы старый код скомпилировался и заработал, утомительно и чревато ошибками. Решить эту проблему помогают using-директивы, облегчающие переход на новую версию библиотеки, где впервые стали применяться пространства имен.
Using-директива начинается ключевым словом using, за которым следует ключевое слово namespace, а затем имя некоторого пространства имен. Это имя должно ссылаться на определенное ранее пространство, иначе компилятор выдаст ошибку. Using-директива позволяет сделать все имена из этого пространства видимыми в неквалифицированной форме.
#include "pnmer.h" // using-директива: все члены cplusplus_primer // становятся видимыми using namespace cplusplus_primer; // имена matrix и inverse можно использовать без спецификации void func( matrix &m ) { // ... inverse( m ); return m; |
namespace A { int i, j; |
namespace blip { int bi = 16, bj = 15, bk = 23; // прочие объявления } int bj = 0; void manip() { using namespace blip; // using-директива - // коллизия имен ::bj and blip::bj // обнаруживается только при // использовании bj ++bi; // blip::bi == 17 ++bj; // ошибка: неоднозначность // глобальная bj или blip::bj? ++::bj; // правильно: глобальная bj == 1 ++blip::bj; // правильно: blip::bj == 16 int bk = 97; // локальная bk скрывает blip::bk ++bk; // локальная bk == 98 |
namespace cplusplus_primer { class matrix { }; // прочие вещи ... } namespace DisneyFeatureAnimation { class matrix { }; // здесь тоже ... using namespace cplusplus_primer; using namespace DisneyFeatureAnimation; matrix m; //ошибка, неоднозначность: |
Имеется механизм, позволяющий обращаться к членам пространства имен, используя их имена без квалификатора, т.е. без префикса namespace_name::. Для этого применяются using-объявления.
Using-объявление начинается ключевым словом using, за которым следует квалифицированное имя члена пространства. Например:
|
namespace cplusplus_primer { namespace MatrixLib { class matrix { /* ... */ }; // ... } } // using-объявление для члена matrix |
using cplusplus_primer::MatrixLib::matrix;
Using-объявление вводит имя в ту область видимости, в которой оно использовано. Так, предыдущее using-объявление делает имя matrix глобально видимым.
После того как это объявление встретилось в программе, использование имени matrix в глобальной области видимости или во вложенных в нее областях относится к этому члену пространства имен. Пусть далее идет следующее объявление:
void func( matrix &m );
Оно вводит функцию func() с параметром типа cplusplus_primer:: MatrixLib::matrix.
Using-объявление ведет себя подобно любому другому объявлению: оно имеет область видимости, и имя, введенное им, можно употреблять начиная с места объявления и до конца области видимости. Using-объявление может использоваться в глобальной области видимости, равно как и в области видимости любого пространства имен. Оно употребляется и в локальной области. Имя, вводимое using-объявлением, как и любым другим, имеет следующие характеристики:
· оно должно быть уникальным в своей области видимости;
· оно скрывает одноименную сущность во внешней области;
· оно скрывается объявлением одноименной сущности во вложенной области.
Например:
|
namespace blip { int bi = 16, bj = 15, bk = 23; // прочие объявления } int bj = 0; void manip() { using blip::bi; // bi в функции manip() ссылается на blip::bi ++bi; // blip::bi == 17 using blip::bj; // скрывает глобальную bj // bj в функции manip()ссылается на blip::bj ++bj; // blip::bj == 16 int bk; // объявление локальной bk using blip::bk; // ошибка: повторное определение bk в manip() } int wrongInit = bk; // ошибка: bk невидима |
// надо использовать blip::bk
Using-объявления в функции manip()
позволяют ссылаться на членов пространства blib с помощью неквалифицированных имен. Такие объявления не видны вне manip(), и неквалифицированные имена могут применяться только внутри этой функции. Вне ее необходимо употреблять квалифицированные имена.
Using-объявление упрощает использование членов пространства имен. Оно вводит только одно имя. Using-объявление может находиться в определенной области видимости, и, значит, мы способны точно указать, в каком месте программы те или иные члены разрешается употреблять без дополнительной квалификации.
В следующем подразделе мы расскажем, как ввести в определенную область видимости все члены некоторого пространства имен.
Условное выражение, или оператор выбора, предоставляет возможность более компактной записи текстов, включающих инструкцию if-else. Например, вместо:
|
bool is_equal; if (!strcmp(str1,str2)) is_equal = true; |
else is_equal = false;
можно употребить более компактную запись:
bool is_equa1 = !strcmp( strl, str2 ) ? true : false;
Условный оператор имеет следующий синтаксис:
expr11 ? expr2 : expr3;
Вычисляется выражение expr1. Если его значением является true, оценивается expr2, если false, то expr3. Данный фрагмент кода:
|
int min( int ia, int ib ) |
{ return ( ia < ib ) ? ia : ib; }
эквивалентен
|
int min(int ia, int ib) { if (ia < ib) return ia; else return ib; |
}
Приведенная ниже программа иллюстрирует использование условного оператора:
|
#include <iostream> int main() { int i = 10, j = 20, k = 30; cout << "Большим из " << i << " и " << j << " является " << ( i > j ? i : j ) << end1; cout << "Значение " << i << ( i % 2 ? " нечетно." : " четно." ) << endl; /* условный оператор может быть вложенным, * но глубокая вложенность трудна для восприятия. * В данном примере max получает значение * максимальной из трех величин */ int max = ( (i > j) ? (( i > k) ? i : k) : ( j > k ) ? j : k); cout << "Большим из " << i << ", " << j << " и " << k << " является " << max << endl; |
}
Результатом работы программы будет:
Большим из 10 и 20 является 20
Значение 10 четно.
Операторы явного преобразования типов, представленные в предыдущем разделе, появились только в стандарте С++; раньше использовалась форма, теперь считающаяся устаревшей. Хотя стандарт допускает и эту форму, мы настоятельно не рекомендуем ею пользоваться. (Только если ваш компилятор не поддерживает новый вариант.)
Устаревшая форма явного преобразования имеет два вида:
|
// появившийся в C++ вид type (expr); // вид, существовавший в C |
(type) expr;
и может применяться вместо операторов static_cast, const_cast и reinterpret_cast.
Вот несколько примеров такого использования:
|
const char *pc = (const char*) pcom; int ival = (int) 3.14159; extern char *rewrite_str( char* ); char *pc2 = rewrite_str( (char*) pc ); |
int addr_va1ue = int( &iva1 );
Эта форма сохранена в стандарте С++ только для обеспечения обратной совместимости с программами, написанными для С и предыдущих версий С++.
Упражнение 4.21
Даны определения переменных:
|
char cval; int ival; float fval; double dva1; |
unsigned int ui;
Какие неявные преобразования типов будут выполнены?
|
(a) cva1 = 'a' + 3; (b) fval = ui - ival * 1.0; (c) dva1 = ui * fval; |
(d) cva1 = ival + fvat + dva1;
Упражнение 4.22
Даны определения переменных:
|
void *pv; int ival; char *pc; double dval; |
const string *ps;
Перепишите следующие выражения, используя операторы явного преобразования типов:
|
(a) pv = (void*)ps; (b) ival = int( *pc ); (c) pv = &dva1; |
(d) pc = (char*) pv;
Устоявшая функция относится к числу кандидатов. В списке ее формальных параметров либо то же самое число элементов, что и в списке фактических аргументов вызванной функции, либо больше. В последнем случае для дополнительных параметров задаются значения по умолчанию, иначе функцию нельзя будет вызвать с данным числом аргументов. Чтобы функция считалась устоявшей, должно существовать преобразование каждого фактического аргумента в тип соответствующего формального параметра. (Такие преобразования были рассмотрены в разделе 9.3.)
В следующем примере для вызова f(5.6)
есть две устоявшие функции: f(int) и f(double).
|
void f(); void f( int ); void f( double ); void f( char*, char* ); int main() { f( 5.6 ); // 2 устоявшие функции: f( int ) и f( double ) return 0; |
}
Функция f(int)
устояла, так как она имеет всего один формальный параметр, что соответствует числу фактических аргументов в вызове. Кроме того, существует стандартное преобразование аргумента типа double в int. Функция f(double) также устояла; она тоже имеет один параметр типа double, и он точно соответствует фактическому аргументу. Функции-кандидаты f() и f(char*, char*)
исключены из списка устоявших, так как они не могут быть вызваны с одним аргументом.
В следующем примере единственной устоявшей функцией для вызова format(3) является format(double). Хотя кандидата format(char*)
можно вызывать с одним аргументом, не существует преобразования из типа фактического аргумента int в тип формального параметра char*, а следовательно, функция не может считаться устоявшей.
|
char* format( int ); void g() { // глобальная функция format( int ) скрыта char* format( double ); char* format( char* ); format(3); // есть только одна устоявшая функция: format( double ) |
}
В следующем примере все три функции-кандидата оказываются устоявшими для вызова max()
внутри func(). Все они могут быть вызваны с двумя аргументами. Поскольку фактические аргументы имеют тип int, они точно соответствуют формальным параметрам функции libs_R_us::max(int, int) и могут быть приведены к типам параметров функции libs_R_us::max(double, double) с помощью трансформации целых в плавающие, а также к типам параметров функции libs_R_us::max(char, char)
Наследование оказывает влияние и на второй шаг разрешения перегрузки функции: отбор устоявших из множества кандидатов. Устоявшей называется функция, для которой существуют приведения типа каждого фактического аргумента к типу соответственного формального параметра.
В разделе 15.9 мы показали, как разработчик класса может предоставить пользовательские преобразования для объектов этого класса, которые неявно вызываются компилятором для трансформации фактического аргумента функции в тип соответственного формального параметра. Пользовательские преобразования бывают двух видов: конвертер или конструктор с одним параметром без ключевого слова explicit. При наследовании на втором шаге разрешения перегрузки рассматривается более широкое множество таких преобразований.
Конвертеры наследуются, как и любые другие функции-члены класса. Например, мы можем написать следующий конвертер для ZooAnimal:
|
class ZooAnimal { public: // конвертер: ZooAnimal ==> const char* operator const char*(); // ... |
};
Производный класс Bear
наследует его от своего базового ZooAnimal. Если значение типа Bear
используется в контексте, где ожидается const char*, то неявно вызывается конвертер для преобразования Bear в const char*:
|
extern void display( const char* ); Bear yogi; // правильно: yogi ==> const char* |
display( yogi );
Конструкторы с одним аргументом без ключевого слова explicit образуют другое множество неявных преобразований: из типа параметра в тип своего класса. Определим такой конструктор для ZooAnimal:
|
class ZooAnimal { public: // преобразование: int ==> ZooAnimal ZooAnimal( int ); // ... |
};
Его можно использовать для приведения значения типа int к типу ZooAnimal. Однако конструкторы не наследуются. Конструктор ZooAnimal
нельзя применять для преобразования объекта в случае, когда целевым является тип производного класса:
|
const int cageNumber = 8788l void mumble( const Bear & ); // ошибка: ZooAnimal( int ) не используется |
mumble( cageNumber );
Поскольку целевым типом является Bear – тип параметра функции mumble(), то рассматриваются только его конструкторы.
Первая задача, которую должна решить наша программа, – это считывание из файла заранее неизвестного количества слов. Слова хранятся в объектах типа string. Возникает вопрос: в каком контейнере мы будем хранить слова – в последовательном или ассоциативном?
С одной стороны, мы должны обеспечить возможность поиска слова и, в случае успеха, извлечь относящуюся к нему информацию. Отображение map является самым удобным для этого классом.
Но сначала нам нужно просто сохранить слова для предварительной обработки – исключения знаков препинания, суффиксов и т.п. Для этой цели последовательный контейнер подходит гораздо больше. Что же нам использовать: вектор или список?
Если вы уже писали программы на С или на С++ прежних версий, для вас, скорее всего, решающим фактором является возможность заранее узнать количество элементов. Если это количество известно на этапе компиляции, вы используете массив, в противном случае – список, выделяя память под очередной его элемент.
Однако это правило неприменимо к стандартным контейнерам: и vector, и deque
допускают динамическое изменение размера. Выбор одного из этих трех классов должен зависеть от способов, с помощью которых элементы добавляются в контейнер и извлекаются из него.
Вектор представляет собой область памяти, где элементы хранятся друг за другом. Для этого типа произвольный доступ (возможность извлечь, например, элемент 5, затем 15, затем 7 и т.д.) можно реализовать очень эффективно, поскольку каждый из них находится на некотором фиксированном расстоянии от начала. Однако вставка, кроме случая добавления в конец, крайне неэффективна: операция вставки в середину вектора потребует перемещения всего, что следует за вставляемым. Особенно это сказывается на больших векторах. (Класс deque устроен аналогично, однако операции вставки и удаления самого первого элемента работают в нем быстрее; это достигается двухуровневым представлением контейнера, при котором один уровень представляет собой реальное размещение элементов, а второй уровень адресует первый и последний из них.)
Когда определяется вектор из пяти объектов класса, например:
vector< Point > vec( 5 );
то инициализация элементов производится в следующем порядке5:
1. С помощью конструктора по умолчанию создается временный объект типа класса, хранящегося в векторе. .
2. К каждому элементу вектора применяется копирующий конструктор, в результате чего каждый объект инициализируется копией временного объекта.
3. Временный объект уничтожается.
Хотя конечный результат оказывается таким же, как при определении массива из пяти объектов класса:
Point pa[ 5 ];
эффективность подобной инициализации вектора ниже, так как, во-первых, на конструирование и уничтожение временного объекта, естественно, нужны ресурсы, а во-вторых, копирующий конструктор обычно оказывается вычислительно более сложным, чем конструктор по умолчанию.
Общее правило проектирования таково: вектор объектов класса удобнее только для вставки элементов, т.е. в случае, когда изначально определяется пустой вектор. Если мы заранее вычислили, сколько придется вставлять элементов, или имеем на этот счет обоснованное предположение, то надо зарезервировать необходимую память, а затем приступать к вставке. Например:
|
vector< Point > cvs; // пустой int cv_cnt = calc_control_vertices(); // зарезервировать память для хранения cv_cnt объектов класса Point // cvs все еще пуст ... cvs.reserve( cv_cnt ); // открыть файл и подготовиться к чтению из него ifstream infile( "spriteModel" ); istream_iterator<Point> cvfile( infile ),eos; // вот теперь можно вставлять элементы |
copy( cvfile, eos, inserter( cvs, cvs.begin() ));
(Алгоритм copy(), итератор вставки inserter и потоковый итератор чтения istream_iterator
рассматривались в главе 12.) Поведение объектов list
(список) и deque
(двусторонняя очередь) аналогично поведению объектов vector
(векторов). Вставка объекта в любой из этих контейнеров осуществляется с помощью копирующего конструктора.
(b) Account iA[1024] = { "Nhi", "Le", "Jon", "Mike", "Greg", "Brent", "Hank" |
(d) string as[] = *ps; |
// печально: не проверяется, что parray адресует массив |
// правильно: определяется размер массива, адресуемого parray |
// в первоначальном варианте языка размер массива требовалось задавать явно |
У класса iStack, разработанного нами в разделе 4.15, два недостатка:
·
он поддерживает только тип int. Мы хотим обеспечить поддержку любых типов. Это можно сделать, преобразовав наш класс в шаблон класса Stack;
· он имеет фиксированную длину. Это неудобно в двух отношениях: заполненный стек становится бесполезным, а в попытке избежать этого мы окажемся перед необходимостью отвести ему изначально слишком много памяти. Разумным выходом будет разрешить динамический рост стека. Это можно сделать, пользуясь тем, что лежащий в основе стека вектор способен динамически расти.
Напомним определение нашего класса iStack:
|
#include <vector> class iStack { public: iStack( int capacity ) : _stack( capacity ), _top( 0 ) {}; bool pop( int &value ); bool push( int value ); bool full(); bool empty(); void display(); int size(); private: int _top; vector< int > _stack; |
};
Сначала реализуем динамическое выделение памяти. Тогда вместо использования индекса при вставке и удалении элемента нам нужно будет применять соответствующие функции-члены. Член _top
больше не нужен: функции push_back() и pop_back() автоматически работают в конце массива. Вот модифицированный текст функций pop() и push():
|
bool iStack::pop( int &top_value ) { if ( empty() ) return false; top_value = _stack.back(); _stack.pop_back(); return true; } bool iStack::push( int value ) { if ( full() ) return false; _stack.push_back( value ); return true; |
}
Функции-члены empty(), size() и full()
также нуждаются в изменении: в этой версии они теснее связаны с лежащим в основе стека вектором.
|
inline bool iStack::empty(){ return _stack.empty(); } inline bool iStack::size() { return _stack.size(); } inline bool iStack::full() { |
void iStack::display() { cout << "( " << size() << " )( bot: "; for ( int ix=0; ix < size(); ++ix ) cout << _stack[ ix ] << " "; cout << " stop )\n"; |
class iStack { public: iStack( int capacity = 0 ); // ... |
inline iStack::iStack( int capacity ) { if ( capacity ) _stack.reserve( capacity ); |
#include <vector> template <class elemType> class Stack { public: Stack( int capacity=0 ); bool pop( elemType &value ); bool push( elemType value ); bool full(); bool empty(); void display(); int size(); private: vector< elemType > _stack; |
Изменим наш класс Bear
так, чтобы он имел собственную реализацию функции-члена onExhibit(), предоставляемой также ZooAnimal:
bool Bear::onExhibit() { ... }
Теперь обращение к onExhibit()
через объект Bear
разрешается в пользу экземпляра, определенного в этом классе:
|
Bear winnie( "любитель меда" ); |
winnie.onExhibit(); // Bear::onExhibit()
Обращение же к onExhibit()
через объект Raccoon
разрешается в пользу функции-члена, унаследованной из ZooAnimal:
|
Raccoon meeko( "любитель всякой еды" ); |
meeko.onExhibit(); // ZooAnimal::onExhibit()
Производный класс Panda
наследует члены своих базовых классов. Их можно отнести к одной из трех категорий:
· члены виртуального базового класса ZooAnimal, такие, как name() и family(), не замещенные ни в Bear, ни в Raccoon;
· член onExhibit()
виртуального базового класса ZooAnimal, наследуемый при обращении через Raccoon и замещенный в классе Bear;
· специализированные в классах Bear и Raccoon
экземпляры функции print() из ZooAnimal.
Можно ли, не опасаясь неоднозначности, напрямую обращаться к унаследованным членам из области видимости класса Panda? В случае невиртуального наследования – нет: все неквалифицированные ссылки на имя неоднозначны. Что касается виртуального наследования, то прямое обращение допустимо к любым членам из первой и второй категорий. Например, дан объект класса Panda:
Panda spot( "Spottie" );
Тогда инструкция
spot.name();
вызывает разделяемую функцию-член name()
виртуального базового ZooAnimal, а инструкция
spot.onExhibit();
вызывает функцию-член onExhibit()
производного класса Bear.
Когда два или более экземпляров члена наследуются разными путями (это относится не только к функциям-членам, но и к данным-членам, а также к вложенным типам) и все они представляют один и тот же член виртуального базового класса, неоднозначности не возникает, поскольку существует единственный разделяемый экземпляр (первая категория). Если один экземпляр представляет член виртуального базового, а другой – член унаследованного от него класса, то неоднозначности также не возникает: специализированному экземпляру из производного класса отдается предпочтение по сравнению с разделяемым экземпляром из виртуального базового (вторая категория). Но если оба экземпляра представляют члены производных классов, то прямое обращение неоднозначно. Лучше всего разрешить эту ситуацию, предоставив замещающий экземпляр в производном классе (третья категория).
class Base { public: bar( int ); // ... protected: int ival; // ... }; class Derived1 : virtual public Base { public: bar( char ); foo( char ); // ... protected: char cval; // ... }; class Derived2 : virtual public Base { public: foo( int ); // ... protected: int ival; char cval; // ... }; |
class Base { public: Base(); Base( string ); Base( const Base& ); // ... protected: string _name; |
(a) любой из class Derived1 : virtual public Vase { ... }; class Derived2 : virtual public Vase { ... }; (b) class VMI : public Derived1, public Derived2 { ... }; |
В основе иерархии классов Query
лежит виртуальная функция eval() (но с точки зрения возможностей языка она наименее интересна). Как и для других функций-членов, разумной реализации eval() в абстрактном классе Query
нет, поэтому мы объявляем ее чисто виртуальной:
|
class Query { public: virtual void eval() = 0; // ... |
};
Реальное разрешение имени eval()
происходит при построении отображения слов на вектор позиций. Если слово есть в тексте, то в отображении будет его вектор позиций. В нашей реализации вектор позиций, если он имеется, передается конструктору NameQuery
вместе с самим словом. Поэтому в классе NameQuery функция eval()
пуста.
Однако мы не можем унаследовать чисто виртуальную функцию из Query. Почему? Потому что NameQuery– это конкретный класс, объекты которого разрешается создавать в приложении. Если бы мы унаследовали чисто виртуальную функцию, то он стал бы абстрактным классом, так что создать объект такого типа не удалось бы. Поэтому мы объявим eval()
пустой функцией:
|
class NameQuery : public Query { public: virtual void eval() {} // ... |
};
Для запроса NotQuery
отыскиваются все строки текста, где указанное слово отсутствует. Для таких строк в член _loc
класса NotQuery
помещаются все пары (строка, колонка). Наша реализация выглядит следующим образом:
|
void NotQuery::eval() { // вычислим операнд _op->eval(); // _all_locs - это вектор, содержащий начальные позиции всех слов, // он является статическим членом NotQuery: // static const vector<locations>* _all_locs vector< location >::const_iterator iter = _all_locs->begin(), iter_end = _all_locs->end(); // получить множество строк, в которых операнд встречается set<short> *ps = _vec2set( _op->locations() ); // для каждой строки, где операнд не найден, // скопировать все позиции в _loc for ( ; iter != iter_end; ++iter ) { if ( ! ps->count( (*iter).first )) { _loc.push_back( *iter ); } } |
class less_than_pair { public: bool operator()( location loc1, location loc2 ) { return (( loc1.first < loc2.first ) || ( loc1.first == loc2.first ) && ( loc1.second < loc2.second )); } }; void OrQuery::eval() { // вычислить левый и правый операнды _lop->eval(); _rop->eval(); // подготовиться к объединению двух векторов позиций vector< location, allocator >::const_iterator riter = _rop->locations()->begin(), liter = _lop->locations()->begin(), riter_end = _rop->locations()->end(), liter_end = _lop->locations()->end(); merge( liter, liter_end, riter, riter_end, inserter( _loc, _loc.begin() ), less_than_pair() ); |
void AndQuery::eval() { // вычислить левый и правый операнды _lop->eval(); _rop->eval(); // установить итераторы vector< location, allocator >::const_iterator riter = _rop->locations()->begin(), liter = _lop->locations()->begin(), riter_end = _rop->locations()->end(), liter_end = _lop->locations()->end(); // продолжать цикл, пока есть что сравнивать while ( liter != liter_end && riter != riter_end ) { // пока номер строки в левом векторе больше, чем в правом while ( (*liter).first > (*riter).first ) { ++riter; if ( riter == riter_end ) return; } // пока номер строки в левом векторе меньше, чем в правом while ( (*liter).first < (*riter).first ) { // если соответствие найдено для последнего слова // в одной строке и первого слова в следующей // _max_col идентифицирует последнее слово в строке if ( ((*liter).first == (*riter).first-1 ) && ((*riter).second == 0 ) && ((*liter).second == (*_max_col)[ (*liter).first ] )) { _loc.push_back( *liter ); _loc.push_back( *riter ); ++riter; if ( riter == riter_end ) return; } ++liter; if ( liter == liter_end ) return; } // пока оба в одной и той же строке while ( (*liter).first == (*riter).first ) { if ( (*liter).second+1 == ((*riter).second) ) { // соседние слова _loc.push_back( *liter ); ++liter; _loc.push_back( *riter ); ++riter; } else if ( (*liter).second <= (*riter).second ) ++liter; else ++riter; if ( liter == liter_end || riter == riter_end ) return; } } |
По умолчанию наследование в C++ является специальной формой композиции по значению. Когда мы пишем:
class Bear : public ZooAnimal { ... };
каждый объект Bear
содержит все нестатические данные-члены подобъекта своего базового класса ZooAnimal, а также нестатические члены, объявленные в самом Bear. Аналогично, если производный класс является базовым для какого-то другого:
class PolarBear : public Bear { ... };
то каждый объект PolarBear
содержит все нестатические члены, объявленные в PolarBear, Bear и ZooAnimal.
В случае одиночного наследования эта форма композиции по значению, поддерживаемая механизмом наследования, обеспечивает компактное и эффективное представление объекта. Проблемы возникают только при множественном наследовании, когда некоторый базовый класс неоднократно встречается в иерархии наследования. Самый известный реальный пример такого рода – это иерархия классов iostream. Взгляните еще раз на рис. 18.2: istream и ostream наследуют одному и тому абстрактному базовому классу ios, а iostream
является производным как от istream, так и от ostream.
|
class iostream : |
public istream, public ostream { ... };
По умолчанию каждый объект iostream
содержит два подобъекта ios: из istream и из ostream. Почему это плохо? С точки зрения эффективности хранение двух копий подобъекта ios – пустая трата памяти, поскольку объекту iostream нужен только один экземпляр. Кроме того, конструктор вызывается для каждого подобъекта. Более серьезной проблемой является неоднозначность, к которой приводит наличие двух экземпляров. Например, любое неквалифицированное обращение к члену класса ios
дает ошибку компиляции. Какой экземпляр имеется в виду? Что будет, если классы istream и ostream
инициализируют свои подобъекты ios по-разному? Можно ли гарантировать, что в классе iostream
используется согласованная пара членов ios? Применяемый по умолчанию механизм композиции по значению не дает таких гарантий.
Для решения данной проблемы язык предоставляет альтернативный механизм композиции по ссылке: виртуальное наследование. В этом случае наследуется только один разделяемый подобъект базового класса, независимо от того, сколько раз базовый класс встречается в иерархии наследования. Этот разделяемый подобъект называется виртуальным базовым классом. С помощью виртуального наследования снимаются проблемы дублирования подобъектов базового класса и неоднозначностей, к которым такое дублирование приводит.
| class Panda : public Bear, |
![]() |
![]() |
![]() |
|||

В данной функции мы применяем оператор delete:
|
void doit_and_bedone( vector< Query* > *pvec ) { // ... for ( ; it != end_it; ++it ) { Query *pq = *it; // ... delete pq; } |
}
Чтобы функция выполнялась правильно, применение delete должно вызывать деструктор того класса, на который указывает pq. Следовательно, необходимо объявить деструктор Query
виртуальным:
|
class Query { public: virtual ~Query() { delete _solution; } // ... |
};
Деструкторы всех производных от Query
классов автоматически считаются виртуальными. doit_and_bedone()
выполняется правильно.
Поведение деструктора при наследовании таково: сначала вызывается деструктор производного класса, в случае pq – виртуальная функция. По завершении вызывается деструктор непосредственного базового класса – статически. Если деструктор объявлен встроенным, то в точке вызова производится подстановка. Например, если pq
указывает на объект класса AndQuery, то
delete pq;
приводит к вызову деструктора класса AndQuery за счет механизма виртуализации. После этого статически вызывается деструктор BinaryObject, а затем – снова статически – деструктор Query.
В следующей иерархии классов
|
class Query { public: // ... protected: virtual ~Query(); // ... }; class NotQuery : public Query { public: ~NotQuery(); // ... |
};
уровень доступа к конструктору NotQuery
открытый при вызове через объект NotQuery, но защищенный – при вызове через указатель или ссылку на объект Query. Таким образом, виртуальная функция подразумевает уровень доступа того класса, через объект которого вызывается:
|
int main() { Query *pq = new NotQuery; // ошибка: деструктор является защищенным delete pq; |
}
Эвристическое правило: если в корневом базовом классе иерархии объявлены одна или несколько виртуальных функций, рекомендуем объявлять таковым и деструктор. Однако, в отличие от конструктора базового класса, его деструктор не стоит делать защищенным.
Рассмотрим следующую простую иерархию классов:
|
#include <iostream> class base { public: virtual int foo( int ival = 1024 ) { cout << "base::foo() -- ival: " << ival << endl; return ival; } // ... }; class derived : public base { public: virtual int foo( int ival = 2048 ) { cout << "derived::foo() -- ival: " << ival << endl; return ival; } // ... |
};
Проектировщик класса хотел, чтобы при вызове без параметров реализации foo() из базового класса по умолчанию передавался аргумент 1024:
|
base b; base *pb = &b; // вызывается base::foo( int ) // предполагалось, что будет возвращено 1024 |
pb->foo();
Кроме того, разработчик хотел, чтобы при вызове его реализации foo() без параметров использовался аргумент по умолчанию 2048:
|
derived d; base *pb = &d; // вызывается derived::foo( int ) // предполагалось, что будет возвращено 2048 |
pb->foo();
Однако в C++ принята другая семантика механизма виртуализации. Вот небольшая программа для тестирования нашей иерархии классов:
|
int main() { derived *pd = new derived; base *pb = pd; int val = pb->foo(); cout << "main() : val через base: " << val << endl; val = pd->foo(); cout << "main() : val через derived: " << val << endl; |
}
После компиляции и запуска программа выводит следующую информацию:
derived::foo() -- ival: 1024
main() : val через base: 1024
derived::foo() -- ival: 2048
main() : val
через derived: 2048
При обоих обращениях реализация foo() из производного класса вызывается корректно, поскольку фактически вызываемый экземпляр определяется во время выполнения на основе типа класса, адресуемого pd и pb. Но передаваемый foo()
аргумент по умолчанию определяется не во время выполнения, а во время компиляции на основе типа объекта, через который вызывается функция. При вызове foo()
void base:: foo( int ival = base_default_value ) { int real_default_value = 1024; // настоящее значение по умолчанию if ( ival == base_default_value ) ival = real_default_value; // ... |
void derived:: foo( int ival = base_default_value ) { int real_default_value = 2048; if ( ival == base_default_value ) ival = real_default_value; // ... |
Как мы видели в разделе 17.4, для объекта производного класса сначала вызывается конструктор базового, а затем производного класса. Например, при таком определении объекта NameQuery
NameQuery poet( "Orlen" );
сначала будет вызван конструктор Query, а потом NameQuery.
При выполнении конструктора базового класса Query часть объекта, соответствующая классу NameQuery, остается неинициализированной. По существу, poet– это еще не объект NameQuery, сконструирован лишь его подобъект.
Что должно происходить, если внутри конструктора базового класса вызывается виртуальная функция, реализации которой существуют как в базовом, так и в производном классах? Какая из них должна быть вызвана? Результат вызова реализации из производного класса в случае, когда необходим доступ к его членам, оказался бы неопределенным. Вероятно, выполнение программы закончилось бы крахом.
Чтобы этого не случилось, в конструкторе базового класса всегда вызывается реализация виртуальной функции, определенная именно в базовом. Иными словами, внутри такого конструктора объект производного класса рассматривается как имеющий тип базового.
То же самое справедливо и внутри деструктора базового класса, вызываемого для объекта производного. И в этом случае часть объекта, относящаяся к производному классу, не определена: не потому, что еще не сконструирована, а потому, что уже уничтожена.
Упражнение 17.12
Внутри объекта NameQuery
естественное внутреннее представление вектора позиций – это указатель, который инициализируется указателем, хранящимся в отображении слов. Оно же является и наиболее эффективным, так как нам нужно скопировать лишь один адрес, а не каждую пару координат. Классы AndQuery, OrQuery и NotQuery должны конструировать собственные векторы позиций на основе вычисления своих операндов. Когда время жизни объекта любого из этих классов завершается, ассоциированный с ним вектор позиций необходимо удалить. Когда же заканчивается время жизни объекта NameQuery, вектор позиций удалять не следует. Как сделать так, чтобы вектор позиций был представлен указателем в базовом классе Query и при этом его экземпляры для объектов AndQuery, OrQuery и NotQuery удалялись, а для объектов NameQuery – нет? (Заметим, что нам не разрешается добавить в класс Query
class AbstractObject { public: ~AbstractObject(); virtual void doit() = 0; // ... |
NameQuery nq( "Sneezy" ); Query q( nq ); |
(a) Base* Base::copy( Base* ); |
(b) Base* Base::copy( Base* ); |
(c) ostream& Base::print( int, ostream&=cout ); |
(d) void Base::eval() const; |
class Object { public: virtual void doit() = 0; // ... protected: virtual ~Object(); }; class MyObject : public Object { public: MyObject( string isA ); string isA() const; protected: string _isA; |
По умолчанию функции-члены класса не являются виртуальными. В подобных случаях при обращении вызывается функция, определенная в статическом типе объекта класса (или указателя, или ссылки на объект), для которого она вызвана:
|
void Query::display( Query *pb ) { set<short> *ps = pb->solutions(); // ... display(); |
}
Статический тип pb– это Query*. При обращении к невиртуальному члену solutions() вызывается функция-член класса Query. Невиртуальная функция display()
вызывается через неявный указатель this. Статическим типом указателя this
также является Query*, поэтому вызвана будет функция-член класса Query.
Чтобы объявить функцию виртуальной, нужно добавить ключевое слово virtual:
|
class Query { public: virtual ostream& print( ostream* = cout ) const; // ... |
};
Если функция-член виртуальна, то при обращении к ней вызывается функция, определенная в динамическом типе объекта класса (или указателя, или ссылки на объект), для которого она вызвана. Однако для самих объектов класса статический и динамический тип – это одно и то же. Механизм виртуальных функций правильно работает только для указателей и ссылок на объекты.
Таким образом, полиморфизм проявляется только тогда, когда объект производного класса адресуется косвенно, через указатель или ссылку на базовый. Использование самого объекта базового класса не сохраняет идентификацию типа производного. Рассмотрим следующий фрагмент кода:
|
NameQuery nq( "lilacs" ); // правильно: но nq "усечено" до подобъекта Query |
Query qobject = nq;
Инициализация qobject
переменной nq
абсолютно законна: теперь qobject равняется подобъекту nq, который соответствует базовому классу Query, однако qobject не является объектом NameQuery. Часть nq, принадлежащая NameQuery, “усечена” перед инициализацией qobject, поскольку она не помещается в область памяти, отведенную под объект Query. Для поддержки этой парадигмы приходится использовать указатели и ссылки, но не сами объекты:
void print ( Query object, const Query *pointer, const Query &reference ) { // до момента выполнения невозможно определить, // какой экземпляр print() вызывается pointer->print(); reference.print(); // всегда вызывается Query::print() object.print(); } int main() { NameQuery firebird( "firebird" ); print( firebird, &firebird, firebird ); |
Первая виртуальная операция, которую мы хотели реализовать, – это печать запроса на стандартный вывод либо в файл:
ostream& print( ostream &os = cout ) const;
Функцию print()
следует объявить виртуальной, поскольку ее реализации зависят от типа, но нам нужно вызывать ее через указатель типа Query*. Например, для класса AndQuery эта функция могла бы выглядеть так:
|
ostream& AndQuery::print( ostream &os ) const { _lop->print( os ); os << " && "; _rop->print( os ); |
}
Необходимо объявить print()
виртуальной функцией в абстрактном базовом Query, иначе мы не сможем вызвать ее для членов классов AndQury, OrQuery и NotQuery, являющихся указателями на операнды соответствующих запросов типа Query*. Однако для самого Query
разумной реализации print() не существует. Поэтому мы определим ее как пустую функцию, а потом сделаем чисто виртуальной:
|
class Query { public: virtual ostream& print( ostream &os=cout ) const {} // ... |
};
В базовом классе, где виртуальная функция появляется в первый раз, ее объявлению должно предшествовать ключевое слово virtual. Если же ее определение находится вне этого класса, повторно употреблять virtual не следует. Так, данное определение print()
приведет к ошибке компиляции:
|
// ошибка: ключевое слово virtual может появляться // только в определении класса |
virtual ostream& Query::print( ostream& ) const { ... }
Правильный вариант не должен включать слово virtual.
Класс, в котором впервые появляется виртуальная функция, должен определить ее или объявить чисто виртуальной (напомним, что пока мы определили ее как пустую). В производном классе может быть либо определена собственная реализация той же функции, которая в таком случае становится активной для всех объектов этого класса, либо унаследована реализация из базового класса. Если в производном классе определена собственная реализация, то говорят, что она замещает реализацию из базового.
class Query { public: // ... // установить _lparen и _rparen void lparen( short lp ) { _lparen = lp; } void rparen( short rp ) { _rparen = rp; } // получить значения_lparen и _rparen short lparen() { return _lparen; } short rparen() { return _rparen; } // напечатать левую и правую скобки void print_lparen( short cnt, ostream& os ) const; void print_rparen( short cnt, ostream& os ) const; protected: // счетчики левых и правых скобок short _lparen; short _rparen; // ... |
ostream& NameQuery:: print( ostream &os ) const { if ( _lparen ) print_lparen( _lparen, os ); os << _name; if ( _rparen ) print_rparen( _rparen, os ); return os; |
class NameQuery : public Query { public: virtual ostream& print( ostream &os ) const; // ... |
class NotQuery : public Query { public: virtual ostream& print( ostream &os ) const; // ... |
ostream& NotQuery:: print( ostream &os ) const { os << " ! "; if ( _lparen ) print_lparen( _lparen, os ); _op->print( os ); if ( _rparen ) print_rparen( _rparen, os ); return os; |
class AndQuery : public Query { public: virtual ostream& print( ostream &os ) const; // ... |
ostream& AndQuery:: print( ostream &os ) const { if ( _lparen ) print_lparen( _lparen, os ); _lop->print( os ); os << " && "; _rop->print( os ); if ( _rparen ) print_rparen( _rparen, os ); return os; |
cout << "Был сформулирован запрос "; Query *pq = retrieveQuery(); |
Query *pq = retrieveQuery(); cout << "В ответ на запрос " << *pq |
inline ostream& operator<<( ostream &os, const Query &q ) { // виртуальный вызов print() return q.print( os ); |
AndQuery query; // сформулировать запрос ... |
NameQuery query2( "Salinger" ); |
Query *pquery = retrieveQuery(); |
Класс, объявленный внутри другого класса, называется вложенным. Он является членом объемлющего класса, а его определение может находиться в любой из секций public, private или protected
объемлющего класса.
Имя вложенного класса известно в области видимости объемлющего класса, но ни в каких других областях. Это означает, что оно не конфликтует с таким же именем, объявленным в объемлющей области видимости. Например:
|
class Node { /* ... */ } class Tree { public: // Node инкапсулирован внутри области видимости класса Tree // В этой области Tree::Node скрывает ::Node class Node {...}; // правильно: разрешается в пользу вложенного класса: Tree::Node Node *tree; }; // Tree::Node невидима в глобальной области видимости // Node разрешается в пользу глобального объявления Node Node *pnode; class List { public: // Node инкапсулирован внутри области видимости класса List // В этой области List::Node скрывает ::Node class Node {...}; // правильно: разрешается в пользу вложенного класса: List::Node Node *list; |
};
Для вложенного класса допустимы такие же виды членов, как и для невложенного:
|
// Не идеально, будем улучшать class List { public: class ListItem { friend class List; // объявление друга ListItem( int val=0 ); // конструктор ListItem *next; // указатель на собственный класс int value; }; // ... private: ListItem *list; ListItem *at_end; |
};
Закрытым называется член, который доступен только в определениях членов и друзей класса. У объемлющего класса нет права доступа к закрытым членам вложенного. Чтобы в определениях членов List
можно было обращаться к закрытым членам ListItem, класс ListItem
объявляет List как друга. Равно и вложенный класс не имеет никаких специальных прав доступа к закрытым членам объемлющего класса. Если бы нужно было разрешить ListItem
доступ к закрытым членам класса List, то в объемлющем классе List
// правильно: объявление в глобальной области видимости |
// Не идеально, будем улучшать class List { public: // ... private: class ListItem { // ... }; ListItem *list; ListItem *at_end; |
// так лучше class List { public: // ... private: // Теперь ListItem закрытый вложенный тип class ListItem { // а его члены открыты public: ListItem( int val=0 ); ListItem *next; int value; }; ListItem *list; ListItem *at_end; |
class List { public: // ... private: class ListItem { public: ListItem( int val=0 ); // ... }; }; // ошибка: ListItem вне области видимости |
// имя вложенного класса квалифировано именем объемлющего List::ListItem::ListItem( int val ) { value = val; next = 0; |
// ошибка: конструктор называется ListItem, а не List::ListItem List::ListItem::List::ListItem( int val ) { value = val; next = 0; |
class List { public: // ... private: // объявление необходимо class ListItem; ListItem *list; ListItem *at_end; }; // имя вложенного класса квалифицировано именем объемлющего класса class List::ListItem { public: ListItem( int val=0 ); ListItem *next; int value; |
class List { public: // ... private: // объявление необходимо class ListItem; ListItem *list; ListItem at_end; // ошибка: неопределенный вложенный класс ListItem |
class List { public: // ... private: // объявление List::ListItem class ListItem; class Ref { // pli имеет тип List::ListItem* ListItem *pli; }; определение List::ListItem class ListItem { // pref имеет тип List::Ref* Ref *pref; }; |
class List { public: int init( int ); private: class List::ListItem { public: ListItem( int val=0 ); void mf( const List & ); int value; }; }; List::ListItem::ListItem { int val ) { // List::init() - нестатический член класса List // должен использоваться через объект или указатель на тип List value = init( val ); // ошибка: неверное использование init |
void List::ListItem::mf( List &i1 ) { memb = i1.init(); // правильно: обращается к init() по ссылке |
class List { public: typedef int (*pFunc)(); enum ListStatus { Good, Empty, Corrupted }; //... private: class ListItem { public: void check_status(); ListStatus status; // правильно pFunc action; // правильно // ... }; // ... |
void List::ListItem::check_status() { ListStatus s = status; switch ( s ) { case Empty: ... case Corrupted: ... case Good: ... } |
List::pFunc myAction; // правильно |
Мы уже упоминали, что пользовательские пространства имен могут быть вложенными. Такие пространства применяются для дальнейшего структурирования кода нашей библиотеки. Например:
|
// ---- 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 & ); // ... } // второе вложенное пространство имен: // зоологическая часть библиотеки namespace AnimalLib { class ZooAnimal { /* ... */ }; class Bear : public ZooAnimal { /* ... */ }; class Raccoon : public Bear { /* ... */ }; // ... } |
}
Пространство имен cplusplus_primer
содержит два вложенных: MatrixLib и AnimalLib.
cplusplus_primer
предотвращает конфликт между именами из нашей библиотеки и именами из глобального пространства вызывающей программы. Вложенность позволяет делить библиотеку на части, в которых сгруппированы связанные друг с другом объявления и определения. MatrixLib
содержит сущности, имеющие отношение к классу matrix, а AnimalLib– к классу ZooAnimal.
Объявление члена вложенного пространства скрыто в этом пространстве. Имя такого члена автоматически дополняется поставленными спереди именами самого внешнего и вложенного пространств.
Например, класс, объявленный во вложенном пространстве MatrixLib, имеет имя
cplusplus_primer::MatrixLib::matrix
а функция
cplusplus_primer::MatrixLib::inverse
Программа, использующая члены вложенного пространства cplusplus_primer::MatrixLib, выглядит так:
|
#include "primer.h" // да, это ужасно... // скоро мы рассмотрим механизмы, облегчающие // использование членов пространств имен! void func( cplusplus_primer::MatrixLib::matrix &m ) { // ... cplusplus_primer::MatrixLib::inverse( m ); return m; |
typedef double Type; namespace cplusplus_primer { typedef int Type; // скрывает ::Type namespace MatrixLib { int val; // Type: объявление найдено в cplusplus_primer int func(Type t) { double val; // скрывает MatrixLib::val val = ...; } // ... } |
Шаблон класса QueueItem
применяется только как вспомогательное средство для реализации Queue. Чтобы запретить любое другое использование, в шаблоне QueueItem
имеется закрытый конструктор, позволяющий создавать объекты этого класса исключительно функциям-членам класса Queue, объявленным друзьями QueueItem. Хотя шаблон QueueItem
виден во всей программе, создать объекты этого класса или обратиться к его членам можно только при посредстве функций-членов Queue.
Альтернативный подход к реализации состоит в том, чтобы вложить определение шаблона класса QueueItem в закрытую секцию шаблона Queue. Поскольку QueueItem
является вложенным закрытым типом, он становится недоступным вызывающей программе, и обратиться к нему можно лишь из шаблона класса Queue и его друзей (например, оператора вывода). Если же сделать члены QueueItem
открытыми, то объявлять Queue другом QueueItem не понадобится.
Семантика исходной реализации при этом сохраняется, но отношение между шаблонами QueueItem и Queue
моделируется более элегантно.
Поскольку при любой конкретизации шаблона Queue
требуется конкретизировать тем же типом и QueueItem, то вложенный класс должен быть шаблоном. Вложенные классы шаблонов сами являются шаблонами классов, а параметры объемлющего шаблона можно использовать во вложенном:
|
template <class Type> class Queue: // ... private: class QueueItem { public: QueueItem( Type val ) : item( val ), next( 0 ) { ... } Type item; QueueItem *next; }; // поскольку QueueItem - вложенный тип, // а не шаблон, определенный вне Queue, // то аргумент шаблона <Type> после QueueItem можно опустить QueueItem *front, *back; // ... |
};
При каждой конкретизации Queue
создается также класс QueueItem с подходящим аргументом для Type. Между конкретизациями шаблонов QueueItem и Queue
имеется взаимно однозначное соответствие.
Вложенный в шаблон класс конкретизируется только в том случае, если он используется в контексте, где требуется полный тип класса. В разделе 16.2 мы упоминали, что конкретизация шаблона класса Queue
template <class Type, int size> class Buffer: public: enum Buf_vals { last = size-1, Buf_size }; typedef Type BufType; BufType array[ size ]; // ... |
// ошибка: какая конкретизация Buffer? Buffer::Buf_vals bfv0; |
template <class T> class Q { public: enum QA { empty, full }; // не зависит от параметров QA status; // ... }; #include <iostream> int main() { Q<double> qd; Q<int> qi; qd.status = Q::empty; // ошибка: какая конкретизация Q? qd.status = Q<double>::empty; // правильно int val1 = Q<double>::empty; int val2 = Q<int>::empty; if ( val1 != val2 ) cerr << "ошибка реализации!" << endl; return 0; |
Исключение– это аномальное поведение во время выполнения, которое программа может обнаружить, например: деление на 0, выход за границы массива или истощение свободной памяти. Такие исключения нарушают нормальный ход работы программы, и на них нужно немедленно отреагировать. В C++ имеются встроенные средства для их возбуждения и обработки. С помощью этих средств активизируется механизм, позволяющий двум несвязанным (или независимо разработанным) фрагментам программы обмениваться информацией об исключении.
Когда встречается аномальная ситуация, та часть программы, которая ее обнаружила, может сгенерировать, или возбудить, исключение. Чтобы понять, как это происходит, реализуем по-новому класс iStack, представленный в разделе 4.15, используя исключения для извещения об ошибках при работе со стеком. Определение класса iStack выглядит следующим образом:
|
#include <vector> class iStack { public: iStack( int capacity ) : _stack( capacity ), _top( 0 ) { } bool pop( int &top_value ); bool push( int value ); bool full(); bool empty(); void display(); int size(); private: int _top; vector< int > _stack; |
};
Стек реализован на основе вектора из элементов типа int. При создании объекта класса iStack его конструктор создает вектор из int, размер которого (максимальное число элементов, хранящихся в стеке) задается с помощью начального значения. Например, следующая инструкция создает объект myStack, который способен содержать не более 20 элементов типа int:
iStack myStack(20);
При манипуляциях с объектом myStack
могут возникнуть две ошибки:
· запрашивается операция pop(), но стек пуст;
· запрашивается операция push(), но стек полон.
Вызвавшую функцию нужно уведомить об этих ошибках посредством исключений. С чего же начать?
Во-первых, мы должны определить, какие именно исключения могут быть возбуждены. В C++ они чаще всего реализуются с помощью классов. Хотя в полном объеме классы будут представлены в главе 13, мы все же определим здесь два из них, чтобы использовать их как исключения для класса iStack. Эти определения мы поместим в заголовочный файл stackExcp.h:
// stackExcp.h class popOnEmpty { /* ... */ }; |
// увы, это не совсем правильно |
// инструкция является вызовом конструктора |
class iStack { public: // ... // больше не возвращают значения void pop( int &value ); void push( int value ); private: // ... |
#include "stackExcp.h" void iStack::pop( int &top_value ) { if ( empty() ) throw popOnEmpty(); top_value = _stack[ --_top ]; cout << "iStack::pop(): " << top_value << endl; } void iStack::push( int value ) { cout << "iStack::push( " << value << " )\n"; if ( full() ) throw pushOnFull( value ); _stack[ _top++ ] = value; |
enum EHstate { noErr, zeroOp, negativeOp, severeError }; int mathFunc( int i ) { if ( i == 0 ) throw zeroOp; // исключение в виде объекта-перечисления // в противном случае продолжается нормальная обработка |
(a) class exceptionType { }; throw exceptionType(); (b) int excpObj; throw excpObj; (c) enum mathErr { overflow, underflow, zeroDivide }; throw mathErr zeroDivide(); (d) int *pi = excpObj; |
Теперь, познакомившись с классами, посмотрим, что происходит, когда функция-член push()
нашего iStack
возбуждает исключение:
|
void iStack::push( int value ) { if ( full() ) // value сохраняется в объекте-исключении throw pushOnFull( value ); // ... |
}
Выполнение инструкции throw
инициирует несколько последовательных действий:
1. Инструкция throw
создает временный объект типа класса pushOnFull, вызывая его конструктор.
2. С помощью копирующего конструктора генерируется объект-исключение типа pushOnFull – копия временного объекта, полученного на шаге 1. Затем он передается обработчику исключения.
3. Временный объект, созданный на шаге 1, уничтожается до начала поиска обработчика.
Зачем нужно генерировать объект-исключение (шаг 2)? Инструкция
throw pushOnFull( value );
создает временный объект, который уничтожается в конце работы throw. Но исключение должно существовать до тех пор, пока не будет найден его обработчик, а он может находиться намного выше в цепочке вызовов. Поэтому необходимо скопировать временный объект в некоторую область памяти (объект-исключение), которая гарантированно существует, пока исключение не будет обработано. Иногда компилятор создает объект-исключение сразу, минуя шаг 1. Однако стандарт этого не требует, да и не всегда такое возможно.
Поскольку объект-исключение создается путем копирования значения, переданного инструкции throw, то возбужденное исключение всегда имеет такой же тип, как и это значение:
|
void iStack::push( int value ) { if ( full() ) { pushOnFull except( value ); stackExcp *pse = &except; throw *pse; // объект-исключение имеет тип stackExcp } // ... |
}
Выражение *pse
имеет тип stackExcp. Тип созданного объекта-исключения – stackExcp, хотя pse
ссылается на объект с фактическим типом pushOnFull. Фактический тип объекта, на который ссылается throw, при создании объекта-исключения не учитывается. Поэтому исключение не будет перехвачено catch-обработчиком pushOnFull.
Действия, выполняемые инструкцией throw, налагают определенные ограничения на то, какие классы можно использовать для создания объектов-исключений. Оператор throw в функции-члене push()
класса iStack
вызовет ошибку компиляции, если:
· в классе pushOnFull нет конструктора, принимающего аргумент типа int, или этот конструктор недоступен;
· в классе pushOnFull
есть копирующий конструктор или деструктор, но хотя бы один из них недоступен;
· pushOnFull – это абстрактный базовый класс. Напомним, что программа не может создавать объекты абстрактных классов (см. раздел 17.1).
В теле функции может встретиться инструкция return. Она завершает выполнение функции. После этого управление возвращается той функции, из которой была вызвана данная. Инструкция return
может употребляться в двух формах:
|
return; |
return expression;
Первая форма используется в функциях, для которых типом возвращаемого значения является void. Использовать return в таких случаях обязательно, если нужно принудительно завершить работу. (Такое применение return
напоминает инструкцию break, представленную в разделе 5.8.) После конечной инструкции функции подразумевается наличие return. Например:
|
void d_copy( double "src, double *dst, int sz ) { /* копируем массив "src" в "dst" * для простоты предполагаем, что они одного размера */ // завершение, если хотя бы один из указателей равен 0 if ( !src || !dst ) return; // завершение, // если указатели адресуют один и тот же массив if ( src == dst ) return; // копировать нечего if ( sz == 0 ) return; // все еще не закончили? // тогда самое время что-то сделать for ( int ix = 0; ix < sz; ++ix ) dst[ix] = src[ix]; // явного завершения не требуется |
}
Во второй форме инструкции return
указывается то значение, которое функция должна вернуть. Это значение может быть сколь угодно сложным выражением, даже содержать вызов функции. В реализации функции factorial(), которую мы рассмотрим в следующем разделе, используется return
следующего вида:
return val * factorial(val-1);
В функции, не объявленная с void в качестве типа возвращаемого значения, обязательно использовать вторую форму return, иначе произойдет ошибка компиляции. Хотя компилятор не отвечает за правильность результата, он сможет гарантировать его наличие. Следующая программа не компилируется из-за двух мест, где программа завершается без возврата значения:
|
// определение интерфейса класса Matrix #include "Matrix.h" bool is_equa1( const Matrix &ml, const Matrix &m2 ) { /* Если содержимое двух объектов Matrix одинаково, * возвращаем true; * в противном случае - false */ // сравним количество столбцов if ( ml.colSize() != m2.co1Size() ) // ошибка: нет возвращаемого значения return; // сравним количество строк if ( ml.rowSize() != m2.rowSize() ) // ошибка: нет возвращаемого значения return; // пробежимся по обеим матрицам, пока // не найдем неравные элементы for ( int row = 0; row < ml.rowSize(); ++row ) for ( int col = 0; co1 < ml.colSize(); ++co1 ) if ( ml[row][col] != m2[row][col] ) return false; // ошибка: нет возвращаемого значения // для случая равенства |
Matrix grow( Matrix* p ) { Matrix val; // ... return val; |
Matrix& grow( Matrix* p ) { Matrix *res; // выделим память для объекта Matrix // большого размера // res адресует этот новый объект // скопируем содержимое *p в *res return *res; |
// ошибка: возврат ссылки на локальный объект Matrix& add( Matrix &m1, Matrix &m2 ) { Matrix result: if ( m1.isZero() ) return m2; if ( m2.isZero() ) return m1; // сложим содержимое двух матриц // ошибка: ссылка на сомнительную область памяти // после возврата return result; |
#include <vector> int &get_val( vector<int> &vi, int ix ) { return vi [ix]; } int ai[4] = { 0, 1, 2, 3 }; vector<int> vec( ai, ai+4 ); // копируем 4 элемента ai в vec int main() { // увеличивает vec[0] на 1 get_val( vec.0 )++; // ... |
Рассмотрим следующую функцию min():
|
int min( int vl, int v2 ) { return( vl < v2 ? vl : v2 ); |
}
Преимущества определения функции для такой небольшой операции таковы:
· как правило, проще прочесть и интерпретировать вызов min(), чем читать условный оператор и вникать в смысл его действий, особенно если v1 и v2 являются сложными выражениями;
· модифицировать одну локализованную реализацию в приложении легче, чем 300. Например, если будет решено изменить проверку на:
( vl == v2 || vl < v2 )
поиск каждого ее вхождения будет утомительным и с большой долей вероятности приведет к ошибкам;
· семантика единообразна. Все проверки выполняются одинаково;
· функция может быть повторно использована в другом приложении.
Однако этот подход имеет один недостаток: вызов функции происходит медленнее, чем непосредственное вычисление условного оператора. Необходимо скопировать два аргумента, запомнить содержимое машинных регистров и передать управление в другое место программы. Решение дают встроенные функции.
Встроенная функция “подставляется по месту” в каждой точке своего вызова. Например:
int minVa12 = min( i, j );
заменяется при компиляции на
int minVal2 = i < j ? i : j;
Таким образом, не требуется тратить время на реализацию min() в виде функции.
Функция min()
объявляется как встроенная с помощью ключевого слова inline
перед типом возвращаемого значения в объявлении или определении:
inline int min( int vl, int v2 ) { /* ... */ }
Заметим, однако, что спецификация inline – это только подсказка компилятору. Компилятор может проигнорировать ее, если функция плохо подходит для встраивания по месту. Например, рекурсивная функция (такая, как rgcd()) не может быть полностью встроена в месте вызова (хотя для самого первого вызова это возможно). Функция из 1200 строк также скорее всего не подойдет. В общем случае такой механизм предназначен для оптимизации небольших, простых, часто используемых функций. Он крайне важен для поддержки концепции сокрытия информации при разработке абстрактных типов данных. Например, встроенной объявлена функция-член size() в классе IntArray из раздела 2.3.
Как уже было сказано, встроенный строковый тип перешел к С++ по наследству от С. Строка символов хранится в памяти как массив, и доступ к ней осуществляется при помощи указателя типа char*. Стандартная библиотека С предоставляет набор функций для манипулирования строками. Например:
|
// возвращает длину строки int strlen( const char* ); // сравнивает две строки int strcmp( const char*, const char* ); // копирует одну строку в другую |
char* strcpy( char*, const char* );
Стандартная библиотека С является частью библиотеки С++. Для ее использования мы должны включить заголовочный файл:
#include <cstring>
Указатель на char, с помощью которого мы обращаемся к строке, указывает на соответствующий строке массив символов. Даже когда мы пишем строковый литерал, например
const char *st = "Цена бутылки вина\n";
компилятор помещает все символы строки в массив и затем присваивает st
адрес первого элемента массива. Как можно работать со строкой, используя такой указатель?
Обычно для перебора символов строки применяется адресная арифметика. Поскольку строка всегда заканчивается нулевым символом, можно увеличивать указатель на 1, пока очередным символом не станет нуль. Например:
while (*st++ ) { ... }
st разыменовывается, и получившееся значение проверяется на истинность. Любое отличное от нуля значение считается истинным, и, следовательно, цикл заканчивается, когда будет достигнут символ с кодом 0. Операция инкремента ++ прибавляет 1 к указателю st и таким образом сдвигает его к следующему символу.
Вот как может выглядеть реализация функции, возвращающей длину строки. Отметим, что, поскольку указатель может содержать нулевое значение (ни на что не указывать), перед операцией разыменования его следует проверять:
|
int string_length( const char *st ) { int cnt = 0; if ( st ) while ( *st++ ) ++cnt; return cnt; |
}
Строка встроенного типа может считаться пустой в двух случаях: если указатель на строку имеет нулевое значение (тогда у нас вообще нет никакой строки) или указывает на массив, состоящий из одного нулевого символа (то есть на строку, не содержащую ни одного значимого символа).
#include <iostream> |
#include <iostream> |
st = st – len; |
Как было показано в главе 1, С++ предоставляет встроенную поддержку для основных типов данных– целых и вещественных чисел, логических значений и символов:
|
// объявление целого объекта ival // ival инициализируется значением 1024 int ival = 1024; // объявление вещественного объекта двойной точности dval // dval инициализируется значением 3.14159 double dval = 3.14159; // объявление вещественного объекта одинарной точности fval // fval инициализируется значением 3.14159 |
float fval = 3.14159;
К числовым типам данных могут применяться встроенные арифметические и логические операции: объекты числового типа можно складывать, вычитать, умножать, делить и т.д.
|
int ival2 = ival1 + 4096; // сложение |
int ival3 = ival2 - ival; // вычитание
dval = fval * ival; // умножение
ival = ival3 / 2; // деление
bool result = ival2 == ival3; // сравнение на равенство
result = ival2 + ival != ival3; // сравнение на неравенство
result = fval + ival2 < dval; // сравнение на меньше
result = ival > ival2; // сравнение на больше
В дополнение к встроенным типам стандартная библиотека С++ предоставляет поддержку для расширенного набора типов, таких, как строка и комплексное число. (Мы отложим рассмотрение класса vector из стандартной библиотеки до раздела 2.7.)
Промежуточное положение между встроенными типами данных и типами данных из стандартной библиотеки занимают составные типы – массивы и указатели. (Указатели рассмотрены в разделе 2.2.)
Массив – это упорядоченный набор элементов одного типа. Например, последовательность
0 1 1 2 3 5 8 13 21
представляет собой первые 9 элементов последовательности Фибоначчи. (Выбрав начальные два числа, вычисляем каждый из следующих элементов как сумму двух предыдущих.)
Для того чтобы объявить массив и проинициализировать его данными элементами, мы должны написать следующую инструкцию С++:
int fibon[9] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 };
Здесь fibon – это имя массива. Элементы массива имеют тип int, размер
fibon[0]; // первый элемент fibon[1]; // второй элемент ... fibon[8]; // последний элемент |
int main() |
int array0[10]; array1[10]; ... array0 = array1; // ошибка Вместо этого мы должны программировать такую операцию с помощью цикла: for (int index=0; index<10; ++index) |
Функцию можно рассматривать как операцию, определенную пользователем. В общем случае она задается своим именем. Операнды функции, или формальные параметры, задаются в списке параметров, через запятую. Такой список заключается в круглые скобки. Результатом функции может быть значение, которое называют возвращаемым. Об отсутствии возвращаемого значения сообщают ключевым словом void. Действия, которые производит функция, составляют ее тело; оно заключено в фигурные скобки. Тип возвращаемого значения, ее имя, список параметров и тело составляют определение функции. Вот несколько примеров:
|
inline int abs( int obj ) { // возвращает абсолютное значение iobj return( iobj < 0 ? -iobj : iobj ); } inline int min( int p1, int p2 ) { // возвращает меньшую из двух величин return( pi < p2 ? pi : p2 ); } int gcd( int vl, int v2 ) { // возвращает наибольший общий делитель while ( v2 ) { int temp = v2; v2 = vl % v2; vl = temp; } return vl; |
}
Выполнение функции происходит тогда, когда в тексте программы встречается оператор вызова. Если функция принимает параметры, при ее вызове должны быть указаны фактические параметры, аргументы. Их перечисляют внутри скобок, через запятую. В следующем примере main() дважды вызывает abs() и по одному разу min() и gcd(). Функция main()
определяется в файле main.C.
|
#include <iostream> int main() { // прочитать значения из стандартного ввода cout << "Введите первое значение: "; int i; cin >> i; if ( !cin ) { cerr << "!? Ошибка ввода - аварийный выход!\n"; return -1; } cout << "Введите второе значение: "; int j; cin >> j; if ( !cin ) { cerr << "!? Ошибка ввода - аварийный выход!\n"; return -2; } cout << "\nmin: " << min( i, j ) << endl; i = abs( i ); j = abs( j ); cout << "НОД: " << gcd( i, j ) << endl; return 0; |
int abs( int ); int min( int, int ); |
// определение функции находится в файле gcd.С int gcd( int, int ); inline int abs(int i) { return( i<0 ? -i : i ); } inline int min(int vl.int v2) { return( vl<v2 ? vl : v2 ); |
Основное средство реализации ввода– это оператор сдвига вправо (>>). Например, в следующей программе из стандартного ввода читается последовательность значений типа int и помещается в вектор:
|
#include <iostream> #include <vector> int main() { vector<int> ivec; int ival; while ( cin >> ival ) ivec.push_back( ival ); // ... |
}
Подвыражение
cin >> ival;
читает целое число из стандартного ввода и копирует его в переменную ival. Результатом является левый операнд – объект класса istream, в данном случае cin. (Как мы увидим, это позволяет сцеплять операторы ввода.)
Выражение
while ( cin >> ival )
читает последовательность значений, пока cin не станет равно false. Значение istream
может быть равно false в двух случаях: достигнут конец файла (т.е. все значения из файла прочитаны успешно) или встретилось неверное значение, скажем 3.14159 (десятичная точка недопустима в целом числе), 1e-1 (буква e
недопустима) или любой строковый литерал. Если вводится неверное значение, объект istream
переводится в состояние ошибки и чтение прекращается. (В разделе 20.7 мы подробнее расскажем о таких состояниях.)
Есть набор предопределенных операторов ввода, принимающих аргументы любого встроенного типа, включая C-строки, а также стандартных библиотечных типов string и complex:
|
#include <iostream> #include <string> int main() { int item_number; string item_name; double item_price; cout << "Пожалуйста, введите item_number, item_name и price: " << endl; cin >> item_number; cin >> item_name; cin >> item_price; cout << "Введены значения: item# " << item_number << " " << item_name << " @$" << item_price << endl; |
}
Вот пример выполнения этой программы:
cin >> item_number; if ( ! cin ) |
#include <iostream> #include <string> int main() { int item_number; string item_name; double item_price; cout << "Пожалуйста, введите item_number, item_name и price: " << endl; // хорошо, но легче допустить ошибку cin >> item_number >> item_name >> item_price; cout << "Введены значения: item# " << item_number << " " << item_name << " @$" << item_price << endl; |
#include <iostream> int main() { char ch; // прочитать и вывести каждый символ while ( cin >> ch ) cout << ch; cout << endl; // ... |
#include <iostream> int main() { char ch; // читать все символы, в том числе пробельные while ( cin.get( ch )) cout.put( ch ); // ... |
#include <algorithm> #include <string> #include <vector> #include <iostream> int main() { istream_iterator< string > in( cin ), eos ; vector< string > text ; // копировать прочитанные из стандартного ввода значения // в вектор text copy( in , eos , back_inserter( text ) ) ; sort( text.begin() , text.end() ) ; // удалить дубликаты vector< string >::iterator it; it = unique( text.begin() , text.end() ) ; text.erase( it , text.end() ) ; // вывести получившийся вектор int line_cnt = 1 ; for ( vector< string >::iterator iter = text.begin() ; iter != text.end() ; ++iter , ++line_cnt ) cout << *iter << ( line_cnt % 9 ? " " : "\n" ) ; cout << endl; |
Определенное пользователем преобразование реализуется в виде конвертера или конструктора. Как уже было сказано, после преобразования, выполненного конвертером, разрешается использовать стандартное преобразование для приведения возвращенного значения к целевому типу. Трансформации, выполненной конструктором, также может предшествовать стандартное преобразование для приведения типа аргумента к типу формального параметра конструктора.
Последовательность определенных пользователем преобразований– это комбинация определенного пользователем и стандартного преобразования, которая необходима для приведения значения к целевому типу. Такая последовательность имеет вид:
Последовательность стандартных преобразований ->
Определенное пользователем преобразование ->
Последовательность стандартных преобразований
где определенное пользователем преобразование реализуется конвертером либо конструктором.
Не исключено, что для трансформации исходного значения в целевой тип существует две разных последовательности пользовательских преобразований, и тогда компилятор должен выбрать из них лучшую. Рассмотрим, как это делается.
В классе разрешается определять много конвертеров. Например, в нашем классе Number их два: operator int() и operator float(), причем оба способны преобразовать объект типа Number в значение типа float. Естественно, можно воспользоваться конвертером Token::operator float() для прямой трансформации. Но и Token::operator int()
тоже подходит, так как результат его применения имеет тип int и, следовательно, может быть преобразован в тип float с помощью стандартного преобразования. Является ли трансформация неоднозначной, если имеется несколько таких последовательностей? Или какую-то из них можно предпочесть остальным?
|
class Number { public: operator float(); operator int(); // ... }; Number num; |
float ff = num; // какой конвертер? operator float()
В таких случаях выбор наилучшей последовательности определенных пользователем преобразований основан на анализе последовательности преобразований, которая применяется после конвертера. В предыдущем примере можно применить такие две последовательности:
class SmallInt { public: SmallInt( int ival ) : value( ival ) { } SmallInt( double dval ) : value( static_cast< int >( dval ) ); { } }; extern void manip( const SmallInt & ); int main() { double dobj; manip( dobj ); // правильно: SmallInt( double ) |
class Number { public: operator float(); operator int(); // ... |
// ошибка: можно применить как float(), так и int() |
// правильно: явное приведение типа |
class SmallInt { public: SmallInt( const Number & ); // ... }; class Number { public: operator SmallInt(); // ... }; extern void compute( SmallInt ); extern Number num; |
// правильно: явный вызов устраняет неоднозначность |
Нашей первой задачей является разбиение строки на слова. Мы будем вычленять слова, находя разделяющие их пробелы с помощью функции find(). Например, в строке
Alice Emma has long flowing red hair.
насчитывается шесть пробелов, следовательно, эта строка содержит семь слов.
Класс string
имеет несколько функций поиска. find() – наиболее простая из них. Она ищет образец, заданный как параметр, и возвращает позицию его первого символа в строке, если он найден, или специальное значение string::npos в противном случае. Например:
|
#include <string> #include <iostream> int main() { string name( "AnnaBelle" ); int pos = name.find( "Anna" ); if ( pos == string::npos ) cout << "Anna не найдено!\n"; else cout << "Anna найдено в позиции: " << pos << endl; |
}
Хотя позиция подстроки почти всегда имеет тип int, более правильное и переносимое объявление типа результата, возвращаемого find(), таково:
string::size_type
Например:
string::size_type pos = name.find( "Anna" );
Функция find()
делает не совсем то, что нам надо. Требуемая функциональность обеспечивается функцией find_first_of(), которая возвращает позицию первого символа, соответствующего одному из заданных в строке-параметре. Вот как найти первый символ, являющийся цифрой:
|
#include <string> #include <iostream> int main() { string numerics( "0123456789" ); string name( "r2d2" ); string:: size_type pos = name.find_first_of( numerics ); cout << "найдена цифра в позиции: " << pos << "\tэлемент равен " << name[pos] << endl; |
}
В этом примере pos
получает значение 1
(напоминаем, что символы строки нумеруются с 0).
Но нам нужно найти все вхождения символа, а не только первое. Такая возможность реализуется передачей функции find_first_of()
#include <string> #include <iostream> int main() { string numerics( "0123456789" ); string name( "r2d2" ); string::size_type pos = 0; // где-то здесь ошибка! while (( pos = name.find_first_of( numerics, pos )) != string::npos ) cout << "найдена цифра в позиции: " << pos << "\tэлемент равен " << name[pos] << endl; |
// исправленная версия цикла while (( pos = name.find_first_of( numerics, pos )) != string::npos ) { cout << "найдена цифра в позиции: " << pos << "\tэлемент равен " << name[pos] << endl; // сдвинуться на 1 символ ++pos; |
// фрагмент программы while (( pos = textline.find_first_of( ' ', pos )) != string::npos ) |
// фрагмент программы // pos: позиция на 1 большая конца слова // prev_pos: позиция начала слова string::size_type pos = 0, prev_pos = 0; while (( pos = textline.find_first_of( ' ', pos )) != string::npos ) { // ... // запомнить позицию начала слова prev_pos = ++pos; |
// фрагмент программы vector<string> words; while (( pos = textline.find_first_of( ' ', pos )) != string::npos ) { words.push_back( textline.substr( prev_pos, pos-prev_pos)); prev_pos = ++pos; |
typedef pair<short,short> location; typedef vector<location> loc; typedef vector<string> text; typedef pair<text* ,loc*> text_loc; text_loc* separate_words( const vector<string> *text_file ) { // words: содержит набор слов // locations: содержит информацию о строке и позиции // каждого слова vector<string> *words = new vector<string>; vector<location> * locations = new vector<location>; short line_pos = 0; // текущий номер строки // iterate through each line of text for ( ; line_pos < text_file->size(); ++line_pos ) // textline: обрабатываемая строка // word_pos: позиция в строке short word_pos = 0; string textline = (*text_file) [ line_pos ]; string::size_type pos = 0, prev_pos = 0; while (( pos = textline.find_first_of( ' ', pos )) != string::npos ) { // сохраним слово words->push_back( textline.substr( prev_pos, pos - prev_pos )); // сохраним информацию о его строке и позиции locations->push_back( make_pair( line_pos, word_pos )); // сместим позицию для следующей итерации ++word_pos; prev_pos = ++pos; } // обработаем последнее слово words->push_back( textline.substr( prev_pos, pos - prev_pos )); locations->push_back( make_pair( line_pos, word_pos )); } return new text_loc( words, locations ); |
int main() { vector<string> *text_file = retrieve_text(); text_loc *text_locations = separate_words( text_file ); // ... |
string river( "Mississippi" ); string::size_type first_pos = river.find( "is" ); |
string elems( "0123456789" ); string dept_code( "03714p3" ); // возвращается позиция символа 'p' |
string linel = "We were her pride of 10 she named us --"; string line2 = "Benjamin, Phoenix, the Prodigal" string line3 = "and perspicacious pacific Suzanne"; |
В главе 3 мы рассмотрели типы данных– как встроенные, так и предоставленные стандартной библиотекой. Здесь мы разберем предопределенные операции, такие, как сложение, вычитание, сравнение и т.п., рассмотрим их приоритеты. Скажем, результатом выражения 3+4*5 является 23, а не 35 потому, что операция умножения (*) имеет более высокий приоритет, чем операция сложения (+). Кроме того, мы обсудим вопросы преобразований типов данных – и явных, и неявных. Например, в выражении 3+0.7 целое значение 3 станет вещественным перед выполнением операции сложения.
При вызове шаблона функции типы и значения его аргументов определяются путем исследования типов фактических аргументов функции. Этот процесс называется выводом аргументов шаблона.
Параметром функции в шаблоне min()
является ссылка на массив элементов типа Type:
|
template <class Type, int size> |
Type min( Type (&r_array)[size] ) { /* ... */ }
Для сопоставления с формальным параметром функции фактический аргумент также должен быть l-значением, представляющим тип массива. Следующий вызов ошибочен, так как pval
имеет тип int*, а не является l-значением типа “массив int”.
|
void f( int pval[9] ) { // ошибка: Type (&)[] != int* int jval = min( pval ); |
}
При выводе аргументов шаблона не принимается во внимание тип значения, возвращаемого конкретизированным шаблоном функции. Например, если вызов min()
записан так:
|
double da[8] = { 10.3, 7.2, 14.0, 3.8, 25.7, 6.4, 5.5, 16.8 }; |
int i1 = min( da );
то конкретизированный экземпляр min() имеет параметр типа “указатель на массив из восьми double” и возвращает значение типа double. Перед инициализацией i1 это значение приводится к типу int. Однако тот факт, что результат вызова min()
используется для инициализации объекта типа int, не влияет на вывод аргументов шаблона.
Чтобы процесс такого вывода завершился успешно, тип фактического аргумента функции не обязательно должен совпадать с типом соответствующего формального параметра. Допустимы три вида преобразований типа: трансформация l-значения, преобразование спецификаторов и приведение к базовому классу, конкретизированному из шаблона класса. Рассмотрим последовательно каждое из них.
Напомним, что трансформация l-значения– это либо преобразование l-значения в r-значение, либо преобразование массива в указатель, либо преобразование функции в указатель (все они рассматривались в разделе 9.3). Для иллюстрации влияния такой трансформации на вывод аргументов шаблона рассмотрим функцию min2() c одним параметром шаблона Type и двумя параметрами функции. Первый параметр min2() – это указатель на тип Type*. size
template <class Type> // первый параметр имеет тип Type* Type min2( Type* array, int size ) { Type min_val = array[0]; for ( int i = 1; i < size; ++i ) if ( array[i] < min_val ) min_val = array[i]; return min_val; |
int ai[4] = { 12, 8, 73, 45 }; int main() { int size = sizeof (ai) / sizeof (ai[0]); // правильно: преобразование массива в указатель min2( ai, size ); |
template <class Type> // первый параметр имеет тип const Type* Type min3( const Type* array, int size ) { // ... |
int *pi = &ai; // правильно: приведение спецификаторов к типу const int* |
template <class Type> class Array { /* ... */ } template <class Type> Type min4( Array<Type>& array ) { Type min_val = array[0]; for ( int i = 1; i < array.size(); ++i ) if ( array[i] < min_val ) min_val = array[i]; return min_val; |
template <class Type> class ArrayRC : public Array<Type> { /* ... */ }; int main() { ArrayRC<int> ia_rc(10); min4( ia_rc ); |
template <class T> T min5( T, T ) { /* ... */ } unsigned int ui; int main() { // ошибка: нельзя конкретизировать min5( unsigned int, int ) // должно быть: min5( unsigned int, unsigned int ) или // min5( int, int ) min5( ui, 1024 ); |
template <class Type> |
int ai[] = { ... }; double dd; int main() { // конкретизируется sum( int, int ) sum( ai[0], dd ); |
template <class Type> Type min3( const Type* array, int size ) { /* ... */ } template <class Type> |
double dobj1, dobj2; float fobj1, fobj2; char cobj1, cobj2; int ai[5] = { 511, 16, 8, 63, 34 }; (a) min5( cobj2, 'c' ); (b) min5( dobj1, fobj1 ); |
Указатель на функцию применяется для вызова функции, которую он адресует. Включать оператор разыменования при этом необязательно. И прямой вызов функции по имени, и косвенный вызов по указателю записываются одинаково:
|
#include <iostream> int min( int*, int ); int (*pf)( int*, int ) = min; const int iaSize = 5; int ia[ iaSize ] = { 7, 4, 9, 2, 5 }; int main() { cout << "Прямой вызов: min: " << min( ia, iaSize ) << endl; cout << "Косвенный вызов: min: " << pf( ia, iaSize ) << endl; return 0; } int min( int* ia, int sz ) { int minVal = ia[ 0 ]; for ( int ix = 1; ix < sz; ++ix ) if ( minVal > ia[ ix ] ) minVal = ia[ ix ]; return minVal; |
}
Вызов
pf( ia, iaSize );
может быть записан также и с использованием явного синтаксиса указателя:
(*pf)( ia, iaSize );
Результат в обоих случаях одинаковый, но вторая форма говорит читателю, что вызов осуществляется через указатель на функцию.
Конечно, если такой указатель имеет нулевое значение, то любая форма вызова приведет к ошибке во время выполнения. Использовать можно только те указатели, которые адресуют какую-либо функцию или были проинициализированы таким значением.
Если мы имеем определение массива:
int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 };
то что означает простое указание его имени в программе?
ia;
Использование идентификатора массива в программе эквивалентно указанию адреса его первого элемента:
|
ia; |
&ia[0]
Аналогично обратиться к значению первого элемента массива можно двумя способами:
|
// оба выражения возвращают первый элемент *ia; |
ia[0];
Чтобы взять адрес второго элемента массива, мы должны написать:
&ia[1];
Как мы уже упоминали раньше, выражение
ia+1;
также дает адрес второго элемента массива. Соответственно, его значение дают нам следующие два способа:
|
*(ia+1); |
ia[1];
Отметим разницу в выражениях:
*ia+1
и
*(ia+1);
Операция разыменования имеет более высокий приоритет, чем операция сложения (о приоритетах операций говорится в разделе 4.13). Поэтому первое выражение сначала разыменовывает переменную ia и получает первый элемент массива, а затем прибавляет к нему 1. Второе же выражение доставляет значение второго элемента.
Проход по массиву можно осуществлять с помощью индекса, как мы делали это в предыдущем разделе, или с помощью указателей. Например:
|
#include <iostream> int main() { int ia[9] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 }; int *pbegin = ia; |
int *pend = ia + 9;
|
while ( pbegin != pend ) { cout << *pbegin <<; ++pbegin; |
}
Указатель
pbegin
инициализируется адресом первого элемента массива. Каждый проход по циклу увеличивает этот указатель на 1, что означает смещение его на следующий элемент. Как понять, где остановиться? В нашем примере мы определили второй указатель pend и инициализировали его адресом, следующим за последним элементом массива ia. Как только значение pbegin
станет равным pend, мы узнаем, что массив кончился.
Перепишем эту программу так, чтобы начало и конец массива передавались параметрами в некую обобщенную функцию, которая умеет печатать массив любого размера:
|
#inc1ude <iostream> void ia_print( int *pbegin, int *pend ) { while ( pbegin != pend ) { cout << *pbegin << ' '; ++pbegin; } } int main() { int ia[9] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 }; ia_print( ia, ia + 9 ); |
#inc1ude <iostream> template <c1ass e1emType> void print( elemType *pbegin, elemType *pend ) { while ( pbegin != pend ) { cout << *pbegin << ' '; ++pbegin; } |
int main() { int ia[9] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 }; double da[4] = { 3.14, 6.28, 12.56, 25.12 }; string sa[3] = { "piglet", "eeyore", "pooh" }; print( ia, ia+9 ); print( da, da+4 ); print( sa, sa+3 ); |
#include <a1gorithm> int main() { int ia[6] = { 107, 28, 3, 47, 104, 76 }; string sa[3] = { "piglet", "eeyore", "pooh" }; sort( ia, ia+6 ); sort( sa, sa+3 ); |
Как и в случае со встроенной операцией сложения, нам может понадобиться набор функций, выполняющих одно и то же действие, но над параметрами различных типов. Предположим, что мы хотим определить функции, возвращающие наибольшее из переданных значений параметров. Если бы не было перегрузки, пришлось бы каждой такой функции присвоить уникальное имя. Например, семейство функций max() могло бы выглядеть следующим образом:
|
int i_max( int, int ); int vi_max( const vector<int> & ); |
int matrix_max( const matrix & );
Однако все они делают одно и то же: возвращают наибольшее из значений параметров. С точки зрения пользователя, здесь лишь одна операция– вычисление максимума, а детали ее реализации большого интереса не представляют.
Отмеченная лексическая сложность отражает ограничение программной среды: всякое имя, встречающееся в одной и той же области видимости, должно относиться к уникальной сущности (объекту, функции, классу и т.д.). Такое ограничение на практике создает определенные неудобства, поскольку программист должен помнить или каким-то образом отыскивать все имена. Перегрузка функций помогает справиться с этой проблемой.
Применяя перегрузку, программист может написать примерно так:
int ix = max( j, k );
|
vector<int> vec; //... |
int iy = max( vec );
Этот подход оказывается чрезвычайно полезным во многих ситуациях.
Функцию-член можно объявить в любой из секций public, private или protected
тела класса. Где именно это следует делать? Открытая функция-член задает операцию, которая может понадобиться пользователю. Множество открытых функций-членов составляет интерфейс
класса. Например, функции-члены home(), move() и get() класса Screen
определяют операции, с помощью которых программа манипулирует объектами этого типа.
Поскольку мы прячем от пользователей внутреннее представление класса, объявляя его члены закрытыми, то для манипуляции объектами типа Screen необходимо предоставить открытые функции-члены. Такой прием– сокрытие информации – защищает написанный пользователем код от изменений во внутреннем представлении.
Внутреннее состояние объекта класса также защищено от случайных изменений. Все модификации объекта производятся с помощью небольшого набора функций, что существенно облегчает сопровождение и доказательство правильности программы.
До сих пор мы встречались лишь с функциями, поддерживающими доступ к закрытым членам только для чтения. Ниже приведены две функции set(), позволяющие пользователю модифицировать объект Screen. Добавим их объявления в тело класса:
|
class Screen { public: void set( const string &s ); void set( char ch ); // объявления других функций-членов не изменяются |
};
Далее следуют определения функций:
|
void Screen::set( const string &s ) { // писать в строку, начиная с текущей позиции курсора int space = remainingSpace(); int len = s.size(); if ( space < len ) { cerr << "Screen: warning: truncation: " << "space: " << space << "string length: " << len << endl; len = space; } _screen.replace( _cursor, len, s ); _cursor += len - 1; } void Screen::set( char ch ) { if ( ch == '\0' ) cerr << "Screen: warning: " << "null character (ignored).\n"; else _screen[_cursor] = ch; |
class Screen { public: // объявления других функций-членов не изменяются private: inline int remainingSpace(); |
inline int Screen::remainingSpace() { int sz = _width * _height; return ( sz - _cursor ); |
#include "Screen.h" #include <iostream> int main() { Screen sobj(3,3); // конструктор определен в разделе 13.3.4 string init("abcdefghi"); cout << "Screen Object ( " << sobj.height() << ", " << sobj.width() << " )\n\n"; // Задать содержимое экрана string::size_type initpos = 0; for ( int ix = 1; ix <= sobj.width(); ++ix ) for ( int iy = 1; iy <= sobj.height(); ++iy ) { sobj.move( ix, iy ); sobj.set( init[ initpos++ ] ); } // Напечатать содержимое экрана for ( int ix = 1; ix <= sobj.width(); ++ix ) { for ( int iy = 1; iy <= sobj.height(); ++iy ) cout << sobj.get( ix, iy ); cout << "\n"; } return 0; |
Третья форма наследования – это защищенное наследование. В таком случае все открытые члены базового класса становятся в производном классе защищенными, т.е. доступными из его дальнейших наследников, но не из любого места программы вне иерархии классов. Например, если бы нужно было унаследовать PeekbackStack от Stack, то закрытое наследование
|
// увы: при этом не ïîääåðæèâàåòñÿ дальнейшее наследование // PeekbackStack: все члены IntArray теперь закрыты |
class Stack : private IntArray { ... }
было бы чересчур ограничительным, поскольку закрытие членов IntArray в классе Stack
делает невозможным их последующее наследование. Для того чтобы поддержать наследование вида:
class PeekbackStack : public Stack { ... };
класс Stack
должен наследовать IntArray
защищенно:
class Stack : protected IntArray { ... };
Значение параметра по умолчанию– это значение, которое разработчик считает подходящим в большинстве случаев употребления функции, хотя и не во всех. Оно освобождает программиста от необходимости уделять внимание каждой детали интерфейса функции.
Значения по умолчанию для одного или нескольких параметров функции задаются с помощью того же синтаксиса, который употребляется при инициализации переменных. Например, функция для создания и инициализации двумерного массива, моделирующего экран терминала, может использовать такие значения для высоты, ширины и символа фона экрана:
|
char *screenInit( int height = 24, int width = 80, |
char background = ' ' );
Функция, для которой задано значение параметра по умолчанию, может вызываться по-разному. Если аргумент опущен, используется значение по умолчанию, в противном случае – значение переданного аргумента. Все следующие вызовы screenInit()
корректны:
|
char *cursor; // эквивалентно screenInit(24,80,' ') cursor = screenInit(); // эквивалентно screenInit(66,80,' ') cursor = screenlnit(66); // эквивалентно screenInit(66,256,' ') cursor = screenlnit(66, 256); |
cursor = screenlnit(66, 256, '#');
Фактические аргументы сопоставляются с формальными параметрами позиционно (в порядке следования), и значения по умолчанию могут использоваться только для подстановки вместо отсутствующих последних аргументов. В нашем примере невозможно задать значение для background, не задавая его для height и width.
|
// эквивалентно screenInit('?',80,' ') cursor = screenInit('?'); // ошибка, неэквивалентно screenInit(24,80,'?') |
cursor = screenInit( , ,'?');
При разработке функции с параметрами по умолчанию придется позаботиться об их расположении. Те, для которых значения по умолчанию вряд ли будут употребляться, необходимо поместить в начало списка. Функция screenInit() предполагает (возможно, основываясь на опыте применения), что параметр height
будет востребован пользователем наиболее часто.
// ошибка: width должна иметь значение по умолчанию, // если такое значение имеет height char *screenlnit( int height = 24, int width, |
// tf.h int ff( int = 0 ); // ft.С #include "ff.h" |
#include <cstdlib> |
#include "ff.h" |
#include "ff.h" |
#include "ff.h" int ff( int a, int b = 0, int с ); // правильно |
int aDefault(); int bDefault( int ); int cDefault( double = 7.8 ); int glob; int ff( int a = aDefault() , int b = bDefau1t( glob ) , |