В главе 6 для иллюстрации обсуждения абстрактных контейнерных типов мы частично реализовали систему текстового поиска и инкапсулировали ее в класс TextQuery. Однако мы не написали к ней никакой вызывающей программы, отложив реализацию поддержки формулирования запросов со стороны пользователя до рассмотрения объектно-ориентированного программирования. В этой главе язык запросов будет реализован в виде иерархии классов Query с одиночным наследованием. Кроме того, мы модифицируем и расширим класс TextQuery из главы 6 для получения полностью интегрированной системы текстового поиска.
Программа для запуска нашей системы текстового поиска будет выглядеть следующим образом:
|
#include "TextQuery.h" int main() { TextQuery tq; tq.build_up_text(); tq.query_text(); |
}
build_text_map() – это слегка видоизмененная функция-член doit() из главы 6. Ее основная задача – построить отображение для хранения позиций всех значимых слов текста. (Если помните, мы не храним семантически нейтральные слова типа союзов if, and, but и т.д. Кроме того, мы заменяем заглавные буквы на строчные и устраняем суффиксы, обозначающие множественное число: например, testifies преобразуется в testify, а marches в march.) С каждым словом ассоциируется вектор позиций, в котором хранятся номера строки и колонки каждого вхождения слова в текст.
query_text()
принимает запросы пользователя и преобразует их во внутреннюю форму на основе иерархии классов Query с одиночным наследованием и динамическим связыванием. Внутреннее представление запроса применяется к отображению слов на вектор позиций, построенному в build_text_map(). Ответом на запрос будет множество строк текстового файла, удовлетворяющих заданному критерию:
Enter a query - please separate each item by a space.
Terminate query (or session) with a dot( . ).
==> fiery && ( bird || shyly )
fiery ( 1 ) lines match
bird ( 1 ) lines match
shyly ( 1 ) lines match
После того как мы построили отображение, хотелось бы распечатать его содержимое. Мы можем сделать это, используя итератор, начальное и конечное значение которого получают с помощью функций-членов begin() и end(). Вот текст функции display_map_text():
|
void display_map_text( map<string,loc*> *text_map ) { typedef map<string,loc*> tmap; tmap::iterator iter = text_map->begin(), iter_end = text_map->end(); while ( iter != iter_end ) { cout << "word: " << (*iter).first << " ("; int loc_cnt = 0; loc *text_locs = (*iter).second; loc::iterator liter = text_locs->begin(), liter_end = text_locs->end(); while (liter != liter_end ) { if ( loc_cnt ) cout << ','; else ++loc_cnt; cout << '(' << (*liter).first << ',' << (*liter).second << ')'; ++liter; } cout << ")\n"; ++iter; } cout << endl; |
}
Если наше отображение не содержит элементов, данная функция не нужна. Проверить, пусто ли оно, можно с помощью функции-члена size():
|
if ( text_map->size() ) |
display_map_text( text_map );
Но более простым способом, без подсчета элементов, будет вызов функции-члена empty():
|
if ( ! text_map->empty() ) |
display_map_text( text_map );
Для проверки наших кодов реализуем небольшую функцию, выполняющую поиск по одному слову (поддержка языка запросов будет добавлена в главе 17). Если слово найдено, мы будем показывать каждую строку, в которой оно содержится. Слово может повторяться в строке, например:
tomorrow and tomorrow and tomorrow
однако такая строка будет представлена только один раз.
Одним из способов не учитывать повторное вхождение слова в строку является использование множества, как показано в следующем фрагменте кода:
|
// получим указатель на вектор позиций loc ploc = (*text_map)[ query_text ]; // переберем все позиции // вставим все номера строк в множество set< short > occurrence_lines; loc::iterator liter = ploc->begin(), liter_end = ploc->end(); while ( liter != liter_end ) { occurrence_lines.insert( occurrence_lines.end(), (*liter).first ); ++liter; |
}
Контейнер set не допускает дублирования ключей. Поэтому можно гарантировать, что occurrence_lines не содержит повторений. Теперь нам достаточно перебрать данное множество, чтобы показать все номера строк, где встретилось данное слово:
|
register int size = occurrence_lines.size(); cout << "\n" << query_text << " встречается " << size << " раз(а):") << "\n\n"; set< short >::iterator it=occurrence_lines.begin(); for ( ; it != occurrence_lines.end(); ++it ) { int line = -it; cout << "\t( строка " << line + 1 << " ) " << (*text_file)[line] << endl; |
}
(Полная реализация query_text() представлена в следующем разделе.)
Класс set
поддерживает операции size(), empty() и erase()
точно таким же образом, как и класс map, описанный выше. Кроме того, обобщенные алгоритмы предоставляют набор специфических функций для множеств, например set_union()
(объединение) и set_difference()
(разность). (Они использованы при реализации языка запросов в главе 17.)
Упражнение 6.23
Добавьте в программу множество слов, в которых заключающее 's' не подчиняется общим правилам и не должно удаляться. Примерами таких слов могут быть Pythagoras, Brahms и Burne_Jones. Включите в функцию suffix_s() из раздела 6.10 проверку этого набора.
Упражнение 6.24
Определите вектор, содержащий названия книг, которые вы собираетесь прочесть в ближайшие шесть виртуальных месяцев, и множество, включающее названия уже прочитанных произведений. Напишите программу, которая выбирает для вас книгу из вектора при условии, что вы ее еще не прочитали. Выбранное название программа должна заносить в множество прочитанных. Однако вы могли отложить книгу; следовательно, нужно обеспечить возможность удалять ее название из множества прочитанных. По окончании шести виртуальных месяцев распечатайте список прочитанного и непрочитанного.
Язык определяет набор стандартных преобразований между объектами встроенного типа, неявно выполняющихся компилятором в следующих случаях:
· арифметическое выражение с операндами разных типов: все операнды приводятся к наибольшему типу из встретившихся. Это называется арифметическим преобразованием. Например:
|
int ival = 3; double dva1 = 3.14159; // ival преобразуется в double: 3.0 |
ival + dva1;
· присваивание значения выражения одного типа объекту другого типа. В этом случае результирующим является тип объекта, которому значение присваивается. Так, в первом примере литерал 0
типа int
присваивается указателю типа int*, значением которого будет 0. Во втором примере double
преобразуется в int.
|
// 0 преобразуется в нулевой указатель типа int* int *pi = 0; // dva1 преобразуется в int: 3 |
ivat = dva1;
· передача функции аргумента, тип которого отличается от типа соответствующего формального параметра. Тип фактического аргумента приводится к типу параметра:
|
extern double sqrt( double ); // 2 преобразуется в double: 2.0 |
cout << "Квадратный корень из 2: " << sqrt( 2 ) << endt;
· возврат из функции значения, тип которого не совпадает с типом возвращаемого результата, заданным в объявлении функции. Тип фактически возвращаемого значения приводится к объявленному. Например:
|
double difference( int ivati, int iva12 ) { // результат преобразуется в double return ivati - iva12; |
}
У каждого объекта класса есть собственная копия данных-членов. Например:
|
int main() { Screen myScreen( 3, 3 ), bufScreen; myScreen.clear(); myScreen.move( 2, 2 ); myScreen.set( '*' ); myScreen.display(); bufScreen.resize( 5, 5 ); bufScreen.display(); |
}
У объекта myScreen
есть свои члены _width, _height, _cursor и _screen, а у объекта bufScreen – свои. Однако каждая функция-член класса существует в единственном экземпляре. Их и вызывают myScreen и bufScreen.
В предыдущем разделе мы видели, что функция-член может обращаться к членам своего класса, не используя операторы доступа. Так, определение функции move()
выглядит следующим образом:
|
inline void Screen::move( int r, int c ) { if ( checkRange( r, c ) ) // позиция на экране задана корректно? { int row = (r-1) * _width; // смещение строки _cursor = row + c - 1; } |
}
Если функция move()
вызывается для объекта myScreen, то члены _width и _height, к которым внутри нее имеются обращения, – это члены объекта myScreen. Если же она вызывается для объекта bufScreen, то и обращения производятся к членам данного объекта. Каким же образом _cursor, которым манипулирует move(), оказывается членом то myScreen, то bufScreen? Дело в указателе this.
Каждой функции-члену передается указатель на объект, для которого она вызвана, – this. В неконстантной функции-члене это указатель на тип класса, в константной – константный указатель на тот же тип, а в функции со спецификатором volatile
указатель с тем же спецификатором. Например, внутри функции-члена move()
класса Screen
указатель this
имеет тип Screen*, а в неконстантной функции-члене List – тип List*.
Поскольку this
адресует объект, для которого вызвана функция-член, то при вызове move() для myScreen он указывает на объект myScreen, а при вызове для bufScreen – на объект bufScreen. Таким образом, член _cursor, с которым работает функция move(), в первом случае принадлежит объекту myScreen, а во втором – bufScreen.
// псевдокод, показывающий, как происходит расширение // определения функции-члена // ЭТО НЕ КОРРЕКТНЫЙ КОД C++ inline void Screen::move( Screen *this, int r, int c ) { if ( checkRange( r, c ) ) { int row = (r-1) * this->_width; this->_cursor = row + c - 1; } |
inline void Screen::home() { this->_cursor = 0; |
Комментарии помогают человеку читать текст программы; писать их грамотно считается правилом хорошего тона. Комментарии могут характеризовать используемый алгоритм, пояснять назначение тех или иных переменных, разъяснять непонятные места. При компиляции комментарии выкидываются из текста программы поэтому размер получающегося исполняемого модуля не увеличивается.
В С++ есть два типа комментариев. Один– такой же, как и в С, использующий символы /* для обозначения начала и */ для обозначения конца комментария. Между этими парами символов может находиться любой текст, занимающий одну или несколько строк: вся последовательность между /* и */
считается комментарием. Например:
|
/* * Это первое знакомство с определением класса в C++. * Классы используются как в объектном, так и в * объектно-ориентированном программировании. Реализация * класса Screen представлена в главе 13. */ class Screen { /* Это называется телом класса */ public: void home(); /* переместить курсор в позицию 0,0 */ void refresh ();/* перерисовать экран */ private: /* Классы поддерживают "сокрытие информации" */ /* Сокрытие информации ограничивает доступ из */ /* программы к внутреннему представлению класса */ /* (его данным). Для этого используется метка */ /* "private:" */ int height, width; |
}
Слишком большое число комментариев, перемежающихся с кодом программы, может ухудшить читаемость текста. Например, объявления переменных width и height в данном тексте окружены комментариями и почти не заметны. Рекомендуется писать развернутое объяснение перед блоком текста. Как и любая программная документация, комментарии должны обновляться в процессе модификации кода. Увы, нередко случается, что они относятся к устаревшей версии.
Комментарии в стиле С не могут быть вложенными. Попробуйте откомпилировать нижеследующую программу в своей системе. Большинство компиляторов посчитают ее ошибочной:
#include <iostream> /* комментарии /* */ не могут быть вложенными. * Строку "не вкладываются" компилятор рассматривает, * как часть программы. Это же относится к данной и следующей строкам */ int main() { cout << "Здравствуй, мир\n"; |
/* * Первое знакомство с определением класса в C++. * Классы используются как в объектном, так и в * объектно-ориентированном программировании. Реализация * класса Screen представлена в главе 13. */ class Screen { // Это называется телом класса public: void home(); // переместить курсор в позицию 0,0 void refresh (); // перерисовать экран private: /* Классы поддерживают "сокрытие информации". */ /* Сокрытие информации ограничивает доступ из */ /* программы к внутреннему представлению класса */ /* (его данным). Для этого используется метка */ /* "private:" */ int height, width; |
Наличие в одном и том же классе конвертеров, выполняющих неявные преобразования во встроенные типы, и перегруженных операторов может приводить к неоднозначности при выборе между ними. Например, есть следующее определение класса String с функцией сравнения:
|
class String { // ... public: String( const char * = 0 ); bool operator== ( const String & ) const; // нет оператора operator== ( const char * ) |
};
и такое использование оператора operator==:
|
String flower( "tulip" ); void foo( const char *pf ) { // вызывается перегруженный оператор String::operator==() if ( flower == pf ) cout << pf << " is a flower!\en"; // ... |
}
Тогда при сравнении
flower == pf
вызывается оператор равенства класса String:
String::operator==( const String & ) const;
Для трансформации правого операнда pf из типа const char* в тип String
параметра operator==()
применяется определенное пользователем преобразование, которое вызывает конструктор:
String( const char * )
Если добавить в определение класса String
конвертер в тип const char*:
|
class String { // ... public: String( const char * = 0 ); bool operator== ( const String & ) const; operator const char*(); // новый конвертер |
};
то показанное использование operator==()
становится неоднозначным:
|
// проверка на равенство больше не компилируется! |
if (flower == pf)
Из-за добавления конвертера operator const
char*()
встроенный оператор сравнения
bool operator==( const char *, const char * )
тоже считается устоявшей функцией. С его помощью левый операнд flower
типа String
может быть преобразован в тип const char *.
Теперь для использования operator==() в foo()
есть две устоявших операторных функции. Первая из них
String::operator==( const String & ) const;
требует применения определенного пользователем преобразования правого операнда pf из типа const char* в тип String. Вторая
bool operator==( const char *, const char * )
namespace NS { class complex { complex( double ); // ... }; class LongDouble { friend LongDouble operator+( LongDouble &, int ) { /* ... */ } public: LongDouble( int ); operator double(); LongDouble operator+( const complex & ); // ... }; LongDouble operator+( const LongDouble &, double ); } int main() { NS::LongDouble ld(16.08); double res = ld + 15.05; // какой operator+? return 0; |
Заголовочный файл предоставляет место для всех extern-объявлений объектов, объявлений функций и определений встроенных функций. Это называется локализацией объявлений. Те исходные файлы, где объект или функция определяется или используется, должны включать
заголовочный файл.
Такие файлы позволяют добиться двух целей. Во-первых, гарантируется, что все исходные файлы содержат одно и то же объявление для глобального объекта или функции. Во-вторых, при необходимости изменить объявление это изменение делается в одном месте, что исключает возможность забыть внести правку в какой-то из исходных файлов.
Пример с addToken()
имеет следующий заголовочный файл:
|
// ----- token.h ----- typedef unsigned char uchar; const uchar INLINE = 128; // ... const uchar IT = ...; const uchar GT = ...; extern uchar lastTok; extern int addToken( uchar ); inline bool is_relational( uchar tok ) { return (tok >= LT && tok <= GT); } // ----- lex.C ----- #include "token.h" // ... // ----- token.C ----- #include "token.h" |
// ...
При проектировании заголовочных файлов нужно учитывать несколько моментов. Все объявления такого файла должны быть логически связанными. Если он слишком велик или содержит слишком много не связанных друг с другом элементов, программисты не станут включать его, экономя на времени компиляции. Для уменьшения временных затрат в некоторых реализациях С++ предусматривается использование предкомпилированных заголовочных файлов. В руководстве к компилятору сказано, как создать такой файл из обычного. Если в вашей программе используются большие заголовочные файлы, применение предкомпиляции может значительно сократить время обработки.
Чтобы это стало возможным, заголовочный файл не должен содержать объявлений встроенных (inline) функций и объектов. Любая из следующих инструкций является определением и, следовательно, не может быть использована в заголовочном файле:
|
extern int ival = 10; double fica_rate; |
// ----- заголовочный файл ----- const int buf_chunk = 1024; extern char *const bufp; // ----- исходный файл ----- |
// ошибка: не должно быть в заголовочном файле |
(a) extern int ix = 1024; (b) int iy; (c) extern void reset( void *p ) { /* ... */ } (d) extern const int *pi; |
(a) int var; (b) inline bool is_equal( const SmallInt &, const SmallInt & ){ } (c) void putValues( int *arr, int size ); (d) const double pi = 3.1416; |
Объединение – это специальный вид класса. Данные-члены хранятся в нем таким образом, что перекрывают друг друга. Все члены размещаются, начиная с одного и того же адреса. Для объединения отводится столько памяти, сколько необходимо для хранения самого большого его члена. В любой момент времени можно присвоить значение лишь одному такому члену.
Рассмотрим пример, иллюстрирующий использование объединения. Лексический анализатор, входящий в состав компилятора, разбивает программу на последовательность лексем. Так, инструкция
int i = 0;
преобразуется в последовательность из пяти лексем:
1. Ключевое слово int.
2. Идентификатор i.
3. Оператор =
4. Константа 0
типа int.
5. Точка с запятой.
Лексический анализатор передает эти лексемы синтаксическому анализатору, парсеру, который идентифицирует полученную последовательность. Полученная информация должна дать парсеру возможность распознать эту последовательность лексем как объявление. Для этого с каждой лексемой ассоциируется информация, позволяющая парсеру увидеть следующее:
|
Type ID Assign Constant Semicolon |
(Тип ИД Присваивание Константа Точка с запятой)
Далее парсер анализирует значения каждой лексемы. В данном случае он видит:
|
Type <==> int ID <==> i |
Constant <==> 0
Для Assign и Semicolon
дополнительной информации не нужно, так как у них может быть только одно значение: соответственно := и ;.
Таким образом, в представлении лексемы могло бы быть два члена – token и value. token – это уникальный код, показывающий, что лексема имеет тип Type, ID, Assign, Constant или Semicolon, например 85 для ID и 72 для Semicolon.value
содержит конкретное значение лексемы. Так, для лексемы ID в предыдущем объявлении value
будет содержать строку "i", а для лексемы Type – некоторое представление типа int.
Представление члена value
union TokenValue { char _cval; int _ival; char *_sval; double _dval; |
// объект типа TokenValue TokenValue last_token; // указатель на объект типа TokenValue |
last_token._ival = 97; |
union TokenValue { public: char _cval; // ... private: int priv; } int main() { TokenValue tp; tp._cval = '\n'; // правильно // ошибка: main() не может обращаться к закрытому члену // TokenValue::priv tp.priv = 1024; |
union illegal_members { Screen s; // ошибка: есть конструктор Screen *ps; // правильно static int is; // ошибка: статический член int &rfi; // ошибка: член-ссылка |
union TokenValue { public: TokenValue(int ix) : _ival(ix) { } TokenValue(char ch) : _cval(ch) { } // ... int ival() { return _ival; } char cval() { return _cval; } private: int _ival; char _cval; // ... }; int main() { TokenValue tp(10); int ix = tp.ival(); //... |
enum TokenKind ( ID, Constant /* и другие типы лексем */ } class Token { public: TokenKind tok; TokenValue val; |
int lex() { Token curToken; char *curString; int curIval; // ... case ID: // идентификатор curToken.tok = ID; curToken.val._sval = curString; break; case Constant: // целая константа curToken.tok = Constant; curToken.val._ival = curIval; break; // ... и т.д. |
char *idVal; // проверить значение дискриминанта перед тем, как обращаться к sval if ( curToken.tok == ID ) |
#include <cassert> // функции доступа к члену объединения sval string Token::sval() { assert( tok==ID ); return val._sval; |
class Token { public: TokenKind tok; // имя типа объединения опущено union { char _cval; int _ival; char *_sval; double _dval; } val; |
class Token { public: TokenKind tok; // анонимное объединение union { char _cval; int _ival; char *_sval; double _dval; }; |
int lex() { Token curToken; char *curString; int curIval; // ... выяснить, что находится в лексеме // ... затем установить curToken case ID: curToken.tok = ID; curToken._sval = curString; break; case Constant: // целая константа curToken.tok = Constant; curToken._ival = curIval; break; // ... и т.д. |
Из чего складывается объектно-ориентированное проектирование четырех рассмотренных выше видов запросов? Как решаются проблемы их внутреннего представления?
С помощью наследования можно определить взаимосвязи между независимыми классами запросов. Для этого мы вводим в рассмотрение абстрактный класс Query, который будет служить для них базовым
(соответственно сами эти классы будут считаться производными). Абстрактный класс можно представить себе как неполный, который становится более или менее завершенным, когда из него порождаются производные классы, – в нашем случае AndQuery, OrQuery, NotQuery и NameQuery.
В нашем абстрактном классе Query
определены данные и функции-члены, общие для всех четырех типов запроса. При порождении из Query
производного класса, скажем AndQuery, мы выделяем уникальные характеристики каждого вида запроса. К примеру, NameQuery – это специальный вид Query, в котором операндом всегда является строка. Мы будем называть NameQuery производным и говорить, что Query
является его базовым классом. (То же самое относится и к классам, представляющим другие типы запросов.) Производный класс наследует данные и функции-члены базового и может обращаться к ним непосредственно, как к собственным членам.
Основное преимущество иерархии наследования в том, что мы программируем открытый интерфейс абстрактного базового класса, а не отдельных производных от него специализированных типов, что позволяет защитить наш код от последующих изменений иерархии. Например, мы определяем eval() как открытую виртуальную функцию абстрактного базового класса Query. Пользовательский код, записанный в виде:
_rop->eval();
экранирован от любых изменений в языке запросов. Это не только позволяет добавлять, модифицировать и удалять типы, не изменяя программы пользователя, но и освобождает автора нового вида запроса от необходимости заново реализовывать поведение или действия, общие для всех типов в иерархии. Такая гибкость достигается за счет двух характеристик механизма наследования: полиморфизма
// pquery может адресовать любой из классов, производных от Query void eval( const Query *pquery ) { pquery->eval(); |
int main() { AndQuery aq; NotQuery notq; OrQuery *oq = new OrQuery; NameQuery nq( "Botticelli" ); // правильно: любой производный от Query класс // компилятор автоматически преобразует в базовый класс eval( &aq ); eval( ¬q ); eval( oq ); eval( &nq ); |
int main() { string name( "Scooby-Doo" ); // ошибка: тип string не является производным от Query eval( &name ); |
// полиморфизма нет int *pi; // нет поддержанного языком полиморфизма void *pvi; // pquery может адресовать объект любого производного от Query класса |
if ( NameQuery *pnq = |
class AndQuery { public: // ... private: Query *_lop; Query *_rop; |

![]() |
![]() |
![]() |
![]() |
|
class Query { ... }; class AndQuery : public Query { ... }; class OrQuery : public Query { ... }; class NotQuery : public Query { ... }; |
|
// ошибка: Query должен быть определен class Query; |
|
// ошибка: опережающее объявление не должно // включать списка базовых классов |
|
// опережающее объявление как производного, // так и обычного класса содержит только имя класса class Query; |
книга аудио-книга аудиокассета детская кукла видеокассета видеоигра для приставки SEGA книга с подневной оплатой видеоигра для приставки SONY |
Вспомним спецификацию нашего массива в предыдущем разделе. Мы говорили о том, что некоторым пользователям может понадобиться упорядоченный массив, в то время как большинство, скорее всего, удовлетворится и неупорядоченным. Если представить себе, что наш массив IntArray
упорядочен, то реализация таких функций, как min(), max(), find(), должна отличаться от их реализации для массива неупорядоченного большей эффективностью. Вместе с тем, для поддержания массива в упорядоченном состоянии все прочие функции должны быть сильно усложнены.
Мы выбрали наиболее общий случай – неупорядоченный массив. Но как же быть с теми немногочисленными пользователями, которым обязательно нужна функциональность массива упорядоченного? Мы должны специально для них создать другой вариант массива?
А вот и еще одна категория недовольных пользователей: их не удовлетворяют накладные расходы на проверку правильности индекса. Мы исходили из того, что корректность работы нашего класса превыше всего, и старались обезопасить себя от ошибочных ситуаций. Но возьмем, к примеру, разработчиков систем виртуальной реальности. Трехмерные изображения должны строиться с максимально возможной скоростью, быть может, за счет точности.
Да, мы можем удовлетворить и тех и других, создав для каждой группы пользователей свой, немного модернизированный, вариант IntArray. Более того, его даже не слишком трудно сделать, поскольку мы старались создать хорошую реализацию и необходимые изменения затронут совсем небольшие участки кода. Итак, копируем исходный текст, вносим необходимые изменения в нужные места и получаем три класса:
|
// неупорядоченный массив без проверки границ индекса class IntArray { ... }; // неупорядоченный массив с проверкой границ индекса class IntArrayRC { ... }; // упорядоченный массив без проверки границ индекса |
class IntSortedArray { ... };
Подобное решение имеет следующие недостатки:
· нам необходимо сопровождать три копии кода, различающиеся весьма незначительно. Хорошо бы выделить общие участки кода. Кроме упрощения сопровождения, это позволит использовать их впоследствии, если мы захотим создать еще один вариант массива, например упорядоченный с проверкой границ индекса;
void process_array (IntArray&); void process_array (IntArrayRC&); |
#include <IntArray.h> void swap (IntArray &ia, int i, int j) { int temp ia[i]; ia[i] = ia[j]; ia[j] = temp; } // ниже идут обращения к функции swap: IntArray ia; IntArrayRC iarc; IntSortedArray ias; // правильно - ia имеет тип IntArray swap (ia,0,10); // правильно - iarc является подклассом IntArray swap (iarc,0,10); // правильно - ias является подклассом IntArray swap (ias,0,10); |
class IntArray { public: // конструкторы explicit IntArray (int sz = DefaultArraySize); IntArray (int *array, int array_size); IntArray (const IntArray &rhs); // виртуальный деструктор virtual ~IntArray() { delete[] ia; } // операции сравнения: bool operator== (const IntArray&) const; bool operator!= (const IntArray&) const; // операция присваивания: IntArray& operator= (const IntArray&); int size() const { return _size; }; // мы убрали проверку индекса... |
void init (IntArray &ia) |
#ifndef IntArrayRC_H #define IntArrayRC_H #include "IntArray.h" class IntArrayRC : public IntArray { public: IntArrayRC( int sz = DefaultArraySize ); IntArrayRC( const int *array, int array_size ); IntArrayRC( const IntArrayRC &rhs ); virtual int& operator[]( int ) const; private: void check_range( int ix ); }; |
IntArrayRC::operator[]( int index ) { check_range( index ); return _ia[ index ]; |
#include <cassert> inline void IntArrayRC::check_range(int index) { assert (index>=0 && index < _size); |
int ia[] = {0,1,1,2,3,5,8,13}; |
inline IntArrayRC::IntArrayRC( int sz ) : IntArray( sz ) {} inline IntArrayRC::IntArrayRC( const int *iar, int sz ) |
#include <iostream> #include "IntArray.h" #include "IntArrayRC.h" void swap( IntArray &ia, int ix, int jx ) { int tmp = ia[ ix ]; ia[ ix ] = ia[ jx ]; ia[ jx ] = tmp; } int main() { int array[ 4 ] = { 0, 1, 2, 3 }; IntArray ia1( array, 4 ); IntArrayRC ia2( array, 4 ); // ошибка: должно быть size-1 // не может быть выявлена объектом IntArray cout << "swap() with IntArray ia1" << endl; swap( ia1, 1, ia1.size() ); // правильно: объект IntArrayRC "поймает" ошибку cout << "swap() with IntArrayRC ia2" << endl; swap( ia2, 1, ia2.size() ); return 0; |
(a) функция-член является подвидом функции (b) функция-член является подвидом класса (c) конструктор является подвидом функции-члена (d) самолет является подвидом транспортного средства (e) машина является подвидом грузовика (f) круг является подвидом геометрической фигуры (g) квадрат является подвидом треугольника (h) автомобиль является подвидом самолета |
(a) rotate(); (b) print(); (c) size(); (d) DateBorrowed(); // дата выдачи книги (e) rewind(); (f) borrower(); // читатель (g) is_late(); // книга просрочена |
(a) Точка (b) Служащий (c) Фигура (d) Телефонный_номер (e) Счет_в_банке |
В этом разделе мы спроектируем и реализуем абстракцию массива, используя механизм классов С++. Первоначальный вариант будет поддерживать только массив элементов типа int. Впоследствии при помощи шаблонов мы расширим наш массив для поддержки любых типов данных.
Первый шаг состоит в том, чтобы определить, какие операции будет поддерживать наш массив. Конечно, было бы заманчиво реализовать все мыслимые и немыслимые операции, но невозможно сделать сразу все на свете. Поэтому для начала определим то, что должен уметь наш массив:
1.
обладать некоторыми знаниями о самом себе. Пусть для начала это будет знание собственного размера;
2. поддерживать операцию присваивания и операцию сравнения на равенство;
3. отвечать на некоторые вопросы, например: какова величина минимального и максимального элемента; содержит ли массив элемент с определенным значением; если да, то каков индекс первого встречающегося элемента, имеющего это значение;
4. сортировать сам себя. Пусть такая операция покажется излишней, все-таки реализуем ее в качестве дополнительного упражнения: ведь кому-то это может пригодиться.
5. Конечно, мы должны реализовать и базовые операции работы с массивом, а именно:Возможность задать размер массива при его создании. (Речь не идет о том, чтобы знать эту величину на этапе компиляции.)
6. Возможность проинициализировать массив некоторым набором значений.
7. Возможность обращаться к элементу массива по индексу. Пусть эта возможность реализуется с помощью стандартной операции взятия индекса.
8. Возможность обнаруживать обращения к несуществующим элементам массива и сигнализировать об ошибке. Не будем обращать внимание на тех потенциальных пользователей нашего класса, которые привыкли работать со встроенными массивами С и не считают данную возможность полезной – мы хотим создать такой массив, который был бы удобен в использовании даже самым неискушенным программистам на С++.
class classname { public: // набор открытых операций private: // закрытые функции, обеспечивающие реализацию |
// статический объект типа IntArray |
// объявление класса IntArray |
class IntArray { public: // операции сравнения: #2b bool operator== (const IntArray&) const; bool operator!= (const IntArray&) const; // операция присваивания: #2a IntArray& operator= (const IntArray&); int size() const; // #1 void sort(); // #4 int min() const; // #3a int max() const; // #3b // функция find возвращает индекс первого // найденного элемента массива // или -1, если элементов не найдено |
// инициализация переменной min_val |
int min_val = myArray.min(); |
// инструкция присваивания - // вызывает функцию-член myArray0.operator=(myArray1) myArray0 = myArray1; // инструкция сравнения - // вызывает функцию-член myArray0.operator==(myArray1) if (myArray0 == myArray1) |
class IntArray { public: // ... int size() const { return _size; } private: // внутренние данные-члены |
IntArray array; |
for (int index=0; index<array.size(); ++index) |
// список перегруженных функций min() // каждая функция отличается от других списком параметров #include <string> int min (const int *pia,int size); int min (int, int); int min (const char *str); char min (string); |
class IntArray { public: explicit IntArray (int sz = DefaultArraySize); IntArray (int *array, int array_size); IntArray (const IntArray &rhs); // ... private: static const int DefaultArraySize = 12; |
IntArray::IntArray (int sz) { // инициализация членов данных _size = sz; ia = new int[_size]; // инициализация элементов массива for (int ix=0; ix<_size; ++ix) ia[ix] = 0; |
int ia[10] = {0,1,2,3,4,5,6,7,8,9}; |
IntArray::IntArray (int *array, int sz) { // инициализация членов данных _size = sz; ia = new int[_size]; // инициализация элементов массива for (int ix=0; ix<_size; ++ix) ia[ix] = array[ix]; |
IntArray::IntArray (const IntArray &rhs ) |
class IntArray { public: explicit IntArray (int sz = DefaultArraySize); IntArray (int *array, int array_size); IntArray (const IntArray &rhs); // ... private: void init (int sz,int *array); // ... }; // функция, используемая всеми конструкторами void IntArray::init (int sz,int *array) { _size = sz; ia = new int[_size]; for (int ix=0; ix<_size; ++ix) if ( !array ) ia[ix] = 0; else ix[ix] = array[ix]; } // модифицированные конструкторы IntArray::IntArray (int sz) { init(sz,0); } IntArray::IntArray (int *array, int array_size) { init (array_size,array); } IntArray::IntArray (const IntArray &rhs) |
class IntArray { |
IntArray array; |
#include <cassert> int& IntArray::operator[] (int index) { assert (index >= 0 && index < _size); return ia[index]; |
Наша функция min()
дает хороший пример как возможностей, так и ограничений механизма шаблонов:
|
template <typename Type> const Type& min( const Type *p, int size ) { Type minval = p[ 0 ]; for ( int ix = 1; ix < size; ++ix ) if ( p[ ix ] < minval ) minval = p[ ix ]; return minval; |
}
Достоинство этого механизма – возможность определить единственный шаблон min(), который конкретизируется для бесконечного множества типов. Ограничение же заключается в том, что даже при такой конкретизации min()
будет работать не со всеми.
Это ограничение вызвано использованием оператора “меньше”: в некоторых случаях базовый тип его не поддерживает. Так, класс изображения Image
может и не предоставлять реализации такого оператора, но мы об этом не знаем и пытаемся найти минимальный кадр анимации в данном массиве изображений. Однако попытка конкретизировать min() для такого массива приведет к ошибке компиляции:
error: invalid types applied to the < operator: Image < Image
(ошибка: оператор < применен к некорректным типам: Image < Image)
Возможна и другая ситуация: оператор “меньше” существует, но имеет неподходящую семантику. Например, если мы хотим найти наименьшую строку, но при этом принимать во внимание только буквы, не учитывая регистр, то такой реализованный в классе оператор не даст нужного результата.
Традиционное решение состоит в том, чтобы параметризовать оператор сравнения. В данном случае это можно сделать, объявив указатель на функцию, принимающую два аргумента и возвращающую значение типа bool:
|
template < typename Type, bool (*Comp)(const Type&, const Type&)> const Type& min( const Type *p, int size, Comp comp ) { Type minval = p[ 0 ]; for ( int ix = 1; ix < size; ++ix ) if ( Comp( p[ ix ] < minval )) minval = p[ ix ]; return minval; |
}
Такое решение вместе с нашей первой реализацией на основе встроенного оператора “меньше” обеспечивает универсальную поддержку для любого типа, включая и класс Image, если только мы придумаем подходящую семантику для сравнения двух изображений. Основной недостаток указателя на функцию связан с низкой эффективностью, так как косвенный вызов не дает воспользоваться преимуществами встроенных функций.
Image im1("foreground.tiff"), im2("background.tiff"); // ... // вызывает Image AddImages::operator()(const Image1&, const Image2&); |
template < typename Type, typename Comp > const Type& min( const Type *p, int size, Comp comp ) { Type minval = p[ 0 ]; for ( int ix = 1; ix < size; ++ix ) if ( Comp( p[ ix ] < minval )) minval = p[ ix ]; return minval; |
Объявлением исключения в catch-обработчике могут быть объявления типа или объекта. В каких случаях это следует делать? Тогда, когда необходимо получить значение или как-то манипулировать объектом, созданным в выражении throw. Если классы исключений спроектированы так, что в объектах-исключениях при возбуждении сохраняется некоторая информация и если в объявлении исключения фигурирует такой объект, то инструкции внутри catch-обработчика могут обращаться к информации, сохраненной в объекте выражением throw.
Изменим реализацию класса исключения pushOnFull, сохранив в объекте-исключении то значение, которое не удалось поместить в стек. Catch-обработчик, сообщая об ошибке, теперь будет выводить его в cerr. Для этого мы сначала модифицируем определение типа класса pushOnFull
следующим образом:
|
// новый класс исключения: // он сохраняет значение, которое не удалось поместить в стек class pushOnFull { public: pushOnFull( int i ) : _value( i ) { } int value { return _value; } private: int _value; |
};
Новый закрытый член _value
содержит число, которое не удалось поместить в стек. Конструктор принимает значение типа int и сохраняет его в члене _data. Вот как вызывается этот конструктор для сохранения значения из выражения throw:
|
void iStack::push( int value ) { if ( full() ) // значение, сохраняемое в объекте-исключении throw pushOnFull( value ); // ... |
}
У класса pushOnFull
появилась также новая функция-член value(), которую можно использовать в catch-обработчике для вывода хранящегося в объекте-исключении значения:
|
catch ( pushOnFull eObj ) { cerr << "trying to push value " << eObj.value() << " on a full stack\n"; |
}
Обратите внимание, что в объявлении исключения в catch-обработчике фигурирует объект eObj, с помощью которого вызывается функция-член value() класса pushOnFull.
Объект-исключение всегда создается в точке возбуждения, даже если выражение throw – это не вызов конструктора и, на первый взгляд, не должно создавать объекта. Например:
enum EHstate { noErr, zeroOp, negativeOp, severeError }; enum EHstate state = noErr; int mathFunc( int i ) { if ( i == 0 ) { state = zeroOp; throw state; // создан объект-исключение } // иначе продолжается обычная обработка |
void calculate( int op ) { try { mathFunc( op ); } catch ( EHstate eObj ) { // eObj - копия сгенерированного объекта-исключения } |
void calculate( int op ) { try { mathFunc( op ); } catch ( EHstate &eObj ) { // eObj ссылается на сгенерированный объект-исключение } |
void calculate( int op ) { try { mathFunc( op ); } catch ( EHstate &eObj ) { // исправить ошибку, вызвавшую исключение eObj = noErr; // глобальная переменная state не изменилась } |
Если сгенерированный объект-исключение имеет тип производного класса, а обрабатывается catch-обработчиком для базового, то этот обработчик не может использовать особенности производного класса. Например, к функции-члену value(), которая объявлена в классе pushOnFull, нельзя обращаться в catch-обработчике Excp:
|
catch ( const Excp &eObj ) { // ошибка: в классе Excp нет функции-члена value() cerr << "попытка поместить значение " << eObj.value() << " в полный стек\n"; |
}
Но мы можем перепроектировать иерархию классов исключений и определить виртуальные функции, которые можно вызывать из catch-обработчика для базового класса Excp с целью получения доступа к функциям-членам более специализированного производного:
|
// новые определения классов, включающие виртуальные функции class Excp { public: virtual void print( string msg ) { cerr << "Произошло исключение" << endl; } |
};
|
class stackExcp : public Excp { }; class pushOnFull : public stackExcp { public: virtual void print() { cerr << "попытка поместить значение " << _value << " в полный стек\n"; } // ... |
};
Функцию print()
теперь можно использовать в catch-обработчике следующим образом:
|
int main() { try { // iStack::push() возбуждает исключение pushOnFull } catch ( Excp eObj ) { eObj.print(); // хотим вызвать виртуальную функцию, // но вызывается экземпляр из базового класса } |
}
Хотя возбужденное исключение имеет тип pushOnFull, а функция print()
виртуальна, инструкция eObj.print() печатает такую строку:
Произошло исключение
Вызываемая print()
является членом базового класса Excp, а не замещает ее в производном. Но почему?
Вспомните, что объявление исключения в catch-обработчике ведет себя почти так же, так объявление параметра. Когда управление попадает в catch-обработчик, то, поскольку в нем объявлен объект, а не ссылка, eObj
int main() { try { // iStack::push() возбуждает исключение pushOnFull } catch ( const Excp &eObj ) { eObj.print(); // вызывается виртуальная функция // pushOnFull::print() } |
Определение класса, например Screen, не приводит к выделению памяти. Память выделяется только тогда, когда определяется объект типа класса. Так, если имеется следующая реализация Screen:
|
class Screen { public: // функции-члены private: string _screen; string:size_type _cursor; short _height; short _width; |
};
то определение
Screen myScreen;
выделяет область памяти, достаточную для хранения четырех членов Screen. Имя myScreen
относится к этой области. У каждого объекта класса есть собственная копия данных-членов. Изменение членов myScreen не отражается на значениях членов любого другого объекта типа Screen.
Область видимости объекта класса зависит от его положения в тексте программы. Он определяется в иной области, нежели сам тип класса:
|
class Screen { // список членов }; int main() { Screen mainScreen; |
}
Тип Screen
объявлен в глобальной области видимости, тогда как объект mainScreen – в локальной области функции main().
Объект класса также имеет время жизни. В зависимости от того, где (в области видимости пространства имен или в локальной области) и как (статическим или нестатическим) он объявлен, он может существовать в течение всего времени выполнения программы или только во время вызова некоторой функции. Область видимости объекта класса и его время жизни ведут себя очень похоже. (Понятия области видимости и времени жизни введены в главе 8.)
Объекты одного и того же класса можно инициализировать и присваивать друг другу. По умолчанию копирование объекта класса эквивалентно копированию всех его членов. Например:
|
Screen bufScreen = myScreen; // bufScreen._height = myScreen._height; // bufScreen._width = myScreen._width; // bufScreen._cursor = myScreen._cursor; |
// bufScreen._screen = myScreen._screen;
Указатели и ссылки на объекты класса также можно объявлять. Указатель на тип класса разрешается инициализировать адресом объекта того же класса или присвоить ему такой адрес. Аналогично ссылка инициализируется l-значением объекта того же класса. (В объектно-ориентированном программировании указатель или ссылка на объект базового класса могут относиться и к объекту производного от него класса.)
int main() { Screen myScreen, bufScreen[10]; Screen *ptr = new Screen; myScreen = *ptr; delete ptr; ptr = bufScreen; Screen &ref = *ptr; Screen &ref2 = bufScreen[6]; |
#include "Screen.h" bool isEqual( Screen& s1, Screen *s2 ) { // возвращает false, если объекты не равны, и true - если равны if (s1.height() != s2->height() || s2.width() != s2->width() ) return false; for ( int ix = 0; ix < s1.height(); ++ix ) for ( int jy = 0; jy < s2->width(); ++jy ) if ( s1.get( ix, jy ) != s2->get( ix, jy ) ) return false; return true; // попали сюда? значит, объекты равны |
class Screen { public: int height() { return _height; } int width() { return _width; } // ... private: short _heigh, _width; // ... |
О классе говорят, что он определен, как только встретилась скобка, закрывающая его тело. После этого становятся известными все члены класса, а следовательно, и его размер.
Можно объявить класс, не определяя его. Например:
class Screen; // объявление класса Screen
Это объявление вводит в программу имя Screen и указывает, что оно относится к типу класса.
Тип объявленного, но еще не определенного класса допустимо использовать весьма ограниченно. Нельзя определять объект типа класса, если сам класс еще не определен, поскольку размер класса в этом момент неизвестен и компилятор не знает, сколько памяти отвести под объект.
Однако указатель или ссылку на объект такого класса объявлять можно, так как они имеют фиксированный размер, не зависящий от типа. Но, поскольку размеры класса и его членов неизвестны, применять оператор разыменования (*) к такому указателю, а также использовать указатель или ссылку для обращения к члену не разрешается, пока класс не будет полностью определен.
Член некоторого класса можно объявить принадлежащим к типу какого-либо класса только тогда, когда компилятор уже видел определение этого класса. До этого объявляются лишь члены, являющиеся указателями или ссылками на такой тип. Ниже приведено определение StackScreen, один из членов которого служит указателем на Screen, который объявлен, но еще не определен:
|
class Screen; // объявление class StackScreen { int topStack; // правильно: указатель на объект Screen Screen *stack; void (*handler)(); |
};
Поскольку класс не считается определенным, пока не закончилось его тело, то в нем не может быть данных-членов его собственного типа. Однако класс считается объявленным, как только распознан его заголовок, поэтому в нем допустимы члены, являющиеся ссылками или указателями на его тип. Например:
|
class LinkScreen { Screen window; LinkScreen *next; LinkScreen *prev; |
};
Упражнение 13.1
Пусть дан класс Person со следующими двумя членами:
|
string _name; |
string _address;
и такие функции-члены:
|
Person( const string &n, const string &s ) : _name( n ), _address( a ) { } string name() { return _name; } |
string address() { return _address; }
Какие члены вы объявили бы в секции public, а какие– в секции private? Поясните свой выбор.
Упражнение 13.2
Объясните разницу между объявлением и определением класса. Когда вы стали бы использовать объявление класса? А определение?
При объявлении объекта класса Screen
константным возникают некоторые проблемы. Предполагается, что после инициализации объекта Screen, его содержимое уже нельзя изменять. Но это не должно мешать нам читать содержимое экрана. Рассмотрим следующий константный объект класса Screen:
const Screen cs ( 5, 5 );
Если мы хотим прочитать символ, находящийся в позиции (3,4), то попробуем сделать так:
|
// прочитать содержимое экрана в позиции (3,4) // Увы! Это не работает cs.move( 3, 4 ); |
char ch = cs.get();
Но такая конструкция не работает: move() – это не константная функция-член, и сделать ее таковой непросто. Определение move()
выглядит следующим образом:
|
inline void Screen::move( int r, int c ) { if ( checkRange( r, c ) ) { int row = (r-1) * _width; _cursor = row + c - 1; // модифицирует _cursor } |
}
Обратите внимание, что move()изменяет член класса _cursor, следовательно, не может быть объявлена константной.
Но почему нельзя модифицировать _cursor для константного объекта класса Screen? Ведь _cursor – это просто индекс. Изменяя его, мы не модифицируем содержимое экрана, а лишь пытаемся установить позицию внутри него. Модификация _cursor
должна быть разрешена несмотря на то, что у класса Screen
есть спецификатор const.
Чтобы разрешить модификацию члена класса, принадлежащего константному объекту, объявим его изменчивым (mutable). Член с таким спецификатором не бывает константным, даже если он член константного объекта. Его можно обновлять, в том числе функцией-членом со спецификатором const. Объявлению изменчивого члена класса должно предшествовать ключевое слово mutable:
|
class Screen { public: // функции-члены private: string _screen; mutable string::size_type _cursor; // изменчивый член short _height; short _width; |
};
Теперь любая константная функция способна модифицировать _cursor, и move()
может быть объявлена константной. Хотя move() изменяет данный член, компилятор не считает это ошибкой.
// move() - константная функция-член inline void Screen::move( int r, int c ) const { // ... // правильно: константная функция-член может модифицировать члены // со спецификатором mutable _cursor = row + c - 1; // ... |
Screen myScreen; |
Для указания виртуального наследования в объявление базового класса вставляется модификатор virtual. Так, в данном примере ZooAnimal
становится виртуальным базовым для Bear и Raccoon:
|
// взаимное расположение ключевых слов public и virtual // несущественно class Bear : public virtual ZooAnimal { ... }; |
class Raccoon : virtual public ZooAnimal { ... };
Виртуальное наследование не является явной характеристикой самого базового класса, а лишь описывает его отношение к производному. Как мы уже отмечали, виртуальное наследование – это разновидность композиции по ссылке. Иначе говоря, доступ к подобъекту и его нестатическим членам косвенный, что обеспечивает гибкость, необходимую для объединения нескольких виртуально унаследованных подобъектов базовых классов в один разделяемый экземпляр внутри производного. В то же время объектом производного класса можно манипулировать через указатель или ссылку на тип базового, хотя последний является виртуальным. Например, все показанные ниже преобразования базовых классов Panda выполняются корректно, хотя Panda
использует виртуальное наследование:
|
extern void dance( const Bear* ); extern void rummage( const Raccoon* ); extern ostream& operator<<( ostream&, const ZooAnimal& ); int main() { Panda yin_yang; dance( &yin_yang ); // правильно rummage( &yin_yang ); // правильно cout << yin_yang; // правильно // ... |
}
Любой класс, который можно задать в качестве базового, разрешается сделать виртуальным, причем он способен содержать все те же элементы, что обычные базовые классы. Так выглядит объявление ZooAnimal:
|
#include <iostream> #include <string> class ZooAnimal; extern ostream& operator<<( ostream&, const ZooAnimal& ); class ZooAnimal { public: ZooAnimal( string name, bool onExhibit, string fam_name ) : _name( name ), _onExhibit( onExhibit ), _fam_name( fam_name ) {} virtual ~ZooAnimal(); virtual ostream& print( ostream& ) const; string name() const { return _name; } string family_name() const { return _fam_name; } // ... protected: bool _onExhibit; string _name; string _fam_name; // ... |
class Bear : public virtual ZooAnimal { public: enum DanceType { two_left_feet, macarena, fandango, waltz }; Bear( string name, bool onExhibit=true ) : ZooAnimal( name, onExhibit, "Bear" ), _dance( two_left_feet ) {} virtual ostream& print( ostream& ) const; void dance( DanceType ); // ... protected: DanceType _dance; // ... |
class Raccoon : public virtual ZooAnimal { public: Raccoon( string name, bool onExhibit=true ) : ZooAnimal( name, onExhibit, "Raccoon" ), _pettable( false ) {} virtual ostream& print( ostream& ) const; bool pettable() const { return _pettable; } void pettable( bool petval ) { _pettable = petval; } // ... protected: bool _pettable; // ... |
· обычный (не шаблонный) дружественный класс или дружественная функция. В следующем примере функция foo(), функция-член bar() и класс foobar
объявлены друзьями всех конкретизаций шаблона QueueItem:
|
class Foo { void bar(); }; template <class T> class QueueItem { friend class foobar; friend void foo(); friend void Foo::bar(); // ... |
};
Ни класс foobar, ни функцию foo() не обязательно объявлять или определять в глобальной области видимости перед объявлением их друзьями шаблона QueueItem.
Однако перед тем как объявить другом какой-либо из членов класса Foo, необходимо определить его. Напомним, что член класса может быть введен в область видимости только через определение объемлющего класса. QueueItem не может ссылаться на Foo::bar(), пока не будет найдено определение Foo;
· связанный
дружественный шаблон класса или функции. В следующем примере определено взаимно однозначное соответствие между классами, конкретизированными по шаблону QueueItem, и их друзьями – также конкретизациями шаблонов. Для каждого класса, конкретизированного по шаблону QueueItem, ассоциированные конкретизации foobar, foo() и Queue::bar()
являются друзьями.
|
template <class Type> class foobar { ... }; template <class Type> void foo( QueueItem<Type> ); template <class Type> class Queue { void bar(); // ... }; template <class Type> class QueueItem { friend class foobar<Type>; friend void foo<Type>( QueueItem<Type> ); friend void Queue<Type>::bar(); // ... |
};
Прежде чем шаблон класса можно будет использовать в объявлениях друзей, он сам должен быть объявлен или определен. В нашем примере шаблоны классов foobar и Queue, а также шаблон функции foo()
template <class Type> class QueueItem { template <class T> friend class foobar; template <class T> friend void foo( QueueItem<T> ); template <class T> friend class Queue<T>::bar(); // ... |
Поскольку QueueItem не предназначен для непосредственного использования в вызывающей программе, то объявление конструктора этого класса помещено в закрытую секцию шаблона. Теперь класс Queue
необходимо объявить другом QueueItem, чтобы можно было создавать и манипулировать объектами последнего.
Существует два способа объявить шаблон класса другом. Первый заключается в том, чтобы объявить любой экземпляр Queue
другом любого экземпляра QueueItem:
|
template <class Type> class QueueItem { // любой экземпляр Queue является другом // любого экземпляра QueueItem template <class T> friend class Queue; |
};
Однако нет смысла объявлять, например, класс Queue, конкретизированный типом string, другом QueueItem, конкретизированного типом complex<double>. Queue<string>
должен быть другом только для класса QueueItem<string>. Таким образом, нам нужно взаимно однозначное соответствие между экземплярами Queue и QueueItem, конкретизированными одинаковыми типами. Чтобы добиться этого, применим второй метод объявления друзей:
|
template <class Type> class QueueItem { // для любого экземпляра QueueItem другом является // только конкретизированный тем же типом экземпляр Queue friend class Queue<Type>; // ... |
};
Данное объявление говорит о том, что для любой конкретизации QueueItem некоторым типом экземпляр Queue, конкретизированный тем же типом, является другом. Так, экземпляр Queue, конкретизированный типом int, будет другом экземпляра QueueItem, тоже конкретизированного типом int. Но для экземпляров QueueItem, конкретизированных типами complex<double> или string, этот экземпляр Queue
другом не будет.
В любой точке программы у пользователю может понадобиться распечатать содержимое объекта Queue. Такая возможность предоставляется с помощью перегруженного оператора вывода. Этот оператор должен быть объявлен другом шаблона Queue, так как ему необходим доступ к закрытым членам класса. Какой же будет его сигнатура?
// как задать аргумент типа Queue? |
template <class Type> ostream& |
template <class Type> ostream& operator<<( ostream &os, const Queue<Type> &q ) { os << "< "; QueueItem<Type> *p; for ( p = q.front; p; p = p->next ) os << *p << " "; os << " >"; return os; |
template <class Type> class Queue { friend ostream& operator<<( ostream &, const Queue<Type> & ); // ... |
template <class Type> ostream& operator<<( ostream &os, const QueueItem<Type> &qi ) { os << qi.item; return os; |
template <class Type> class QueueItem { friend class Queue<Type>; friend ostream& operator<<( ostream &, const QueueItem<Type> & ); // ... |
#include <iostream> #include "Queue.h" int main() { Queue<int> qi; // конкретизируются оба экземпляра // ostream& operator<<(ostream &os, const Queue<int> &) // ostream& operator<<(ostream &os, const QueueItem<int> &) cout << qi << endl; int ival; for ( ival = 0; ival < 10; ++ival ) qi.add( ival ); cout << qi << endl; int err_cnt = 0; for ( ival = 0; ival < 10; ++ival ) { int qval = qi.remove(); if ( ival != qval ) err_cnt++; } cout << qi << endl; if ( !err_cnt ) cout << "!! queue executed ok\n"; else cout << "?? queue errors: " << err_cnt << endl; return 0; |
Как было сказано в главе 7, объявление
функции устанавливает ее имя, а также тип возвращаемого значения и список параметров. Определение функции, помимо этой информации, задает еще и тело– набор инструкций, заключенных в фигурные скобки. Функция должна быть объявлена перед вызовом. Например:
|
// объявление функции calc() // определение находится в другом файле void calc(int); int main() { int loc1 = get(); // ошибка: get() не объявлена calc(loc1); // правильно: calc() объявлена // ... |
}
Определение объекта имеет две формы:
|
type_specifier object_name; |
type_specifier object_name = initializer;
Вот, например, определение obj1. Здесь obj1
инициализируется значением 97:
int obj1 = 97;
Следующая инструкция задает obj2, хотя начальное значение не задано:
int obj2;
Объект, определенный в глобальной области видимости без явной инициализации, гарантированно получит нулевое значение. Таким образом, в следующих двух примерах и var1, и var2
будут равны нулю:
|
int var1 = 0; |
int var2;
Глобальный объект можно определить в программе только один раз. Поскольку он должен быть объявлен в исходном файле перед использованием, то для программы, состоящей из нескольких файлов, необходима возможность объявить объект, не определяя его. Как это сделать?
С помощью ключевого слова extern, аналогичного объявлению функции: оно указывает, что объект определен в другом месте – в этом же исходном файле или в другом. Например:
extern int i;
Эта инструкция “обещает”, что в программе имеется определение, подобное
int i;
extern-объявление не выделяет места под объект. Оно может встретиться несколько раз в одном и том же исходном файле или в разных файлах одной программы. Однако обычно находится в общедоступном заголовочном файле, который включается в те модули, где необходимо использовать глобальный объект:
|
// заголовочный файл extern int obj1; extern int obj2; // исходный файл int obj1 = 97; |
int obj2;
Объявление глобального объекта с указанием ключевого слова extern и с явной инициализацией считается определением. Под этот объект выделяется память, и другие определения не допускаются:
|
extern const double pi = 3.1416; // определение |
const double pi; // ошибка: повторное определение pi
Ключевое слово extern
может быть указано и при объявлении функции – для явного обозначения его подразумеваемого смысла: “определено в другом месте”. Например:
extern void putValues( int*, int );
Теперь, научившись объявлять, определять и использовать функции в программах, познакомимся с перегрузкой – еще одним аспектом в C++. Перегрузка позволяет иметь несколько одноименных функций, выполняющих схожие операции над аргументами разных типов.
Вы уже воспользовались предопределенной перегруженной функцией. Например, для вычисления выражения
1 + 3
вызывается операция целочисленного сложения, тогда как вычисление выражения
1.0 + 3.0
осуществляет сложение с плавающей точкой. Выбор той или иной операции производится незаметно для пользователя. Операция сложения перегружена, чтобы обеспечить работу с операндами разных типов. Ответственность за распознавание контекста и применение операции, соответствующей типам операндов, возлагается на компилятор, а не на программиста.
В этой главе мы покажем, как определять собственные перегруженные функции.
Функции-члены класса можно перегружать:
|
class myClass { public: void f( double ); char f( char, char ); // перегружает myClass::f( double ) // ... |
};
Как и в случае функций, объявленных в пространстве имен, функции-члены могут иметь одинаковые имена при условии, что списки их параметров различны либо по числу параметров, либо по их типам. Если же объявления двух функций-членов отличаются только типом возвращаемого значения, то второе объявление считается ошибкой компиляции:
|
class myClass { public: void mf(); double mf(); // ошибка: так перегружать нельзя // ... |
};
В отличие от функций в пространствах имен, функции-члены должны быть объявлены только один раз. Если даже тип возвращаемого значения и списки параметров двух функций-членов совпадают, то второе объявление компилятор трактует как неверное повторное объявление:
|
class myClass { public: void mf(); void mf(); // ошибка: повторное объявление // ... |
};
Все функции из множества перегруженных должны быть объявлены в одной и той же области видимости. Поэтому функции-члены никогда не перегружают функций, объявленных в пространстве имен. Кроме того, поскольку у каждого класса своя область видимости, функции, являющиеся членами разных классов, не перегружают друг друга.
Множество перегруженных функций-членов может содержать как статические, так и нестатические функции:
|
class myClass { public: void mcf( double ); static void mcf( int* ); // перегружает myClass::mcf( double ) // ... |
};
Какая из функций-членов будет вызвана– статическая или нестатическая – зависит от результатов разрешения перегрузки. Процесс разрешения в ситуации, когда устояли как статические, так и нестатические члены, мы подробно рассмотрим в следующем разделе.
Каждое имя в С++ программе должно относиться к уникальной сущности (объекту, функции, типу или шаблону). Это не значит, что оно встречается только один раз во всей программе: его можно повторно использовать для обозначения другой сущности, если только есть некоторый контекст, помогающий различить разные значения одного и того же имени. Контекстом, служащим для такого различения, служит область видимости. В С++ поддерживается три их типа: локальная область видимости, область видимости пространства имен
и область видимости класса.
Локальная область– это часть исходного текста программы, содержащаяся в определении функции (или в блоке). Любая функция имеет собственную такую часть, и каждая составная инструкция (или блок) внутри функции также представляет собой отдельную локальную область.
Область видимости пространства имен – часть исходного текста программы, не содержащаяся внутри объявления или определения функции или определения класса. Самая внешняя часть называется глобальной областью видимости или глобальной областью видимости пространства имен.
Объекты, функции, типы и шаблоны могут быть определены в глобальной области видимости. Программисту разрешено задать пользовательские
пространства имен, заключенные внутри глобальной области с помощью определения пространства имен. Каждое такое пространство является отдельной областью видимости. Пользовательское пространство, как и глобальное, может содержать объявления и определения объектов, функций, типов и шаблонов, а также вложенные пользовательские пространства имен. (Они рассматриваются в разделах 8.5 и 8.6.)
Каждое определение класса представляет собой отдельную область видимости класса. (О таких областях мы расскажем в главе 13.)
Имя может обозначать различные сущности в зависимости от области видимости. В следующем фрагменте программы имя s1
относится к четырем разным сущностям:
|
#include <iostream> #include <string> // сравниваем s1 и s2 лексикографически int lexicoCompare( const string &sl, const string &s2 ) { ... } // сравниваем длины s1 и s2 int sizeCompare( const string &sl, const string &s2 ) { ... } typedef int ( PFI)( const string &, const string & ); // сортируем массив строк void sort( string *s1, string *s2, PFI compare =lexicoCompare ) { ... } string sl[10] = { "a", "light", "drizzle", "was", "falling", "when", "they", "left", "the", "school" }; int main() { // вызов sort() со значением по умолчанию параметра compare // s1 - глобальный массив sort( s1, s1 + sizeof(s1)/sizeof(s1[0]) - 1 ); // выводим результат сортировки for ( int i = 0; i < sizeof(s1) / sizeof(s1[0]); ++i ) cout << s1[ i ].c_str() << "\n\t"; |
В этой главе обсуждаются два важных вопроса, касающиеся объявлений в С++. Где употребляется объявленное имя? Когда можно безопасно использовать объект или вызывать функцию, т.е. каково время жизни сущности в программе? Для ответа на первый вопрос мы введем понятие областей видимости и покажем, как они ограничивают применение имен в исходном файле программы. Мы рассмотрим разные типы таких областей: глобальную и локальную, а также более сложное понятие областей видимости пространств имен, которое появится в конце главы. Отвечая на второй вопрос, мы опишем, как объявления вводят глобальные объекты и функции (сущности, “живущие” в течение всего времени работы программы), локальные (“живущие” на определенном отрезке выполнения) и динамически размещаемые объекты (временем жизни которых управляет программист). Мы также исследуем свойства времени выполнения, характерные для этих объектов и функций.
Тело класса определяет область видимости. Объявления членов класса внутри тела вводят их имена в область видимости класса.
Для обращения к ним применяются операторы доступа (точка и стрелка) и оператор разрешения области видимости (::). Когда употребляется оператор доступа, то предшествующее ему имя обозначает объект или указатель на объект типа класса, а следующее за ним имя должно находиться в области видимости этого класса. Аналогично при использовании оператора разрешения области видимости поиск имени, следующего за ним, идет в области видимости класса, имя которого стоит перед оператором. (В главах 17 и 18 мы увидим, что производный класс может обращаться к членам своих базовых.)
Однако применение операторов доступа или оператора разрешения области видимости нужно не всегда. Некоторые части программы сами по себе находятся в области видимости класса, и в них к членам класса можно обращаться напрямую. Одной из таких частей является само определение класса. Имя его члена можно использовать в теле после объявления:
|
class String { public: typedef int index_type; // тип параметра - это на самом деле String::index_type char& operator[]( index_type ) |
};
Порядок объявления членов класса в его теле важен: нельзя ссылаться на члены, которые будут объявлены позже. Например, если объявление оператора operator[]()
находится раньше объявления typedef index_type, то приведенное ниже объявление operator[]() оказывается ошибочным, поскольку в нем используется еще неизвестное имя index_type:
|
class String { public: // ошибка: имя index_type не объявлено char &operator[]( index_type ); typedef int index_type; |
};
Однако из этого правила есть два исключения. Первое касается имен, использованных в определениях встроенных функций-членов, второе – имен, применяемых как аргументы по умолчанию. Рассмотрим обе ситуации.
Разрешение имен в определениях встроенных функций-членов происходит в два этапа. Сначала объявление функции (т.е. тип возвращаемого значения и список параметров) обрабатывается в том месте, где оно встретилось в определении класса. Затем тело функции обрабатывается во всей области видимости, сразу после того, как были просмотрены объявления всех членов. Посмотрим на наш пример, в котором оператор operator[]()
class String { public: typedef int index_type; char &operator[]( index_type elem ) { return _string[ elem ]; } private: char *_string; |
class Screen { public: // bkground относится к статическому члену, // объявленному позже в определении класса Screen& clear( char = bkground ); private: static const char bkground = '#'; |
class Screen { public: // ... // ошибка: bkground - нестатический член Screen& clear( char = bkground ); private: const char bkground = '#'; |
class String { public: typedef int index_type; char& operator[]( index_type ); private: char *_string; }; // в operator[]() есть обращения к index_type и _string inline char& operator[]( index_type elem ) { return _string[ elem ]; |
class Account: // ... private: static double _interestRate; static double initInterestRate(); }; // ссылается на Account::initInterest() |
class Account: // ... private: static const int nameSize = 16; static const char name[nameSize]; // nameSize не квалифицировано именем класса Account |
class Account { typedef double Money; //... private: static Money _interestRate; static Money initInterest(); }; // Money должно быть квалифицировано именем класса Account:: |
У каждого класса есть собственная область видимости, в которой определены имена членов и вложенные типы (см. разделы 13.9 и 13.10). При наследовании область видимости производного класса вкладывается в область видимости непосредственного базового. Если имя не удается разрешить в области видимости производного класса, то поиск определения продолжается в области видимости базового.
Именно эта иерархическая вложенность областей видимости классов при наследовании и делает возможным обращение к именам членов базового класса так, как если бы они были членами производного. Рассмотрим сначала несколько примеров одиночного наследования, а затем перейдем к множественному. Предположим, есть упрощенное определение класса ZooAnimal:
|
class ZooAnimal { public: ostream &print( ostream& ) const; // сделаны открытыми только ради демонстрации разных случаев string is_a; int ival; private: double dval; |
};
и упрощенное определение производного класса Bear:
|
class Bear : public ZooAnimal { public: ostream &print( ostream& ) const; // сделаны открытыми только ради демонстрации разных случаев string name; int ival; |
};
Когда мы пишем:
|
Bear bear; |
bear.is_a;
то имя разрешается следующим образом:
· bear – это объект класса Bear. Сначала поиск имени is_a
ведется в области видимости Bear. Там его нет.
· Поскольку класс Bear
производный от ZooAnimal, то далее поиск is_a
ведется в области видимости последнего. Обнаруживается, что имя принадлежит его члену. Разрешение закончилось успешно.
Хотя к членам базового класса можно обращаться напрямую, как к членам производного, они сохраняют свою принадлежность к базовому классу. Как правило, не имеет значения, в каком именно классе определено имя. Но это становится важным, если в базовом и производном классах есть одноименные члены. Например, когда мы пишем:
int ival; int Bear::mumble( int ival ) { return ival + // обращение к параметру ::ival + // обращение к глобальному объекту ZooAnimal::ival + Bear::ival; |
int dval; int Bear::mumble( int ival ) { // ошибка: разрешается в пользу закрытого члена ZooAnimal::dval return ival + dval; |
int dval; int Bear::mumble( int ival ) { foo( dval ); // ... |
ostream& Bear::print( ostream &os) const { // вызывается ZooAnimal::print(os) ZooAnimal::print( os ); os << name; return os; |
Как влияет множественное наследование на алгоритм просмотра области видимости класса? Все непосредственные базовые классы просматриваются одновременно, что может приводить к неоднозначности в случае, когда в нескольких из них есть одноименные члены. Рассмотрим на нескольких примерах, как возникает неоднозначность и какие меры можно предпринять для ее устранения. Предположим, есть следующий набор классов:
|
class Endangered { public: ostream& print( ostream& ) const; void highlight(); // ... }; class ZooAnimal { public: bool onExhibit() const; // ... private: bool highlight( int zoo_location ); // ... }; class Bear : public ZooAnimal { public: ostream& print( ostream& ) const; void dance( dance_type ) const; // ... |
};
Panda объявляется производным от двух классов:
|
class Panda : public Bear, public Endangered { public: void cuddle() const; // ... |
};
Хотя при наследовании функций print() и highlight() из обоих базовых классов Bear и Endangered
имеется потенциальная неоднозначность, сообщение об ошибке не выдается до момента явно неоднозначного обращения к любой из этих функций.
В то время как неоднозначность двух унаследованных функций print() очевидна с первого взгляда, наличие конфликта между членами highlight() удивляет (ради этого пример и составлялся): ведь у них разные уровни доступа и разные прототипы. Более того, экземпляр из Endangered – это член непосредственного базового класса, а из ZooAnimal – член класса, стоящего на две ступеньки выше в иерархии.
Однако все это не имеет значения (впрочем, как мы скоро увидим, может иметь, но в случае виртуального наследования). Bear наследует закрытую функцию-член highlight() из ZooAnimal; лексически она видна, хотя вызывать ее из Bear или Panda
запрещено. Значит, Panda
наследует два лексически видимых члена с именем highlight, поэтому любое неквалифицированное обращение к этому имени приводит к ошибке компиляции.
int main() { Panda yin_yang; yin_yang.dance( Bear::macarena ); |
void Panda::mumble() { dance( Bear::macarena ); // ... |
// правильно: Bear::dance() |
int main() { // ошибка: неоднозначность: одна из // Bear::print( ostream& ) const // Endangered::print( ostream& ) const Panda yin_yang; yin_yang.print( cout ); |
int main() { // правильно, но не лучшее решение Panda yin_yang; yin_yang.Bear::print( cout ); |
class Base { public: void print( string ) const; // ... }; class Derived1 : public Base { public: void print( int ) const; // ... }; class Derived2 : public Base { public: void print( double ) const; // ... }; class MI : public Derived1, public Derived2 { public: void print( complex<double> ) const; // ... |
MI mi; string dancer( "Nejinsky" ); |
inline void Panda::highlight() { Endangered::highlight(); } inline ostream& Panda::print( ostream &os ) const { Bear::print( os ); Endangered::print( os ); return os; |
class Base1 { public: // ... protected: int ival; double dval; char cval; // ... private: int *id; // ... }; class Base2 { public: // ... protected: float fval; // ... private: double dval; // ... }; class Derived : public Base1 { public: // ... protected: string sval; double dval; // ... }; class MI : public Derived, public Base2 { public: // ... protected: int *ival; complex<double> cval; // ... |
int ival; double dval; void MI:: foo( double dval ) { int id; // ... |
void MI:: bar() { int sval; // вопрос упражнения относится к коду, начинающемуся с этого места ... } (a) dval = 3.14159; (d) fval = 0; (b) cval = 'a'; (e) sval = *ival; |
int id; void MI:: foobar( float cval ) { int dval; // вопросы упражнения относятся к коду, начинающемуся с этого места ... |
Операции, описанные в предыдущих разделах, составляют набор, поддерживаемый непосредственно контейнерами vector и deque. Согласитесь, что это весьма небогатый интерфейс и ему явно не хватает базовых операций find(), sort(), merge() и т.д. Планировалось вынести общие для всех контейнеров операции в набор обобщенных алгоритмов, которые могут применяться ко всем контейнерным типам, а также к массивам встроенных типов. (Обобщенные алгоритмы описываются в главе 12 и в Приложении.) Эти алгоритмы связываются с определенным типом контейнера с помощью передачи им в качестве параметров пары соответствующих итераторов. Вот как выглядят вызовы алгоритма find() для списка, вектора и массива разных типов:
|
#include <list> #include <vector> int ia[ 6 ] = { 0, 1, 2, 3, 4, 5 }; vector<string> svec; list<double> dtist; // соответствующий заголовочный файл #include <algorithm> vector<string>::iterator viter; list<double>::iterator liter; #int *pia; // find() возвращает итератор на найденный элемент // для массива возвращается указатель ... pia = find( &ia[0], &ia[6], some_int_value ); liter = find( dlist.begin(), dlist.end(), some_double_value ); |
viter = find( svec.begin(), svec.end(), some_string_value );
Контейнер list
поддерживает дополнительные операции, такие, как sort() и merge(), поскольку в нем не реализован произвольный доступ к элементам. (Эти операции описаны в разделе 12.6.)
Теперь вернемся к нашей поисковой системе.
Упражнение 6.11
Напишите программу, в которой определены следующие объекты:
|
int ia[] = { 1, 5, 34 }; int ia2[] = { 1, 2, 3 }; int ia3[] = { 6, 13, 21, 29, 38, 55, 67, 89 }; |
vector<int> ivec;
Используя различные операции вставки и подходящие значения ia, ia2 и ia3, модифицируйте вектор ivec
так, чтобы он содержал последовательность:
{
0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89 }
Упражнение 6.12
Напишите программу, определяющую данные объекты:
|
int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89 }; |
list<int> ilist( ia, ia+11 );
Используя функцию-член erase() с одним параметром, удалите из ilist все нечетные элементы.
В нашу реализацию класса Array
(см. главу 2) мы включили функции-члены для поддержки операций min(), max() и sort(). Однако в стандартном классе vector эти, на первый взгляд фундаментальные, операции отсутствуют. Для нахождения минимального или максимального значения элементов вектора следует вызвать один из обобщенных алгоритмов. Алгоритмами они называются потому, что реализуют такие распространенные операции, как min(), max(), find() и sort(), а обобщенными (generic)– потому, что применимы к различным контейнерным типам: векторам, спискам, массивам. Контейнер связывается с применяемым к нему обобщенным алгоритмом посредством пары итераторов (мы говорили о них в разделе 6.5), указывающих, какие элементы следует посетить при обходе контейнера. Специальные объекты-функции
позволяют переопределить семантику операторов в обобщенных алгоритмах. Итак, в этой главе рассматриваются обобщенные алгоритмы, объекты-функции и итераторы.
Первые два аргумента любого обобщенного алгоритма (разумеется, есть исключения, которые только подтверждают правило) – это пара итераторов, обычно называемых first и last, ограничивающих диапазон элементов внутри контейнера или встроенного массива, к которым применяется этот алгоритм. Как правило, диапазон элементов (иногда его называют интервалом с включенной левой границей) обозначается следующим образом:
|
// читается так: включает первый и все последующие элементы, // кроме последнего |
[ first, last )
Эта запись говорит о том, что диапазон начинается с элемента first и продолжается до элемента last, исключая последний. Если
first == last
то говорят, что диапазон пуст.
К паре итераторов предъявляется следующее требование: если начать с элемента first и последовательно применять оператор инкремента, то возможно достичь элемента last. Однако компилятор не в состоянии проверить выполнение этого ограничения; если оно нарушается, поведение программы не определено, обычно все заканчивается аварийным остановом и дампом памяти.
В объявлении каждого алгоритма указывается минимально необходимая категория итератора (см. раздел 12.4). Например, для алгоритма find(), реализующего однопроходный обход контейнера с доступом только для чтения, требуется итератор чтения, но можно передать и однонаправленный или двунаправленный итератор, а также итератор с произвольным доступом. Однако передача итератора записи приведет к ошибке. Не гарантируется, что ошибки, связанные с передачей итератора не той категории, будут обнаружены во время компиляции, поскольку категории итераторов – это не собственно типы, а лишь параметры-типы, передаваемые шаблону функции.
Некоторые алгоритмы существуют в нескольких версиях: в одной используется встроенный оператор, а во второй – объект-функция или указатель на функцию, которая предоставляет альтернативную реализацию оператора. Например, unique() по умолчанию сравнивает два соседних элемента с помощью оператора равенства, определенного для типа объектов в контейнере. Но если такой оператор равенства не определен или мы хотим сравнивать элементы иным способом, то можно передать либо объект-функцию, либо указатель на функцию, обеспечивающую нужную семантику. Встречаются также алгоритмы с похожими, но разными именами. Так, предикатные версии всегда имеют имя, оканчивающееся на _if, например find_if(). Скажем, есть алгоритм replace(), реализованный с помощью встроенного оператора равенства, и replace_if(), которому передается объект-предикат или указатель на функцию.
В этом приложении мы рассмотрим все алгоритмы. Мы решили расположить их в алфавитном порядке (за небольшими исключениями), чтобы проще было найти нужный. Каждый алгоритм представлен в следующем виде: сначала описывается прототип функции, затем сам алгоритм, причем особое внимание уделяется интуитивно неочевидным особенностям, и, наконец, приводится пример программы, показывающий, как можно данный алгоритм использовать.
Первыми двумя аргументами всех обобщенных алгоритмов (естественно, не без исключений) является пара итераторов, обычно first и last, обозначающих диапазон элементов внутри контейнера или встроенного массива, над которым работает алгоритм. Этот диапазон (часто называемый интервалом с включенной левой границей), как правило, записывается в виде:
|
// следует читать: включая first и все последующие // элементы до last, но не включая сам last |
[ first, last )
Это означает, что диапазон начинается с first и заканчивается last, однако сам элемент last
не включается. Если
first == last
то говорят, что диапазон пуст.
К паре итераторов предъявляется такое требование: last должен быть достижим, если начать с first и последовательно применять оператор инкремента. Однако компилятор не может проверить выполнение данного ограничения. Если требование не будет выполнено, поведение программы не определено; обычно это заканчивается ее крахом и дампом памяти.
В объявлении каждого алгоритма подразумевается минимальная поддержка, которую должны обеспечить итераторы (краткое обсуждение пяти категорий итераторов см. в разделе 12.4). Например, алгоритм find(), реализующий однопроходный обход контейнера и выполняющий только чтение, требует итератора чтения InputIterator. Ему также можно передать одно- или двунаправленный итератор или итератор с произвольным доступом. Однако передача итератора записи приведет к ошибке. Не гарантируется, что подобные ошибки (при передаче итератора неподходящей категории) будут обнаружены компилятором, поскольку категории итераторов– это не сами типы, а лишь параметры, которыми конкретизируется шаблон функции.
Наш класс ilist
имеет серьезный недостаток: он может хранить элементы только целого типа. Если бы он мог содержать элементы любого типа– как встроенного, так и определенного пользователем, – то его область применения была бы гораздо шире. Модифицировать ilist для поддержки произвольных типов данных позволяет механизм шаблонов (см. главу 16).
При использовании шаблона вместо параметра подставляется реальный тип данных. Например:
list< string > slist;
создает экземпляр списка, способного содержать объекты типа string, а
list< int > ilist;
создает список, в точности повторяющий наш ilist. С помощью шаблона класса можно обеспечить поддержку произвольных типов данных одним экземпляром кода. Рассмотрим последовательность действий, уделив особое внимание классу list_item.
Определение шаблона класса начинается ключевым словом template, затем следует список параметров в угловых скобках. Параметр представляет собой идентификатор, перед которым стоит ключевое слово class или typename. Например:
|
template <class elemType> |
class list_item;
Эта инструкция объявляет list_item
шаблоном класса с единственным параметром-типом. Следующее объявление эквивалентно предыдущему:
|
template <typename elemType> |
class list_item;
Ключевые слова class и typename
имеют одинаковое значение, можно использовать любое из них. Более удобное для запоминания typename появилось в стандарте С++ сравнительно недавно и поддерживается еще не всеми компиляторами. Поскольку наши тексты были написаны до появления этого ключевого слова, в них употребляется class. Шаблон класса list_item
выглядит так:
|
template <class elemType> class list_item { public: list_item( elemType value, list_item *item = 0 ) : _value( value ) { if ( !item ) _next = 0; else { _next = item->_next; item->_next = this; } } elemType value() { return _value; } list_item* next() { return _next; } void next( list_item *link ) { _next = link; } void value( elemType new_value ) { _value = new_value; } private: elemType _value; list_item *_next; |
template <class elemType> class list { public: list() : _at_front( 0 ), _at_end( 0 ), _current( 0 ), _size( 0 ) {} 1ist( const list& ); list& operator=( const list& ); ~list() { remove_all(); } void insert ( list_item<elemType> *ptr, elemType value ); void insert_end( elemType value ); void insert_front( elemType value ); void insert_all( const list &rhs ); int remove( elemType value ); void remove_front(); void remove_all(); list_item<elemType> *find( elemType value ); list_item<elemType> *next_iter(); list_item<elemType>* init_iter( list_item<elemType> *it ); void disp1ay( ostream &os = cout ); void concat( const list& ); void reverse (); int size() { return _size; } private: void bump_up_size() { ++_size; } void bump_down_size() { --_size; } list_item<elemType> *_at_front; 1ist_item<elemType> *_at_end; list_item<elemType> *_current; int _size; |
namespace Primer_Third_Edition { template <typename elemType> class list_item{ ... }; template <typename elemType> class list{ ... }; // ... |
// наш заголовочный файл #include "list.h" // сделаем наши определения видимыми в программе using namespace Primer_Third_Edition; // теперь можно использовать наш класс list list< int > ilist; |
ilist_item::~ilist_item() { delete _next; |
void ilist::remove_end(); |
class ilist { public: // ... ilist_item* find( int value, ilist_item *start_at = 0 ); // ... |
void ilist:: insert( ilist_item *begin, int *array_of_value, |
ilist_item *it = mylist.find( 1 ); |
ilist ilist::reverse_copy(); |
После того как мы разбили каждую строку на слова, необходимо избавиться от знаков препинания. Пока из строки
magical but untamed. "Daddy, shush, there is no such thing,"
у нас получился такой набор слов:
magical
but
untamed.
"Daddy,
shush,
there
is
no
such
thing,"
Как нам теперь удалить ненужные знаки препинания? Для начала определим строку, содержащую все символы, которые мы хотим удалить:
string filt_elems( "\",.;:!?)(\\/" );
(Обратная косая черта указывает на то, что следующий за ней символ должен в данном контексте восприниматься буквально, а не как специальная величина. Так, \"
обозначает символ двойной кавычки, а не конец строки, а \\ – символ обратной косой черты.)
Теперь можно применить функцию-член find_first_of() для поиска всех вхождений нежелательных символов:
|
while (( pos = word.find_first_of( filt_elems, pos )) |
!= string::npos )
Найденный символ удаляется с помощью функции-члена erase():
word.erase(pos,1);
Первый аргумент этой функции означает позицию подстроки, а второй – ее длину. Мы удаляем один символ, находящийся в позиции pos. Второй аргумент является необязательным; если его опустить, будут удалены все символы от pos до конца строки.
Вот полный текст функции filter_text(). Она имеет два параметра: указатель на вектор строк, содержащий текст, и строку с символами, которые нужно убрать.
|
void filter_text( vector<string> *words, string filter ) { vector<string>::iterator iter = words->begin(); vector<string>::iterator iter_end = words->end(); // Если filter не задан, зададим его сами if ( ! filter.size() ) filter.insert( 0, "\".," ); while ( iter != iter_end ) { string::size_type pos = 0; // удалим каждый найденный элемент while (( pos = (*iter).find_first_of( filter, pos )) != string::npos ) (*iter).erase(pos,1); iter++; } |
while (( pos = (*iter).find_first_of( filter, pos )) != string::npos ) { (*iter).erase(pos,1); ++ pos; // неправильно... |
string filt_elems( "\",.;:!?)(\\/" ); |
string sentence( "kind of" ); string s1 ( "whistle" ) |
Обработка исключений – это механизм, позволяющий двум независимо разработанным программным компонентам взаимодействовать в аномальной ситуации, называемой исключением. В этой главе мы расскажем, как генерировать, или возбуждать, исключение в том месте программы, где имеет место аномалия. Затем мы покажем, как связать catch-обработчик исключений с множеством инструкций программы, используя try-блок. Потом речь пойдет о спецификации исключений – механизме, с помощью которого можно связать список исключений с объявлением функции, и функция не сможет возбудить никаких других исключений. Закончится эта глава обсуждением решений, принимаемых при проектировании программы, в которой используются исключения.
Если исключения организуются в иерархии, то исключение типа некоторого класса может быть перехвачено обработчиком, соответствующим любому его открытому базовому классу. Например, исключение типа pushOnFull перехватывается обработчиками исключений типа stackExcp или Excp.
|
int main() { try { // ... } catch ( Excp ) { // обрабатывает исключения popOnEmpty и pushOnFull } catch ( pushOnFull ) { // обрабатывает исключение pushOnFull |
}
Здесь порядок catch-обработчиков желательно изменить. Напоминаем, что они просматриваются в порядке появления после try-блока. Как только будет найден обработчик, способный обработать данное исключение, поиск прекращается. В примере выше Excp
может обработать исключения типа pushOnFull, а это значит, что специализированный обработчик таких исключений задействован не будет. Правильная последовательность такова:
|
catch ( pushOnFull ) { // обрабатывает исключение pushOnFull } catch ( Excp ) { // обрабатывает другие исключения |
}
catch-обработчик для производного класса должен идти первым. Тогда catch-обработчик для базового класса получит управление только в том случае, если более специализированного обработчика не нашлось.
Если исключения организованы в иерархии, то пользователи библиотеки классов могут выбрать в своем приложении уровень детализации при работе с исключениями, возбужденными внутри библиотеки. Например, кодируя функцию main(), мы решили, что исключения типа pushOnFull
должны обрабатываться несколько иначе, чем прочие, и потому написали для них специализированный catch-обработчик. Что касается остальных исключений, то они обрабатываются единообразно:
|
catch ( pushOnFull eObj ) { // используется функция-член value() класса pushOnFull // см. раздел 11.3 cerr << "попытка поместить значение " << eObj.value() << " в полный стек\n"; } catch ( Excp ) { // используется функция-член print() базового класса Excp::print( "произошло исключение" ); |
void calculate( int parm ) { try { mathFunc( parm ); // возбуждает исключение divideByZero } catch ( mathExcp mExcp ) { // частично обрабатывает исключение // и генерирует объект-исключение еще раз throw; } |
class pushOnFull { public: pushOnFull( int i ) : _value( i ) { } int value() { return _value; } ~pushOnFull(); // вновь объявленный деструктор private: int _value; |
catch ( pushOnFull eObj ) { cerr << "попытка поместить значение " << eObj.value() << " в полный стек\n"; |
Операции begin() и end()
возвращают соответственно итераторы, указывающие на первый элемент и на элемент, расположенный за последним. Можно также вернуть обратный итератор, обходящий контейнер от последнего элемента к первому. Во всех контейнерах для поддержки такой возможности используются операции rbegin() и rend(). Есть константные и неконстантные версии обратных итераторов:
|
vector< int > vec0; const vector< int > vec1; vector< int >::reverse_iterator r_iter0 = vec0.rbegin(); |
vector< int >::const_reverse_iterator r_iter1 = vec1.rbegin();
Обратный итератор применяется так же, как прямой. Разница состоит в реализации операторов перехода к следующему и предыдущему элементам. Для прямого итератора оператор ++
дает доступ к следующему элементу контейнера, тогда как для обратного – к предыдущему. Например, для обхода вектора в обратном направлении следует написать:
|
// обратный итератор обходит вектор от конца к началу vector< type >::reverse_iterator r_iter; for ( r_iter = vec0.rbegin(); // r_iter указывает на последний элемент r_iter != vec0.rend(); // пока не достигли элемента перед первым r_iter++ ) // переходим к предыдущему элементу |
{ /* ... */ }
Инвертирование семантики операторов инкремента и декремента может внести путаницу, но зато позволяет программисту передавать алгоритму пару обратных итераторов вместо прямых. Так, для сортировки вектора в порядке убывания мы передаем алгоритму sort()
пару обратных итераторов:
|
// сортирует вектор в порядке возрастания sort( vec0.begin(), vec0.end() ); // сортирует вектор в порядке убывания |
sort( vec0.rbegin(), vec0.rend() );
Абстракция очереди реализует метод доступа FIFO (first in, first out – “первым вошел, первым вышел”): объекты добавляются в конец очереди, а извлекаются из начала. Стандартная библиотека предоставляет две разновидности этого метода: очередь FIFO, или простая очередь, и очередь с приоритетами, которая позволяет сопоставлять элементы с их приоритетами.
Текущий элемент помещается не в конец такой очереди, а перед элементами с более низким приоритетом. Программист, определяющий такую структуру, задает способ вычисления приоритетов. В реальной жизни подобное можно увидеть, скажем, при регистрации багажа в аэропорту. Как правило, пассажиры, чей рейс через 15 минут, передвигаются в начало очереди, чтобы не опоздать на самолет. Примером из практики программирования служит планировщик операционной системы, определяющий последовательность выполнения процессов.
Для использования queue и priority_queue
необходимо включить заголовочный файл:
#include <queue>
Полный набор операций с контейнерами queue и priority_queue приведен в таблице 6.6.
Таблица 6.6. Операции с queue и priority_queue
| Операция | Действие | ||
| empty() | Возвращает true, если очередь пуста, и false в противном случае | ||
| size() | Возвращает количество элементов в очереди | ||
| pop() | Удаляет первый элемент очереди, но не возвращает его значения. Для очереди с приоритетом первым является элемент с наивысшим приоритетом | ||
| front() | Возвращает значение первого элемента очереди, но не удаляет его. Применимо только к простой очереди | ||
| back() | Возвращает значение последнего элемента очереди, но не удаляет его. Применимо только к простой очереди | ||
| top() | Возвращает значение элемента с наивысшим приоритетом, но не удаляет его. Применимо только к очереди с приоритетом | ||
| push(item) | Помещает новый элемент в конец очереди. Для очереди с приоритетом позиция элемента определяется его приоритетом. |
Элементы priority_queue
отсортированы в порядке убывания приоритетов. По умолчанию упорядочение основывается на операции “меньше”, определенной над парами элементов. Конечно, можно явно задать указатель на функцию или объект-функцию, которая будет использоваться для сортировки. (В разделе 12.3 можно найти более подробное объяснение и иллюстрации использования такой очереди.)
Доступность конструктора определяется тем, в какой секции класса он объявлен. Мы можем ограничить или явно запретить некоторые формы создания объектов, если поместим соответствующий конструктор в неоткрытую секцию. В примере ниже конструктор по умолчанию класса Account
объявлен закрытым, а с двумя параметрами– открытым:
|
class Account { friend class vector< Account >; public: explicit Account( const char*, double = 0.0 ); // ... private: Account(); // ... |
};
Обычная программа сможет теперь определять объекты класса Account, лишь указав как имя владельца счета, так и начальный баланс. Однако функции-члены Account и дружественный ему класс vector могут создавать объекты, пользуясь любым конструктором.
Конструкторы, не являющиеся открытыми, в реальных программах C++ чаще всего используются для:
· предотвращения копирования одного объекта в другой объект того же класса (эта проблема рассматривается в следующем подразделе);
· указания на то, что конструктор должен вызываться только в случае, когда данный класс выступает в роли базового в иерархии наследования, а не для создания объектов, которыми программа может манипулировать напрямую (см. обсуждение наследования и объектно-ориентированного программирования в главе 17).
Ниже представлен полный текст программы, разработанной в этой главе, с двумя модификациями: мы инкапсулировали все структуры данных и функции в класс TextQuery (в последующих главах мы обсудим подобное использование классов), кроме того, текст был изменен, так как наш компилятор поддерживал стандарт С++ не полностью.
Например, библиотека iostream не соответствовала текущему стандарту. Шаблоны не поддерживали значения аргументов по умолчанию. Возможно, вам придется изменить кое-что в этой программе, чтобы она компилировалась в вашей системе.
|
// стандартные заголовочные файлы С++ #include <algorithm> #include <string> #include <vector> #include <utility> #include <map> #include <set> // заголовочный файл iostream, не отвечающий стандарту #include <fstream.h> // заголовочные файлы С #include <stddef.h> #include <ctype.h> // typedef для удобства чтения typedef pair<short,short> location; typedef vector<location,allocator> loc; typedef vector<string,allocator> text; typedef pair<text*,loc*> text_loc; class TextQuery { public: TextQuery() { memset( this, 0, sizeof( TextQuery )); } static void filter_elements( string felems ) { filt_elems = felems; } void query_text(); void display_map_text(); void display_text_locations(); void doit() { retrieve_text(); separate_words(); filter_text(); suffix_text(); strip_caps(); build_word_map(); } private: void retrieve_text(); void separate_words(): void filter_text(); void strip_caps(); void suffix_textQ; void suffix_s( string& ); void build_word_map(); private: vector<string,allocator> *lines_of_text; text_loc *text_locations; map< string,loc*, less<string>,allocator> *word_map; static string filt_elems; }; string TextQuery::filt_elems( "\", •;: !?)(\V" ); int main() { TextQuery tq; tq.doit(); tq.query_text(); tq.display_map_text(); } void TextQuery:: retrieve_text() { string file_name; cout << "please enter file name: "; cin >> file_name; ifstream infile( file_name.c_str(), ios::in ); if ( !infile ) { cerr << "oops' unable to open file " << file_name << " -- bailing out!\n"; exit( -1 ); } else cout << "\n"; lines_of_text = new vector<string,allocator>; string textline; while ( getline( infile, textline, '\n' )) lines_of_text->push_back( textline ); } void TextQuery:: separate_words() { vector<string,allocator> *words = new vector<string,allocator>; vector<location,allocator> *locations = new vector<location,allocator>; for ( short line_pos = 0; line_pos < lines_of_text->size(); line_pos++ ) { short word_pos = 0; string textline = (*lines_of_text)[ line_pos ]; string::size_type eol = textline.1ength(); 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++; pos++; prev_pos = pos; } words->push_back( textline.substr( prev_pos, pos - prev_pos )); locations->push_back(make_pair(line_pos,word_pos)); } text_locations = new text_loc( words, locations ); } void TextQuery:: filter_text() { if ( filt_elems.empty() ) return; vector<string,allocator> *words = text_locations->first; vector<string,allocator>::iterator iter = words->begin(); vector<string,allocator>::iterator iter_end = words->end(); while ( iter != iter_end ) { string::size_type pos = 0; while ((pos = (*iter).find_first_of(filt_elems, pos)) != string::npos ) (*iter).erase(pos,l); ++iter; } } void TextQuery:: suffix_text() { vector<string,allocator> *words = text_locations->first; vector<string,allocator>::iterator iter = words->begin(); vector<string,allocator>::iterator iter_end = words->end() ; while ( iter != iter_end ) { if ( (*iter).size() <= 3 ) { iter++; continue; } if ( (*iter)[ (*iter).size()-l ] == 's' ) suffix_s( *iter ); // дополнительная обработка суффиксов... iter++; } } void TextQuery:: suffix_s( string &word ) { string::size_type spos = 0; string::size_type pos3 = word.size()-3; // "ous", "ss", "is", "ius" string suffixes( "oussisius" ); if ( ! word.compare( pos3, 3, suffixes, spos, 3 ) || ! word.compare( pos3, 3, suffixes, spos+6, 3) || ! word.compare( pos3+l, 2, suffixes, spos+2, 2 ) || ! word.compare( pos3+l, 2, suffixes, spos+4, 2 )) return; string ies( "ies" ); if ( ! word.compare( pos3, 3, ies )) { word.replace( pos3, 3, 1, 'у' ); return; } string ses( "ses" ); if ( ! word.compare( pos3, 3, ses )) { word.erase( pos3+l, 2 ); return; } // удалим 's' в конце word.erase( pos3+2 ); // удалим "'s" if ( word[ pos3+l ] == '\'' ) word.erase( pos3+l ); } void TextQuery:: strip_caps() { vector<string,allocator> *words = text_locations->first; vector<string,allocator>::iterator iter = words->begin(); vector<string,allocator>::iterator iter_end = words->end(); string caps( "ABCDEFGHI3KLMNOPQRSTUVWXYZ" ); while ( iter != iter_end ) { string::size_type pos = 0; while (( pos = (*iter).find_first_of( caps, pos )) != string::npos ) (*iter)[ pos ] = to1ower( (*iter)[pos] ); ++iter; } } void TextQuery:: build_word_map() { word_map = new map<string,loc*,less<string>,allocator>; typedef map<string,loc*,less<string>,allocator>::value_type value_type; typedef set<string,less<string>,allocator>::difference_type diff_type; set<string,less<string>,allocator> exclusion_set; ifstream infile( "exclusion_set" ); if ( !infile ) { static string default_excluded_words[25] = { "the","and","but","that","then","are","been", "can","can't","cannot","could","did","for", "had","have","him","his","her","its"."into", "were","which","when","with","would" }; cerr << "warning! unable to open word exclusion file! -- " << "using default set\n"; copy( default_excluded_words, default_excluded_words+25, inserter(exclusion_set, exclusion_set.begin())); } else { istream_iterator< string, diff_type > input_set( infile ), eos; copy( input_set, eos, inserter( exclusion_set, exclusion_set.begin() )); } // пробежимся по всем словам, вставляя пары vector<string,allocator> *text_words = text_locations->first; vector<location,allocator> *text.locs = text_locations->second; register int elem_cnt = text_words->size(); for ( int ix = 0; ix < elem_cnt; ++-ix ) { string textword = ( *text_words )[ ix ]; if ( textword.size() < 3 || exclusion_set.count( textword )) continue; if ( ! word_map->count((*text_words)[ix] )) { // слово отсутствует, добавим: loc *ploc = new vector<location,allocator>; ploc->push_back( (*text_locs)[ix] ); word_map-> insert( value_type( (*text_words)[ix],ploc )); } else (*word_map) [(*text_words) [ix]]-> push_back( (*text_locs) [ix] ); } } void TextQuery:: query_text() { string query_text; do { cout << "enter a word against which to search the text.\n" << "to quit, enter a single character ==> "; cin >> query_text; if ( query_text.size() < 2 ) break; string caps( "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ); string::size_type pos = 0; while (( pos = query_text.find_first_of( caps, pos )) != string::npos ) query_text[ pos ] = to1ower( query_text[pos] ); // query_text должно быть введено if ( !word_map->count( query_text )) { cout << "\nSorry. There are no entries for " << query_text << ".\n\n"; continue; } loc *ploc = (*word_map) [ query_text ]; set<short,less<short>,allocator> occurrence_1i nes; loc::iterator liter = ploc->begin(), liter_end = ploc->end(); while ( liter != liter_end ) { occurrence_lines.1nsert( occurrence_lines.end(), (*liter).first); ++liter; } register int size = occurrence_lines.size(); cout << "\n" << query_text << " occurs " << size << (size == 1 ? " time:" : " times:") << "\n\n"; set<short,less<short>,allocator>::iterator it=occurrence_lines.begin(); for ( ; it != occurrence_"lines.end(); ++it ) { int line = *it; cout << "\t( line " // будем нумеровать строки с 1, // как это принято везде << line + 1 << " ) " << (*lines_of_text)[line] << endl; } cout << endl; } while ( ! query_text.empty() ); cout << "Ok, bye!\n"; } void TextQuery:: display_map_text() { typedef map<string,loc*, less<string>, allocator> map_text; map_text::iterator iter = word_map->begin(), iter_end = word_map->end(); while ( iter != iter_end ) { cout << "word: " << (*iter).first << " ("; int loc_cnt = 0; loc *text_locs = (*iter).second; loc::iterator liter = text_locs->begin(), liter_end = text_locs->end(); while ( liter != liter_end ) { if ( loc_cnt ) cout << ","; else ++loc_cnt; cout << "(" << (*liter).first << "," << (*liter).second << ")"; ++"liter; } cout << ")\n"; ++iter; } cout << endl; } void TextQuery:: disp1ay_text_locations() { vector<string,allocator> *text_words = text_locations->first; vector<location,allocator> *text_locs = text_locations->second; register int elem_cnt = text_words->size(); if ( elem_cnt != text_locs->size() ) { cerr << "oops! internal error: word and position vectors " << "are of unequal size\n" << "words: " << elem_cnt << " " << "locs: " << text_locs->size() << " -- bailing out!\n"; exit( -2 ); } for ( int ix=0; ix < elem_cnt; ix++ ) { cout << "word: " << (*text_words)[ ix ] << "\t" << "location: (" << (*text_locs)[ix].first << "," << (*text.locs)[ix].second << ")" << "\n"; } cout << endl; |
set<string> exclusion_set; ifstream infile( "exclusion_set" ); copy( default_excluded_words, default_excluded_words+25, |
Встроенный деструктор может стать причиной непредвиденного увеличения размера программы, поскольку он вставляется в каждой точке выхода внутри функции для каждого активного локального объекта. Например, в следующем фрагменте
|
Account acct( "Tina Lee" ); int swt; // ... switch( swt ) { case 0: return; case 1: // что-то сделать return; case 2: // сделать что-то другое return; // и так далее |
}
компилятор подставит деструктор перед каждой инструкцией return. Деструктор класса Account
невелик, и затраты времени и памяти на его подстановку тоже малы. В противном случае придется либо объявить деструктор невстроенным, либо реорганизовать программу. В примере выше инструкцию return в каждой метке case
можно заменить инструкцией break с тем, чтобы у функции была единственная точка выхода:
|
// переписано для обеспечения единственной точки выхода switch( swt ) { case 0: break; case 1: // что-то сделать break; case 2: // сделать что-то другое break; // и так далее } // единственная точка выхода |
return;
Упражнение 14.6
Напишите подходящий деструктор для приведенного набора членов класса, среди которых pstring
адресует динамически выделенный массив символов:
|
class NoName { public: ~NoName(); // ... private: char *pstring; int ival; double dval; |
};
Упражнение 14.7
Необходим ли деструктор для класса, который вы выбрали в упражнении 14.3? Если нет, объясните почему. В противном случае предложите реализацию.
Упражнение 14.8
Сколько раз вызываются деструкторы в следующем фрагменте:
|
void mumble( const char *name, fouble balance, char acct_type ) { Account acct; if ( ! name ) return; if ( balance <= 99 ) return; switch( acct_type ) { case 'z': return; case 'a': case 'b': return; } // ... |
}
Операции инкремента (++) и декремента (--) дают возможность компактной и удобной записи для изменения значения переменной на единицу. Чаще всего они используются при работе с массивами и коллекциями – для изменения величины индекса, указателя или итератора:
|
#include <vector> #include <cassert> int main() { int ia[10] = {0,1,2,3,4,5,6,7,8,9}; vector<int> ivec( 10 ); int ix_vec = 0, ix_ia = 9; while ( ix_vec < 10 ) ivec[ ix_vec++ ] = ia[ ix_ia-- ]; int *pia = &ia[9]; vector<int>::iterator iter = ivec.begin(); while ( iter != ivec.end() ) assert( *iter++ == *pia-- ); |
}
Выражение
ix_vec++
является постфиксной формой оператора инкремента. Значение переменной ix_vec
увеличивается после того, как ее текущее значение употреблено в качестве индекса. Например, на первой итерации цикла значение ix_vec
равно 0. Именно это значение применяется как индекс массива ivec, после чего ix_vec
увеличивается и становится равным 1, однако новое значение используется только на следующей итерации. Постфиксная форма операции декремента работает точно так же: текущее значение ix_ia берется в качестве индекса для ia, затем ix_ia
уменьшается на 1.
Существует и префиксная форма этих операторов. При использовании такой формы текущее значение сначала уменьшается или увеличивается, а затем используется новое значение. Если мы пишем:
|
// неверно: ошибки с границами индексов в // обоих случаях int ix_vec = 0, ix_ia = 9; while ( ix_vec < 10 ) |
ivec[ ++ix_vec ] = ia[ --ix_ia ];
значение ix_vec
увеличивается на единицу и становится равным 1 до первого использования в качестве индекса. Аналогично ix_ia
получает значение 8 при первом использовании. Для того чтобы наша программа работала правильно, мы должны скорректировать начальные значения переменных ix_ivec и ix_ia:
|
// правильно int ix_vec = -1, ix_ia = 8; while ( ix_vec < 10 ) |
ivec[ ++ix_vec ] = ia[ --ix_ia ];
Инициализация задает начальное значение переменной. Например:
|
int ival = 1024; |
int *pi = 0;
В результате операции присваивания объект получает новое значение, при этом старое пропадает:
|
ival = 2048; |
pi = &iva1;
Иногда путают инициализацию и присваивание, так как они обозначаются одним и тем же знаком =. Объект инициализируется только один раз– при его определении. В то же время операция может быть применена к нему многократно.
Что происходит, если тип объекта не совпадает с типом значения, которое ему хотят присвоить? Допустим,
ival = 3.14159; // правильно?
В таком случае компилятор пытается трансформировать тип объекта, стоящего справа, в тип объекта, стоящего слева. Если такое преобразование возможно, компилятор неявно изменяет тип, причем при потере точности обычно выдается предупреждение. В нашем случае вещественное значение 3.14159 преобразуется в целое значение 3, и это значение присваивается переменной ival.
Если неявное приведение типов невозможно, компилятор сигнализирует об ошибке:
pi = ival; // ошибка
Неявная трансформация типа int в тип указатель на int
невозможна. (Набор допустимых неявных преобразований типов мы обсудим в разделе 4.14.)
Левый операнд операции присваивания должен быть l-значением. Очевидный пример неправильного присваивания:
1024 = ival; // ошибка
Возможно, имелось в виду следующее:
|
int value = 1024; |
value = ival; // правильно
Однако недостаточно потребовать, чтобы операнд слева от знака присваивания был l-значением.
Так, после определений
|
const int array_size = 8; int ia[ array_size ] = { 0, 1, 2, 2, 3, 5, 8, 13 }; |
int *pia = ia;
выражение
array_size = 512; // ошибка
ошибочно, хотя array_size и является l-значением: объявление array_size константой не дает возможности изменить его значение. Аналогично
ia = pia; // ошибка
ia – тоже l-значение, но оно не может быть значением массива.
Неверна и инструкция
pia + 2=1; // ошибка
Хотя pia+2
дает адрес ia[2], присвоить ему значение нельзя. Если мы хотим изменить элемент ia[2], то нужно воспользоваться операцией разыменования. Корректной будет следующая запись:
extern char next_char(); int main() { char ch = next_char(); while ( ch != '\n' ) { // сделать что-то ... ch = next_char(); } // ... |
extern char next_char(); int main() { char ch; while (( ch = next_char() ) != '\n' ) { // сделать что-то ... } // ... |
int main () { int ival, jval; ival = jval = 0; // правильно: присваивание 0 обеим переменным |
int main () { int ival; int *pval; ival = pval = 0; // ошибка: разные типы |
int main() { // ... int ival = jval = 0; // верно или нет? // ... |
int main() { // правильно: определение и инициализация int ival = 0, jval = 0; // ... |
int arraySum( int ia[], int sz ) { int sum = 0; for ( int i = 0; i < sz; ++i ) sum = sum + ia[ i ]; return sum; |
int arraySum( int ia[], int sz ) { int sum = 0; for ( int i =0; i < sz; ++i ) // эквивалентно: sum = sum + ia[ i ]; sum += ia[ i ]; return sum; |
int main() { float fval; int ival; int *pi; fval = ival = pi = 0; |
(a) if ( ptr = retrieve_pointer() != 0 ) (b) if ( ival = 1024 ) |
Класс комплексных чисел стандартной библиотеки С++ представляет собой хороший пример использования объектной модели. Благодаря перегруженным арифметическим операциям объекты этого класса используются так, как будто они принадлежат одному из встроенных типов данных. Более того, в подобных операциях могут одновременно принимать участие и переменные встроенного арифметического типа, и комплексные числа. (Отметим, что здесь мы не рассматриваем общие вопросы математики комплексных чисел. См. [PERSON68] или любую книгу по математике.) Например, можно написать:
|
#inc1ude <complex> comp1ex< double > a; comp1ex< double > b; // ... |
complex< double > с = a * b + a / b;
Комплексные и арифметические типы разрешается смешивать в одном выражении:
complex< double > complex_obj = a + 3.14159;
Аналогично комплексные числа инициализируются арифметическим типом, и им может быть присвоено такое значение:
|
double dval = 3.14159; |
complex_obj = dval;
Или
|
int ival = 3; |
complex_obj = ival;
Однако обратное неверно. Например, следующее выражение вызовет ошибку компиляции:
|
// ошибка: нет неявного преобразования // в арифметический тип |
double dval = complex_obj;
Нужно явно указать, какую часть комплексного числа – вещественную или мнимую – мы хотим присвоить обычному числу. Класс комплексных чисел имеет две функции, возвращающих соответственно вещественную и мнимую части. Мы можем обращаться к ним, используя синтаксис доступа к членам класса:
|
double re = complex_obj.real(); |
double im = complex_obj.imag();
или эквивалентный синтаксис вызова функции:
|
double re = real(complex_obj); |
double im = imag(complex_obj);
Класс комплексных чисел поддерживает четыре составных оператора присваивания: +=, -=, *= и /=. Таким образом,
complex_obj += second_complex_obj;
Поддерживается и ввод/вывод комплексных чисел. Оператор вывода печатает вещественную и мнимую части через запятую, в круглых скобках. Например, результат выполнения операторов вывода
complex< double > complex0( 3.14159, -2.171 ); comp1ex< double > complex1( complexO.real() ); |
// допустимые форматы для ввода комплексного числа // 3.14159 ==> comp1ex( 3.14159 ); // ( 3.14159 ) ==> comp1ex( 3.14159 ); // ( 3.14, -1.0 ) ==> comp1ex( 3.14, -1.0 ); // может быть считано как // cin >> a >> b >> с // где a, b, с - комплексные числа |
#include <complex> inline complex<double>& operator+=( complex<double> &cval, double dval ) { return cval += complex<double>( dval ); |
#include <iostream> #include <complex> // определения операций... int main() { complex< double > cval ( 4.0, 1.0 ); cout << cval << endl; cval += 1; cout << cval << endl; cval -= 1; cout << cval << endl; cval *= 2; cout << cval << endl; cout /= 2; cout << cval << endl; |
#include <iostream> #include <complex> // определения операций... int main() { complex< double > cval( 4.0, 1.0 ); cout << cval << endl; ++cva1; cout << cval << endl; |
Функция-член push_back()
позволяет добавить единственный элемент в конец контейнера. Но как вставить элемент в произвольную позицию? А целую последовательность элементов? Для этих случаев существуют более общие операции.
Например, для вставки элемента в начало контейнера можно использовать:
|
vector< string > svec; list< string > slist; string spouse( "Beth" ); slist.insert( slist.begin(), spouse ); |
svec.insert( svec.begin(), spouse );
Первый параметр функции-члена insert() (итератор, адресующий некоторый элемент контейнера) задает позицию, а второй – вставляемое перед этой позицией значение. В примере выше элемент добавляется в начало контейнера. А так можно реализовать вставку в произвольную позицию:
|
string son( "Danny" ); list<string>::iterator iter; iter = find( slist.begin(), slist.end(), son ); |
slist.insert( iter, spouse );
Здесь find()
возвращает позицию элемента в контейнере, если элемент найден, либо итератор end(), если ничего не найдено. (Мы вернемся к функции find() в конце следующего раздела.) Как можно догадаться, push_back()
эквивалентен следующей записи:
|
// эквивалентный вызов: slist.push_back( value ); |
slist.insert( slist.end(), value );
Вторая форма функции-члена insert()
позволяет вставить указанное количество одинаковых элементов, начиная с определенной позиции. Например, если мы хотим добавить десять элементов Anna в начало вектора, то должны написать:
|
vector<string> svec; string anna( "Anna" ); |
svec.insert( svec.begin(), 10, anna );
insert()
имеет и третью форму, помогающую вставить в контейнер несколько элементов. Допустим, имеется следующий массив:
string sarray[4] = { "quasi", "simba", "frollo", "scar" };
Мы можем добавить все его элементы или только некоторый диапазон в наш вектор строк:
|
svec.insert( svec.begin(), sarray, sarray+4 ); svec.insert( svec.begin() + svec.size()/2, |
// вставляем элементы svec // в середину svec_two svec_two.insert( svec_two.begin() + svec_two.size()/2, |
list< string > slist; // ... // вставляем элементы svec // перед элементом, содержащим stringVal list< string >::iterator iter = find( slist.begin(), slist.end(), stringVal ); |
Таблица 4.2. Операции сравнения и логические операции
| Символ операции | Значение | Использование | |||
| ! | Логическое НЕ | !expr | |||
| < | Меньше | expr1 < expr2 | |||
| <= | Меньше или равно | expr1 <= expr2 | |||
| > | Больше | expr1 > expr2 | |||
| >= | Больше или равно | expr1 >= expr2 | |||
| == | Равно | expr1 == expr2 | |||
| != | Не равно | expr1 != expr2 | |||
| && | Логическое И | expr1 && expr2 | |||
| || | Логическое ИЛИ | expr1 || expr2 | |||
| Примечание. Все операции в результате дают значение типа bool |
Операции сравнения и логические операции в результате дают значение типа bool, то есть true или false. Если же такое выражение встречается в контексте, требующем целого значения, true
преобразуется в 1, а false – в 0. Вот фрагмент кода, подсчитывающего количество элементов вектора, меньших некоторого заданного значения:
| vector<int>::iterator iter = ivec.beg-in() ;
while ( iter != ivec.end() ) { // эквивалентно: e1em_cnt = e1em_cnt + (*iter < some_va1ue) // значение true/false выражения *iter < some_va1ue // превращается в 1 или 0 e1em_cnt += *iter < some_va1ue; ++iter; |
}
Мы просто прибавляем результат операции “меньше” к счетчику. (Пара +=
обозначает составной оператор присваивания, который складывает операнд, стоящий слева, и операнд, стоящий справа. То же самое можно записать более компактно: elem_count = elem_count + n. Мы рассмотрим такие операторы в разделе 4.4.)
Логическое И (&&) возвращает истину только тогда, когда истинны оба операнда. Логическое ИЛИ (||) дает истину, если истинен хотя бы один из операндов. Гарантируется, что операнды вычисляются слева направо и вычисление заканчивается, как только результирующее значение становится известно. Что это значит? Пусть даны два выражения:
| expr1 && expr2 |
expr1 || expr2
Если в первом из них expr1
равно false, значение всего выражения тоже будет равным false вне зависимости от значения expr2, которое даже не будет вычисляться. Во втором выражении expr2 не оценивается, если expr1
while ( ptr != О && ptr->va1ue < upperBound && ptr->va1ue >= 0 && notFound( ia[ ptr->va1ue ] )) |
bool found = false; // пока элемент не найден // и ptr указывает на объект (не 0) while ( ! found && ptr ) { found = 1ookup( *ptr ); ++ptr; |
// Внимание! Порядок вычислений не определен! if ( ia[ index++ ] < ia[ index ] ) |
if ( ia[ index ] < ia[ index+1 ] ) // поменять местами элементы |
// Внимание! это не сравнение 3 переменных друг с другом if ( ival != jva1 != kva1 ) |
if ( ival != jva1 && ival != kva1 && jva1 != kva1 ) |
(a) ptr->iva1 != 0 (с) ptr != 0 && *ptr++ (e) vec[ iva1++ ] <= vec[ ival ]; |
|
void list::merge( list rhs ); template <class Compare> |
void list::merge( list rhs, Compare comp );
Элементы двух упорядоченных списков объединяются либо на основе оператора “меньше”, определенного для типа элементов в контейнере, либо на основе указанной пользователем операции сравнения. (Заметьте, что элементы списка rhs перемещаются в список, для которого вызвана функция-член merge(); по завершении операции список rhs
будет пуст.) Например:
|
int array1[ 10 ] = { 34, 0, 8, 3, 1, 13, 2, 5, 21, 1 }; int array2[ 5 ] = { 377, 89, 233, 55, 144 }; list< int > ilist1( array1, array1 + 10 ); list< int > ilist2( array2, array2 + 5 ); // для объединения требуется, чтобы оба списка были упорядочены ilist1.sort(); ilist2.sort(); |
ilist1.merge( ilist2 );
После выполнения операции merge() список ilist2
пуст, а ilist1
содержит первые 15 чисел Фибоначчи в порядке возрастания.
void list::remove( const elemType &value );
Операция remove()
удаляет все элементы с заданным значением:
ilist1.remove( 1 );
|
template < class Predicate > |
void list::remove_if( Predicate pred );
Операция remove_if()
удаляет все элементы, для которых выполняется указанное условие, т.е. предикат pred
возвращает true. Например:
|
class Even { public: bool operator()( int elem ) { return ! (elem % 2 ); } }; |
ilist1.remove_if( Even() );
удаляет все четные числа из списка, определенного при рассмотрении merge().
void list::reverse();
Операция reverse()
изменяет порядок следования элементов списка на противоположный:
ilist1.reverse();
|
void list::sort(); template <class Compare> |
void list::sort( Compare comp );
По умолчанию sort()
упорядочивает элементы списка по возрастанию с помощью оператора “меньше”, определенного в классе элементов контейнера. Вместо этого можно явно передать в качестве аргумента оператор сравнения. Так,
list1.sort();
упорядочивает list1 по возрастанию, а
list1.sort( greater<int>() );
упорядочивает list1 по убыванию, используя оператор “больше”.
|
void list::splice( iterator pos, list rhs ); void list::splice( iterator pos, list rhs, iterator ix ); void list::splice( iterator pos, list rhs, |
iterator first, iterator last );
Операция splice()
имеет три формы: перемещение одного элемента, всех элементов или диапазона из одного списка в другой. В каждом случае передается итератор, указывающий на позицию вставки, а перемещаемые элементы располагаются непосредственно перед ней. Если даны два списка:
|
int array[ 10 ] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 }; list< int > ilist1( array, array + 10 ); |
list< int > ilist2( array, array + 2 ); // содержит 0, 1
то следующее обращение к splice()
перемещает первый элемент ilist1 в ilist2. Теперь ilist2
содержит элементы 0, 1 и 0, тогда как в ilist1
элемента 0
больше нет.
|
// ilist2.end() указывает на позицию, куда нужно переместить элемент // элементы вставляются перед этой позицией // ilist1 указывает на список, из которого перемещается элемент // ilist1.begin() указывает на сам перемещаемый элемент |
ilis2.splice( ilist2.end(), ilist1, ilist1.begin() );
В следующем примере применения splice() передаются два итератора, ограничивающие диапазон перемещаемых элементов:
|
list< int >::iterator first, last; first = ilist1.find( 2 ); last = ilist1.find( 13 ); |
ilist2.splice( ilist2.begin(), ilist1, first, last );
В данном случае элементы 2, 3, 5 и 8
удаляются из ilist1 и вставляются в начало ilist2. Теперь ilist1
содержит пять элементов 1, 1, 13, 21 и 34. Для их перемещения в ilist2
можно воспользоваться третьей вариацией операции splice():
|
list< int >::iterator pos = ilist2.find( 5 ); |
ilist2.splice( pos, ilist1 );
Итак, список ilist1
пуст. Последние пять элементов перемещены в позицию списка ilist2, предшествующую той, которую занимает элемент 5.