Если программист хочет использовать функцию, написанную на другом языке, в частности на С, то компилятору нужно указать, что при вызове требуются несколько иные условия. Скажем, имя функции или порядок передачи аргументов различаются в зависимости от языка программирования.
Показать, что функция написана на другом языке, можно с помощью директивы связывания в форме простой либо составной инструкции:
|
// директива связывания в форме простой инструкции extern "C" void exit(int); // директива связывания в форме составной инструкции extern "C" { int printf( const char* ... ); int scanf( const char* ... ); } // директива связывания в форме составной инструкции extern "C" { #include <cmath> |
}
Первая форма такой директивы состоит из ключевого слова extern, за которым следует строковый литерал, а за ним – “обычное” объявление функции. Хотя функция написана на другом языке, проверка типов вызова выполняется полностью. Несколько объявлений функций могут быть помещены в фигурные скобки составной инструкции директивы связывания – второй формы этой директивы. Скобки отмечают те объявления, к которым она относится, не ограничивая их видимости, как в случае обычной составной инструкции. Составная инструкция extern "C" в предыдущем примере говорит только о том, что функции printf() и scanf()
написаны на языке С. Во всех остальных отношениях эти объявления работают точно так же, как если бы они были расположены вне инструкции.
Если в фигурные скобки составной директивы связывания помещается директива препроцессора #include, все объявленные во включаемом заголовочном файле функции рассматриваются как написанные на языке, указанном в этой директиве. В предыдущем примере все функции из заголовочного файла cmath написаны на языке С.
Директива связывания не может появиться внутри тела функции. Следующий фрагмент кода вызывает ошибку компиляции:
|
int main() { // ошибка: директива связывания не может появиться // внутри тела функции extern "C" double sqrt( double ); double getValue(); //правильно double result = sqrt ( getValue() ); //... return 0; |
extern "C" double sqrt( double ); int main() { double getValue(); //правильно double result = sqrt ( getValue() ); //... return 0; |
// функция calc() может быть вызвана из программы на C |
// ---- myMath.h ---- extern "C" double calc( double ); // ---- myMath.C ---- // объявление calc() в myMath.h #include "myMath.h" // определение функции extern "C" calc() // функция calc() может быть вызвана из программы на C |
const char *str = "hello"; void *malloc( int ); char *strcpy( char *, const char * ); int printf( const char *, ... ); int exit( int ); int strlen( const char * ); int main() { /* программа на языке С */ char* s = malloc( strlen(str)+l ); strcpy( s, str ); printf( "%s, world\n", s ); exit( 0 ); |
Директива typedef
позволяет задать синоним для встроенного либо пользовательского типа данных. Например:
|
typedef double wages; typedef vector<int> vec_int; typedef vec_int test_scores; typedef bool in_attendance; |
typedef int *Pint;
Имена, определенные с помощью директивы typedef, можно использовать точно так же, как спецификаторы типов:
|
// double hourly, weekly; wages hourly, weekly; // vector<int> vecl( 10 ); vec_int vecl( 10 ); // vector<int> test0( c1ass_size ); const int c1ass_size = 34; test_scores test0( c1ass_size ); // vector< bool > attendance; vector< in_attendance > attendance( c1ass_size ); // int *table[ 10 ]; |
Pint table [ 10 ];
Эта директива начинается с ключевого слова typedef, за которым идет спецификатор типа, и заканчивается идентификатором, который становится синонимом для указанного типа.
Для чего используются имена, определенные с помощью директивы typedef? Применяя мнемонические имена для типов данных, можно сделать программу более легкой для восприятия. Кроме того, принято употреблять такие имена для сложных составных типов, в противном случае воспринимаемых с трудом (см. пример в разделе 3.14), для объявления указателей на функции и функции-члены класса (см. раздел 13.6).
Ниже приводится пример вопроса, на который почти все дают неверный ответ. Ошибка вызвана непониманием директивы typedef как простой текстовой макроподстановки. Дано определение:
typedef char *cstring;
Каков тип переменной cstr в следующем объявлении:
extern const cstring cstr;
Ответ, который кажется очевидным:
const char *cstr
Однако это неверно. Спецификатор const
относится к cstr, поэтому правильный ответ – константный указатель на char:
char *const
cstr;
Заголовочные файлы включаются в текст программы с помощью директивы препроцессора #include. Директивы препроцессора начинаются со знака “диез” (#), который должен быть самым первым символом строки. Программа, которая обрабатывает эти директивы, называется препроцессором
(в современных компиляторах препроцессор обычно является частью самого компилятора).
Директива #include
включает в программу содержимое указанного файла. Имя файла может быть указано двумя способами:
|
#include <some_file.h> |
#include "my_file.h"
Если имя файла заключено в угловые скобки (<>), считается, что нам нужен некий стандартный заголовочный файл, и компилятор ищет этот файл в предопределенных местах. (Способ определения этих мест сильно различается для разных платформ и реализаций.) Двойные кавычки означают, что заголовочный файл – пользовательский, и его поиск начинается с того каталога, где находится исходный текст программы.
Заголовочный файл также может содержать директивы #include. Поэтому иногда трудно понять, какие же конкретно заголовочные файлы включены в данный исходный текст, и некоторые заголовочные файлы могут оказаться включенными несколько раз. Избежать этого позволяют условные директивы препроцессора. Рассмотрим пример:
#ifndef BOOKSTORE_H
|
#define BOOKSTORE_H /* содержимое файла bookstore.h */ |
#endif
Условная директива
#ifndef проверяет, не было ли значение BOOKSTORE_H
определено ранее. (BOOKSTORE_H – это константа препроцессора; такие константы принято писать заглавными буквами.) Препроцессор обрабатывает следующие строки вплоть до директивы #endif. В противном случае он пропускает строки от #ifndef до # endif.
Директива
#define BOOKSTORE_H
определяет константу препроцессора BOOKSTORE_H. Поместив эту директиву непосредственно после директивы #ifndef, мы можем гарантировать, что содержательная часть заголовочного файла bookstore.h
будет включена в исходный текст только один раз, сколько бы раз ни включался в текст сам этот файл.
int main() { #ifdef DEBUG cout << "Начало выполнения main()\n"; #endif string word; vector<string> text; while ( cin >> word ) { #ifdef DEBUG cout << "Прочитано слово: " << word << "\n"; #endif text.push_back(word); } // ... |
int main() |
int main() |
#idfef __cplusplus // компиляция С++ программы extern "C"; // extern "C" объясняется в главе 7 #endif |
if ( element_count == 0 ) |
Вторая форма функции-члена erase()
принимает в качестве параметров два итератора, ограничивающих удаляемую подстроку. Например, превратим
string name( "AnnaLiviaPlurabelle" );
в строку "Annabelle":
|
typedef string::size_type size_type; size_type startPos = name.find( 'L' ) size_type endPos = name.find_1ast_of( 'b' ); name.erase( name.begin()+startPos, |
name.begin()+endPos );
Символ, на который указывает второй итератор, не входит в удаляемую подстроку.
Для третьей формы параметром является только один итератор; эта форма удаляет все символы, начиная с указанной позиции до конца строки. Например:
name.erase( name. begin()+4 );
оставляет строку "Anna".
Функция-член insert()
позволяет вставить в заданную позицию строки другую строку или символ. Общая форма выглядит так:
string_object.insert( position, new_string );
position
обозначает позицию, перед которой производится вставка. new_string
может быть объектом класса string, C-строкой или символом:
|
string string_object( "Missisippi" ); string::size_type pos = string_object.find( "isi" ); |
string_object.insert( pos+1, 's' );
Можно выделить для вставки подстроку из new_string:
|
string new_string ( "AnnaBelle Lee" ); string_object += ' '; // добавим пробел // найдем начальную и конечную позицию в new_string pos = new_string.find( 'B' ); string::size_type posEnd = new_string.find( ' ' ); string_object.insert( string_object.size(), // позиция вставки new_string, pos, // начало подстроки в new_string posEnd // конец подстроки new_string |
)
string_object
получает значение "Mississippi Belle". Если мы хотим вставить все символы new_string, начиная с pos, последний параметр нужно опустить.
Пусть есть две строки:
|
string sl( "Mississippi" ); |
string s2( "Annabelle" );
Как получить третью строку со значением "Miss Anna"?
string s3; // скопируем первые 4 символа s1 |
// добавим пробел |
// добавим 4 первых символа s2 |
string beauty; // присвоим beauty значение "belle" |
// присвоим beauty значение "belle" |
string current_project( "C++ Primer, 3rd Edition" ); |
int elem_count( const string &word, char elem ) { int occurs = 0; // не надо больше проверять ix for ( int ix=0; ix < word.size(); ++-ix ) if ( word[ ix ] == elem ) ++occurs; return occurs; |
void mumble( const string &st, int index ) { // возможна ошибка char ch = st[ index ]; // ... |
void mumble( const string &st, int index ) { try { char ch = st.at( index ); // ... } catch ( std::out_of_range ){...} // ... |
string cobol_program_crash( "abend" ); |
string sentence( " An ADT provides both interface and implementation." ); string::size_type position = sentence.find_1ast_of( 'A' ); string::size_type length = 3; // заменяем ADT на Abstract Data Type |
string new_str( "Abstract Data Type" ); |
#include <string> typedef string::size_type size_type; // найдем позицию трех букв size_type posA = new_str.find( 'A' ); size_type posD = new_str.find( 'D' ); size_type posT = new_str.find( 'T' ); // нашли: заменим T на "Type" sentence.replace( position+2, 1, new_str, posT, 4 ); // нашли: заменим D на "Data " sentence.replace( position+1, 1, new_str, posD, 5 ); // нашли: заменим A на "Abstract " |
string hmm( "Some celebrate Java as the successor to C++." ); string:: size_type position = hmm.find( 'J' ); // заменим Java на xxxx |
const char *lang = "EiffelAda95JavaModula3"; int index[] = { 0, 6, 11, 15, 22 }; string ahhem( "C++ is the language for today's power programmers." ); |
string sentence( "An ADT provides both interface and implementation." ); // указывает на 'A' в ADT string: iterator start = sentence. begin()+3; // заменяем ADT на Abstract Data Type |
string quote1( "When lilacs last in the dooryard bloom'd" ); |
string generate_salutation( string generic1, string lastname, string generic2, string::size_type pos, |
string lastName( "AnnaP" ); string greetings = |
Иногда необходимо прочитать из входного потока последовательность не интерпретируемых байтов, а типов данных, таких, как char, int, string и т.д. Функция-член get() класса istream
читает по одному байту, а функция getline() читает строку, завершающуюся либо символом перехода на новую строку, либо каким-то иным символом, определяемым пользователем. У функции-члена get() есть три формы:
·
get(char& ch) читает из входного потока один символ (в том числе и пустой) и помещает его в ch. Она возвращает объект iostream, для которого была вызвана. Например, следующая программа собирает статистику о входном потоке, а затем копирует входной поток в выходной:
|
#include <iostream> int main() { char ch; int tab_cnt = 0, nl_cnt = 0, space_cnt = 0, period_cnt = 0, comma_cnt = 0; while ( cin.get(ch)) { switch( ch ) { case ' ': space_cnt++; break; case '\t': tab_cnt++; break; case '\n': nl_cnt++; break; case '.': period_cnt++; break; case ',': comma_cnt++; break; } cout.put(ch); } cout << "\nнаша статистика:\n\t" << "пробелов: " << space_cnt << '\t' << "символов новой строки: " << nl_cnt << '\t' << "табуляций: " << tab_cnt << "\n\t" << "точек: " << period_cnt << '\t' << "запятых: " << comma_cnt << endl; |
}
Функция-член put()
класса ostream
дает альтернативный метод вывода символа в выходной поток: put()
принимает аргумент типа char и возвращает объект класса ostream, для которого была вызвана.
После компиляции и запуска программа печатает следующий результат:
Alice Emma has long flowing red hair. Her Daddy says
#include <iostream> int main() { int ch; // альтернатива: // while ( ch = cin.get() && ch != EOF ) while (( ch = cin.get()) != EOF ) cout.put( ch ); return 0; |
#include <iostream> int main() { const int max_line = 1024; char line[ max_line ]; while ( cin.get( line, max_line )) { // читается не больше max_line - 1 символов, // чтобы оставить место для нуля int get_count = cin.gcount(); cout << "фактически прочитано символов: " << get_count << endl; // что-то сделать со строкой // если встретился символ новой строки, // удалить его, прежде чем приступать к чтению следующей if ( get_count < max_line-1 ) cin.ignore(); } |
#include <iostream> int main() { const int lineSize = 1024; int lcnt = 0; // сколько строк прочитано int max = -1; // длина самой длинной строки char inBuf[ lineSize ]; // читается до конца строки, но не более 1024 символов while (cin.getline( inBuf, lineSize )) { // сколько символов фактически прочитано int readin = cin.gcount(); // статистика: счетчик строк, самая длинная строка ++lcnt; if ( readin > max ) max = readin; cout << "Строка #" << lcnt << "\tПрочитано символов: " << readin << endl; cout.write( inBuf, readin).put('\n').put('\n'); } cout << "Всего прочитано строк: " << lcnt << endl; cout << "Самая длинная строка: " << max << endl; |
// возвращает символ в поток putback( char class ); // устанавливает "указатель на следующий символ потока istream на один символ назад unget(); // возвращает следующий символ (или EOF), // но не извлекает его из потока |
char ch, next, lookahead; while ( cin.get( ch )) { switch (ch) { case '/': // это комментарий? посмотрим с помощью peek() // если да, пропустить остаток строки next = cin.peek(); if ( next == '/' ) cin.ignore( lineSize, '\n' ); break; case '>': // проверка на лексему >>= next = cin.peek(); if ( next == '>' ) { lookahead = cin.get(); next = cin.peek(); if ( next != '=' ) cin.putback( lookahead ); } // ... |
Часто бывает так, что внутреннее представление типа класса изменяется в последующих версиях программы. Допустим, опрос пользователей нашего класса Screen
показал, что для его объектов всегда задается размер экрана 80 ´ 24. В таком случае было бы желательно заменить внутреннее представление экрана менее гибким, но более эффективным:
|
class Screen { public: // функции-члены private: // инициализация статических членов (см. 13.5) static const int _height = 24; static const int _width = 80; string _screen; string::size_type _cursor; |
};
Прежняя реализация функций-членов (то, как они манипулируют данными-членами класса) больше не годится, ее нужно переписать. Но это не означает, что должен измениться и интерфейс функций-членов (список формальных параметров и тип возвращаемого значения).
Если бы данные-члены класса Screen
были открыты и доступны любой функции внутри программы, как отразилось бы на пользователях изменение внутреннего представления этого класса?
·
все функции, которые напрямую обращались к данным-членам старого представления, перестали бы работать. Следовательно, пришлось бы отыскивать и изменять соответствующие части кода;
· так как интерфейс не изменился, то коды, манипулировавшие объектами класса Screen только через функции-члены, не пришлось бы модифицировать. Но поскольку сами функции-члены все же изменились, программу пришлось бы откомпилировать заново.
Сокрытие информации – это формальный механизм, предотвращающий прямой доступ к внутреннему представлению типа класса из функций программы. Ограничение доступа к членам задается с помощью секций тела класса, помеченных ключевыми словами public, private и protected – спецификаторами доступа. Члены, объявленные в секции public, называются открытыми, а объявленные в секциях private и protected
соответственно закрытыми или защищенными.
· открытый член
class Screen { public: void home() { _cursor = 0; } char get() { return _screen[_cursor]; } char get( int, int ); void move( int, int ); // ... private: string _screen; string::size_type _cursor; short _height, _width; |
Объект производного класса фактически построен из нескольких частей. Каждый базовый класс вносит свою долю в виде подобъекта, составленного из нестатических данных-членов этого класса. Объект производного класса построен из подобъектов, соответствующих каждому из его базовых, а также из части, включающей нестатические члены самого производного класса. Так, наш объект NameQuery состоит из подобъекта Query, содержащего члены _loc и _solution, и части, принадлежащей NameQuery,– она содержит только член _name.
Внутри производного класса к членам, унаследованным из базового, можно обращаться напрямую, как к его собственным. (Глубина цепочки наследования не увеличивает затраты времени и не лимитирует доступ к ним.) Например:
|
void NameQuery:: display_partial_solution( ostream &os ) { os << _name << " is found in " << (_solution ? _solution->size() : 0) << " lines of text\n"; |
}
Это касается и доступа к унаследованным функциям-членам базового класса: мы вызываем их так, как если бы они были членами производного – либо через его объект:
|
NameQuery nq( "Frost" ); // вызывается NameQuery::eval() nq.eval(); // вызывается Query::display() |
nq.display();
либо непосредственно из тела другой (или той же самой) функции-члена:
|
void NameQuery:: match_count() { if ( ! _solution ) // вызывается Query::_vec2set() _solution = _vec2set( &_loc ); return _solution->size(); |
}
Однако прямой доступ из производного класса к членам базового запрещен, если имя последнего скрыто в производном классе:
|
class Diffident { public: // ... protected: int _mumble; // ... }; class Shy : public Diffident { public: // ... protected: // имя Diffident::_mumble скрыто string _mumble; // ... |
};
В области видимости Shy
употребление неквалифицированного имени _mumble разрешается в пользу члена _mumble
void Shy:: turn_eyes_down() { // ... _mumble = "excuse me"; // правильно // ошибка: int Diffident::_mumble скрыто _mumble = -1; |
void Shy:: turn_eyes_down() { // ... _mumble = "excuse me"; // правильно // правильно: имя члена базового класса квалифицировано Diffident::_mumble = -1; |
class Diffident { public: void mumble( int softness ); // ... }; class Shy : public Diffident { public: // скрывает видимость функции-члена Diffident::_mumble, // а не перегружает ее void mumble( string whatYaSay ); void print( int soft, string words ); // ... |
Shy simon; // правильно: Shy::mumble( string ) simon.mumble( "pardon me" ); // ошибка: ожидался первый аргумент типа string // Diffident::mumble( int ) невидима |
class Diffident { public: void turn_aside( ); // ... }; class Shy : public Diffident { public: // скрывает видимость // Diffident::turn_aside() void turn_aside(); // ... |
class Shy : public Diffident { public: // один из способов реализовать множество перегруженных // членов базового и производного классов void mumble( string whatYaSay ); void mumble( int softness ) { Diffident::mumble( softness ); } // ... |
class Shy : public Diffident { public: // в стандартном C++ using-объявление // создает множество перегруженных // членов базового и производного классов void mumble( string whatYaSay ); using Diffident::mumble; // ... |
class Query { public: const vector<location>* locations() { return &_loc; } // ... protected: vector<location> _loc; // ... |
bool NameQuery:: compare( const Query *pquery ) { // правильно: защищенный член подобъекта Query int myMatches = _loc.size(); // ошибка: нет прав доступа к защищенному члену // независимого объекта Query int itsMatches = pquery->_loc.size(); return myMatches == itsMatches; |
bool NameQuery:: compare( const Query *pquery ) { // правильно: защищенный член подобъекта Query int myMatches = _loc.size(); // правильно: используется открытый метод доступа int itsMatches = pquery->locations()->size(); return myMatches == itsMatches; |
bool NameQuery:: compare( const NameQuery *pname ) { int myMatches = _loc.size(); // правильно int itsMatches = name->_loc.size(); // тоже правильно return myMatches == itsMatches; |
// ошибка: suffix() - не член класса Query |
// ошибка: _name - не член класса Query |
// ошибка: у класса Query нет базового класса NameQuery |
class Query { friend class NameQuery; public: // ... |
class Base { public: foo( int ); // ... protected: int _bar; double _foo_bar; }; class Derived : public Base { public: foo( string ); bool bar( Base *pb ); void foobar(); // ... protected: string _bar; |
Derived d; d.foo( 1024 ); |
(c) bool Derived::bar( Base *pb ) |
Говорят, что определение функции-члена принадлежит области видимости класса независимо от того, находится ли оно вне или внутри его тела. Отсюда следуют два вывода:
· в определении функции-члена могут быть обращения к любым членам класса, открытым или закрытым, и это не нарушает ограничений доступа;
· когда функция-член обращается к членам класса, операторы доступа “точка” и “стрелка” не необходимы.
Например:
|
#include <string> void Screen::copy( const Screen &sobj ) { // если этот объект и объект sobj - одно и то же, // копирование излишне // мы анализируем указатель this (см. раздел 13.4) if ( this != &sobj ) { _height = sobj._height; _width = sobj._width; _cursor = 0; // создаем новую строку; // ее содержимое такое же, как sobj._screen _screen = sobj._screen; } |
}
Хотя _screen, _height, _width и _cursor
являются закрытыми членами класса Screen, функция-член copy()
работает с ними напрямую. Если при обращении к члену отсутствует оператор доступа, то считается, что речь идет о члене того класса, для которого функция-член вызвана. Если вызвать copy() следующим образом:
|
#include "Screen.h" int main() { Screen s1; // Установить s1 Screen s2; s2.copy(s1); // ... |
}
то параметр sobj
внутри определения copy()
соотносится с объектом s1 из функции main(). Функция-член copy() вызвана для объекта s2, стоящего перед оператором “точка”. Для такого вызова члены _screen, _height, _width и _cursor, при обращении к которым внутри определения этой функции нет оператора доступа, – это члены объекта s2. В следующем разделе мы рассмотрим доступ к членам класса внутри определения функции-члена более подробно и, в частности, покажем, как для поддержки такого доступа применяется указатель this.
Иногда удобно разрешить некоторым функциям доступ к закрытым членам класса. Механизм друзей
позволяет классу разрешать доступ к своим неоткрытым членам.
Объявление друга начинается с ключевого слова friend и может встречаться только внутри определения класса. Так как друзья не являются членами класса, то не имеет значения, в какой секции они объявлены. В примере ниже мы сгруппировали все подобные объявления сразу после заголовка класса:
|
class Screen { friend istream& operator>>( istream&, Screen& ); friend ostream& operator<<( ostream&, const Screen& ); public: // ... оставшаяся часть класса Screen |
};
Операторы ввода и вывода теперь могут напрямую обращаться к закрытым членам класса Screen. Простая реализация оператора вывода выглядит следующим образом:
|
#include <iostream> ostream& operator<<( ostream& os, const Screen& s ) { // правильно: можно обращаться к _height, _width и _screen os << "<" << s._height << "," << s._width << ">"; os << s._screen; return os; |
}
Другом может быть функция из пространства имен, функция-член другого класса или даже целый класс. В последнем случае всем его функциям-членам предоставляется доступ к неоткрытым членам класса, объявляющего дружественные отношения. (В разделе 15.2 друзья обсуждаются более подробно.)
Рассмотрим еще раз перегруженные операторы равенства для класса String, определенные в области видимости пространства имен. Оператор равенства для двух объектов String
выглядит следующим образом:
|
bool operator==( const String &str1, const String &str2 ) { if ( str1.size() != str2.size() ) return false; return strcmp( str1.c_str(), str2.c_str() ) ? false : true; |
}
Сравните это определение с определением того же оператора как функции-члена:
|
bool String::operator==( const String &rhs ) const { if ( _size != rhs._size ) return false; return strcmp( _string, rhs._string ) ? false : true; |
}
Нам пришлось модифицировать способ обращения к закрытым членам класса String. Поскольку новый оператор равенства– это глобальная функция, а не функция-член, у него нет доступа к закрытым членам класса String. Для получения размера объекта String и лежащей в его основе C-строки символов используются функции-члены size() и c_str().
Альтернативной реализацией является объявление глобальных операторов равенства друзьями класса String. Если функция или оператор объявлены таким образом, им предоставляется доступ к неоткрытым членам.
Объявление друга (оно начинается с ключевого слова friend) встречается только внутри определения класса. Поскольку друзья не являются членами класса, объявляющего дружественные отношения, то безразлично, в какой из секций – public, private или protected – они объявлены. В примере ниже мы решили поместить все подобные объявления сразу после заголовка класса:
|
class String { friend bool operator==( const String &, const String & ); friend bool operator==( const char *, const String & ); friend bool operator==( const String &, const char * ); public: // ... остальная часть класса String |
};
В этих трех строчках три перегруженных оператора сравнения, принадлежащие глобальной области видимости, объявляются друзьями класса String, а следовательно, в их определениях можно напрямую обращаться к закрытым членам данного класса:
В главе 9 подробно описывалось, как разрешается вызов перегруженной функции. Если фактические аргументы при вызове имеют тип класса, указателя на тип класса или указателя на члены класса, то на роль возможных кандидатов претендует большее число функций. Следовательно, наличие таких аргументов оказывает влияние на первый шаг процедуры разрешения перегрузки – отбор множества функций-кандидатов.
На третьем шаге этой процедуры выбирается наилучшее соответствие. При этом ранжируются преобразования типов фактических аргументов в типы формальных параметров функции. Если аргументы и параметры имеют тип класса, то в множество возможных преобразований следует включать и последовательности определенных пользователем преобразований, также подвергая их ранжированию.
В этом разделе мы детально рассмотрим, как фактические аргументы и формальные параметры типа класса влияют на отбор функций-кандидатов и как последовательности определенных пользователем преобразований сказываются на выборе наилучшей из устоявших функции.
Следующая реализация шаблона функции не компилируется. Можете ли вы сказать, почему?
|
// в таком виде это не компилируется template < typename type > int count( const vector< type > &vec, type value ) { int count = 0; vector< type >::iterator iter = vec.begin(); while ( iter != vec.end() ) if ( *iter == value ) ++count; return count; |
}
Проблема в том, что у ссылки vec
есть спецификатор const, а мы пытаемся связать с ней итератор без такого спецификатора. Если бы это было разрешено, то ничто не помешало бы нам модифицировать с помощью этого итератора элементы вектора. Для предотвращения подобной ситуации язык требует, чтобы итератор, связанный с const-вектором, был константным. Мы можем сделать это следующим образом:
|
// правильно: это компилируется без ошибок |
vector< type>::const_iterator iter = vec.begin();
Требование, чтобы с const-контейнером был связан только константный итератор, аналогично требованию о том, чтобы const-массив адресовался только константным указателем. В обоих случаях это вызвано необходимостью гарантировать, что содержимое const-контейнера не будет изменено.
Операции begin() и end()
перегружены и возвращают константный или неконстантный итератор в зависимости от наличия спецификатора const в объявлении контейнера. Если дана такая пара объявлений:
|
vector< int > vec0; |
const vector< int > vec1;
то при обращениях к begin() и end() для vec0
будет возвращен неконстантный, а для vec1 – константный итератор:
|
vector< int >::iterator iter0 = vec0.begin(); |
vector< int >::const_iterator iter1 = vec1.begin();
Разумеется, присваивание константному итератору неконстантного разрешено всегда. Например:
|
// правильно: инициализация константного итератора неконстантным |
vector< int >::const_iterator iter2 = vec0.begin();
Библиотека iostream
поддерживает и файловый ввод/вывод. Все операции, применимые в стандартному вводу и выводу, могут быть также применены к файлам. Чтобы использовать файл для ввода или вывода, мы должны включить еще один заголовочный файл:
#include <fstream>
Перед тем как открыть файл для вывода, необходимо объявить объект типа ofstream:
ofstream outfile("name-of-file");
Проверить, удалось ли нам
открыть файл, можно следующим образом:
|
if ( ! outfile ) // false, если файл не открыт |
cerr << "Ошибка открытия файла.\n"
Так же открывается файл и для ввода, только он имеет тип ifstream:
|
ifstream infile("name-of-file"); |
if ( ! infile ) // false, если файл не открыт
cerr << "Ошибка открытия файла.\n"
Ниже приводится текст простой программы, которая читает файл с именем in_file и выводит все прочитанные из этого файла слова, разделяя их пробелом, в другой файл, названный out_file.
|
#include <iostream> #include <fstream> #include <string> int main() { ifstream infile("in_file"); ofstream outfile("out_file"); if ( ! infile ) { cerr << "Ошибка открытия входного файла.\n" return -1; } if ( ! outfile ) { cerr << "Ошибка открытия выходного файла.\n" return -2; } |
string word;
while ( infile >> word )
outfile << word << ' ';
return 0;
}
В главе 20 библиотека ввода/вывода будет рассмотрена подробно. А в следующих разделах мы увидим, как можно создавать новые типы данных, используя механизм классов и шаблонов.
Мы рассмотрели, как объявлять переменные (глава 3), как писать выражения (глава 4) и инструкции (глава 5). Здесь мы покажем, как группировать эти компоненты в определения функций, чтобы облегчить их многократное использование внутри программы. Мы увидим, как объявлять и определять функции и как вызывать их, рассмотрим различные виды передаваемых параметров и обсудим особенности использования каждого вида. Мы расскажем также о различных видах значений, которые может вернуть функция. Будут представлены четыре специальных случая применения функций: встроенные (inline), рекурсивные, написанные на других языках и объявленные директивами связывания, а также функция main(). В завершение главы мы разберем более сложное понятие – указатель на функцию.
Пользователям, по-видимому, понадобится широкий набор операций над объектами типа Screen: возможность перемещать курсор, проверять и устанавливать области экрана и рассчитывать его реальные размеры во время выполнения, а также копировать один объект в другой. Все эти операции можно реализовать с помощью функций-членов.
Функции-члены класса объявляются в его теле. Это объявление выглядит точно так же, как объявление функции в области видимости пространства имен. (Напомним, что глобальная область видимости – это тоже область видимости пространства имен. Глобальные функции рассматривались в разделе 8.2, а пространства имен – в разделе 8.5.) Например:
|
class Screen { public: void home(); void move( int, int ); char get(); char get( int, int ); void checkRange( int, int ); // ... |
};
Определение функции-члена также можно поместить внутрь тела класса:
|
class Screen { public: // определения функций home() и get() void home() { _cursor = 0; } char get() { return _screen[_cursor]; } // ... |
};
home() перемещает курсор в левый верхний угол экрана; get()
возвращает символ, находящийся в текущей позиции курсора.
Функции-члены отличаются от обычных функций следующим:
· функция-член объявлена в области видимости своего класса, следовательно, ее имя не видно за пределами этой области. К функции-члену можно обратиться с помощью одного из операторов доступа к членам – точки (.) или стрелки (->):
|
ptrScreen->home(); |
myScreen.home();
(в разделе 13.9 область видимости класса обсуждается более детально);
· функции-члены имеют право доступа как к открытым, так и к закрытым членам класса, тогда как обычным функциям доступны лишь открытые. Конечно, функции-члены одного класса, как правило, не имеют доступа к данным-членам другого класса.
Функция-член может быть перегруженной (перегруженные функции рассматриваются в главе 9). Однако она способна перегружать лишь другую функцию-член своего класса. По отношению к функциям, объявленным в других классах или пространствах имен, функция-член находится в отдельной области видимости и, следовательно, не может перегружать их. Например, объявление get(int, int) перегружает лишь get() из того же класса Screen:
|
class Screen { public: // объявления перегруженных функций-членов get() char get() { return _screen[_cursor]; } char get( int, int ); // ... |
};
(Подробнее мы остановимся на функциях-членах класса в разделе 13.3.)
Функции-члены реализуют набор операций, применимых к объектам класса. Например, для Screen
такой набор состоит из следующих объявленных в нем функций-членов:
|
class Screen { public: void home() { _cursor = 0; } char get() { return _screen[_cursor]; } char get( int, int ); void move( int, int ); bool checkRange( int, int ); int height() { return _height; } int width() { return _width; } // ... |
};
Хотя у любого объекта класса есть собственная копия всех данных-членов, каждая функция-член существует в единственном экземпляре:
|
Screen myScreen, groupScreen; myScreen.home(); |
groupScreen.home();
При вызове функции home() для объекта myScreen
происходит обращение к его члену _cursor. Когда же эта функция вызывается для объекта groupScreen, то она обращается к члену _cursor именно этого объекта, причем сама функция home() одна и та же. Как же может одна функция-член обращаться к данным-членам разных объектов? Для этого применяется указатель this, рассматриваемый в следующем разделе.
Как и для обычных классов, функция-член шаблона класса может быть определена либо внутри определения шаблона (и тогда называется встроенной), либо вне его. Мы уже встречались со встроенными функциями-членами при рассмотрении шаблона Queue. Например, конструктор Queue
является встроенным, так как определен внутри определения шаблона класса:
|
template <class Type> class Queue { // ... public: // встроенный конструктор Queue() : front( 0 ), back( 0 ) { } // ... |
};
При определении функции-члена шаблона вне определения самого шаблона следует применять специальный синтаксис для обозначения того, членом какого именно шаблона является функция. Определению функции-члена должно предшествовать ключевое слово template, за которым следуют параметры шаблона. Так, конструктор Queue
можно определить следующим образом:
|
template <class Type> class Queue { public: Queue(); private: // ... }; template <class Type> inline Queue<Type>:: |
Queue( ) { front = back = 0; }
За первым вхождением Queue
(перед оператором ::) следует список параметров, показывающий, какому шаблону принадлежит данная функция-член. Второе вхождение Queue в определение конструктора (после оператора ::) содержит имя функции-члена, за которым может следовать список параметров шаблона, хотя это и необязательно. После имени функции идет ее определение;. в нем могут быть ссылки на параметр шаблона Type всюду, где в определении обычной функции использовалось бы имя типа.
Функция-член шаблона класса сама является шаблоном. Стандарт C++ требует, чтобы она конкретизировалась только при вызове либо при взятии ее адреса. (Некоторые более старые компиляторы конкретизируют такие функции одновременно с конкретизацией самого шаблона класса.) При конкретизации функции-члена используется тип того объекта, для которого функция вызвана:
Queue<string> qs;
Объект qs
имеет тип Queue<string>. При инициализации объекта этого класса вызывается конструктор Queue<string>. В данном случае аргументом, которым конкретизируется функция-член (конструктор), будет string.
Функция-член шаблона конкретизируется только при реальном использовании в программе (т.е. при вызове или взятии ее адреса). От того, в какой именно момент конкретизируется функция-член, зависит разрешение имен в ее определении (см. раздел 16.11) и объявление ее специализации (см. раздел 16.9).
Чтобы понять, как определяются и используются функции-члены шаблонов классов, продолжим изучение шаблонов Queue и QueueItem:
|
template <class Type> class Queue { public: Queue() : front( 0 ), back ( 0 ) { } ~Queue(); Type& remove(); void add( const Type & ); bool is_empty() const { return front == 0; } private: QueueItem<Type> *front; QueueItem<Type> *back; |
};
Деструктор, а также функции-члены remove() и add()
определены не в теле шаблона, а вне его. Деструктор Queue
опустошает очередь:
|
template <class Type> Queue<Type>::~Queue() { while (! is_empty() ) remove(); |
}
Функция-член Queue<Type>::add()
помещает новый элемент в конец очереди:
|
template <class Type> void Queue<Type>::add( const Type &val ) { // создать новый объект QueueItem QueueItem<Type> *pt = new QueueItem<Type>( val ); if ( is_empty() ) front = back = pt; else { back->next = pt; back = pt; } |
}
Функция-член Queue<Type>::remove()
возвращает значение элемента, находящегося в начале очереди, и удаляет сам элемент.
|
#include <iostream> #include <cstdlib> template <class Type> Type Queue<Type>::remove() { if ( is_empty() ) { cerr << "remove() вызвана для пустой очереди\n"; exit( -1 ); } QueueItem<Type> *pt = front; front = front->next; Type retval = pt->item; delete pt; return retval; |
}
Мы поместили определения функций-членов в заголовочный файл Queue.h, включив его в каждый файл, где возможны конкретизации функций. (Обоснование этого решения, а также рассмотрение более общих вопросов, касающихся модели компиляции шаблонов, мы отложим до раздела 16.8.)
В следующей программе иллюстрируется использование и конкретизация функции-члена шаблона Queue:
|
#include <iostream> #include "Queue.h" int main() { // конкретизируется класс Queue<int> // оператор new требует, чтобы Queue<int> был определен Queue<int> *p_qi = new Queue<int>; int ival; for ( ival = 0; ival < 10; ++ival ) // конкретизируется функция-член add() p_qi->add( ival ); int err_cnt = 0; for ( ival = 0; ival < 10; ++ival ) { // конкретизируется функция-член remove() int qval = p_qi->remove(); if ( ival != qval ) err_cnt++; } if ( !err_cnt ) cout << "!! queue executed ok\n"; else cerr << "?? queue errors: " << err_cnt << endl; return 0; |
}
После компиляции и запуска программа выводит следующую строку:
!! queue executed ok
Упражнение 16.5
Используя шаблон класса Screen, определенный в разделе 16.2, реализуйте функции-члены Screen
(см. разделы 13.3, 13.4 и 13.6) в виде функций-членов шаблона.
Любая попытка модифицировать константный объект из программы обычно помечается компилятором как ошибка. Например:
|
const char blank = ' '; |
blank = '\n'; // ошибка
Однако объект класса, как правило, не модифицируется программой напрямую. Вместо этого вызывается та или иная открытая функция-член. Чтобы не было “покушений” на константность объекта, компилятор должен различать безопасные (те, которые не изменяют объект) и небезопасные (те, которые пытаются это сделать) функции-члены:
|
const Screen blankScreen; blankScreen.display(); // читает объект класса |
blankScreen.set( '*' ); // ошибка: модифицирует объект класса
Проектировщик класса может указать, какие функции-члены не модифицируют объект, объявив их константными с помощью спецификатора const:
|
class Screen { public: char get() const { return _screen[_cursor]; } // ... |
};
Для класса, объявленного как const, могут быть вызваны только те функции-члены, которые также объявлены со спецификатором const. Ключевое слово const
помещается между списком параметров и телом функции-члена. Для константной функции-члена, определенной вне тела класса, это слово должно присутствовать как в объявлении, так и в определении:
|
class Screen { public: bool isEqual( char ch ) const; // ... private: string::size_type _cursor; string _screen; // ... }; bool Screen::isEqual( char ch ) const { return ch == _screen[_cursor]; |
}
Запрещено объявлять константную функцию-член, которая модифицирует члены класса. Например, в следующем упрощенном определении:
|
class Screen { public: int ok() const { return _cursor; } void error( int ival ) const { _cursor = ival; } // ... private: string::size_type _cursor; // ... |
};
определение функции-члена ok()
корректно, так как она не изменяет значения _cursor. В определении же error()
значение _cursor
изменяется, поэтому такая функция-член не может быть объявлена константной и компилятор выдает сообщение об ошибке:
#include <cstring> class Text { public: void bad( const string &parm ) const; private: char *_text; }; void Text::bad( const string &parm ) const { _text = parm.c_str(); // ошибка: нельзя модифицировать _text for ( int ix = 0; ix < parm.size(); ++ix ) _text[ix] = parm[ix]; // плохой стиль, но не ошибка |
class Screen { public: char get(int x, int y); char get(int x, int y) const; // ... |
int main() { const Screen cs; Screen s; char ch = cs.get(0,0); // вызывает константную функцию-член ch = s.get(0,0); // вызывает неконстантную функцию-член |
class Screen { public: char poll() volatile; // ... }; |
Функцией-кандидатом называется функция, имеющая то же имя, что и вызванная. Кандидаты отыскиваются двумя способами:
·
объявление функции видимо в точке вызова. В следующем примере
|
void f(); void f( int ); void f( double, double = 3.4 ); void f( char*, char* ); int main() { f( 5.6 ); // для разрешения этого вызова есть четыре кандидата return 0; |
}
все четыре функции f()
удовлетворяют этому условию. Поэтому множество кандидатов содержит четыре элемента;
· если тип фактического аргумента объявлен внутри некоторого пространства имен, то функции-члены этого пространства, имеющие то же имя, что и вызванная функция, добавляются в множество кандидатов:
|
namespace NS { class C { /* ... */ }; void takeC( C& ); } // тип cobj - это класс C, объявленный в пространстве имен NS NS::C obj; int main() { // в точке вызова не видна ни одна из функций takeC() takeC( cobj); // правильно: вызывается NS::takeC( C& ), // потому что аргумент имеет тип NS::C, следовательно, // принимается во внимание функция takeC(), // объявленная в пространстве имен NS return 0; |
}
Таким образом, совокупность кандидатов является объединением множества функций, видимых в точке вызова, и множества функций, объявленных в том же пространстве имен, к которому принадлежат типы фактических аргументов.
При идентификации множества перегруженных функций, видимых в точке вызова, применимы уже рассмотренные ранее правила.
Функция, объявленная во вложенной области видимости, скрывает, а не перегружает одноименную функцию во внешней области. В такой ситуации кандидатами будут только функции из во вложенной области, т.е. такие, которые не скрыты при вызове. В следующем примере функциями-кандидатами, видимыми в точке вызова, являются format(double) и format(char*):
Функцией-кандидатом называется функция с тем же именем, что и вызванная. Предположим, что имеется такой вызов:
|
SmallInt si(15); |
add( si, 566 );
Функция-кандидат должна иметь имя add. Какие из объявлений add()
принимаются во внимание? Те, которые видимы в точке вызова.
Например, обе функции add(), объявленные в глобальной области видимости, будут кандидатами для следующего вызова:
|
const matrix& add( const matrix &, int ); double add( double, double ); int main() { SmallInt si(15); add( si, 566 ); // ... |
}
Рассмотрение функций, чьи объявления видны в точке вызова, производится не только для вызовов с аргументами типа класса. Однако для них поиск объявлений проводится еще в двух областях видимости:
· если фактический аргумент – это объект типа класса, указатель или ссылка на тип класса либо указатель на член класса и этот тип объявлен в пользовательском пространстве имен, то к множеству функций-кандидатов добавляются функции, объявленные в этом же пространстве и имеющие то же имя, что и вызванная:
|
namespace NS { class SmallInt { /* ... */ }; class String { /* ... */ }; String add( const String &, const String & ); } int main() { // si имеет тип class SmallInt: // класс объявлен в пространстве имен NS NS::SmallInt si(15); add( si, 566 ); // NS::add() - функция-кандидат return 0; |
}
Аргумент si
имеет тип SmallInt, т.е. тип класса, объявленного в пространстве имен NS. Поэтому к множеству функций-кандидатов добавляется add(const String &, const String &), объявленная в этом пространстве имен;
· если фактический аргумент – это объект типа класса, указатель или ссылка на класс либо указатель на член класса и у этого класса есть друзья, имеющие то же имя, что и вызванная функция, то они добавляются к множеству функций-кандидатов:
|
namespace NS { class SmallInt { friend SmallInt add( SmallInt, int ) { /* ... */ } }; } int main() { NS::SmallInt si(15); add( si, 566 ); // функция-друг add() - кандидат return 0; |
Наследование влияет на первый шаг процедуры разрешения перегрузки функции – формирование множества кандидатов для данного вызова, причем это влияние может быть различным в зависимости от того, рассматривается ли вызов обычной функции вида
func( args );
или функции-члена с помощью операторов доступа “точка” или “стрелка”:
|
object.memfunc( args ); |
pointer->memfunc( args );
В данном разделе мы изучим оба случая.
Если аргумент обычной функции имеет тип класса, ссылки или указателя на тип класса, и класс определен в пространстве имен, то кандидатами будут все одноименные функции, объявленные в этом пространстве, даже если они невидимы в точке вызова (подробнее об этом говорилось в разделе 15.10). Если аргумент при наследовании имеет тип класса, ссылки или указателя на тип класса, и у этого класса есть базовые, то в множество кандидатов добавляются также функции, объявленные в тех пространствах имен, где определены базовые классы. Например:
|
namespace NS { class ZooAnimal { /* ... */ }; void display( const ZooAnimal& ); } // базовый класс Bear объявлен в пространстве имен NS class Bear : public NS::ZooAnimal { }; int main() { Bear baloo; display( baloo ); return 0; |
}
Аргумент baloo
имеет тип класса Bear. Кандидатами для вызова display() будут не только функции, объявления которых видимы в точке ее вызова, но также и те, что объявлены в пространствах имен, в которых объявлены класс Bear и его базовый класс ZooAnimal. Поэтому в множество кандидатов добавляется функция display(const ZooAnimal&), объявленная в пространстве имен NS.
Если аргумент имеет тип класса и в определении этого класса объявлены функции-друзья с тем же именем, что и вызванная функция, то эти друзья также будут кандидатами, даже если их объявления не видны в точке вызова (см. раздел 15.10). Если аргумент при наследовании имеет тип класса, у которого есть базовые, то в множество кандидатов добавляются одноименные функции-друзья каждого из них. Предположим, что в предыдущем примере display() объявлена как функция-друг ZooAnimal:
Рассмотрим два вида вызовов функции-члена:
|
mc.mf( arg ); |
pmc->mf( arg );
где mc – выражение типа myClass, а pmc – выражение типа “указатель на тип myClass”. Множество кандидатов для обоих вызовов составлено из функций, найденных в области видимости класса myClass при поиске объявления mf().
Аналогично для вызова функции вида
myClass::mf( arg );
множество кандидатов также состоит из функций, найденных в области видимости класса myClass при поиске объявления mf(). Например:
|
class myClass { public: void mf( double ); void mf( char, char = '\n' ); static void mf( int* ); // ... }; int main() { myClass mc; int iobj; mc.mf( iobj ); |
}
Кандидатами для вызова функции в main()
являются все три функции-члена mf(), объявленные в myClass:
|
void mf( double ); void mf( char, char = '\n' ); |
static void mf( int* );
Если бы в myClass не было объявлено ни одной функции-члена с именем mf(), то множество кандидатов оказалось бы пустым. (На самом деле рассматривались бы также и функции из базовых классов. О том, как они попадают в это множество, мы поговорим в разделе 19.3.) Если для вызова функции не оказывается кандидатов, компилятор выдает сообщение об ошибке.
Когда вызов функции вида
calc(t)
встречается в области видимости класса (например, внутри функции-члена), то первая часть множества кандидатов, описанного в предыдущем подразделе (т.е. множество, включающее объявления функций, видимых в точке вызова), может содержать не только функции-члены класса. Для построения такого множества применяется разрешение имени. (Эта тема детально разбиралась в разделах 13.9 – 13.12.)
Рассмотрим пример:
|
namespace NS { struct myClass { void k( int ); static void k( char* ); void mf(); }; int k( double ); }; void h(char); void NS::myClass::mf() { h('a'); // вызывается глобальная h( char ) k(4); // вызывается myClass::k( int ) |
}
Как отмечалось в разделе 13.11, квалификаторы NS::myClass:: просматриваются в обратном порядке: сначала поиск видимого объявления для имени, использованного в определении функции-члена mf(), ведется в классе myClass, а затем – в пространстве имен NS. Рассмотрим первый вызов:
h( 'a' );
При разрешении имени h() в определении функции-члена mf() сначала просматриваются функции-члены myClass. Поскольку функции-члена с таким именем в области видимости этого класса нет, то далее поиск идет в пространстве имен NS. Функции h()нет и там, поэтому мы переходим в глобальную область видимости. Результат – глобальная функция h(char), единственная функция-кандидат, видимая в точке вызова.
Как только найдено подходящее объявление, поиск прекращается. Следовательно, множество содержит только те функции, объявления которых находятся в областях видимости, где разрешение имени завершилось успешно. Это можно наблюдать на примере построения множества кандидатов для вызова
k( 4 );
Сначала поиск ведется в области видимости класса myClass. При этом найдены две функции-члена k(int) и k(char*). Поскольку множество кандидатов содержит лишь функции, объявленные в той области, где разрешение успешно завершилось, то пространство имен NS не просматривается и функция k(double) в данное множество не включается.
Если обнаруживается, что вызов неоднозначен, поскольку в множестве нет наиболее подходящей функции, то компилятор выдает сообщение об ошибке. Поиск кандидатов, лучше соответствующих фактическим аргументам, в объемлющих областях видимости не производится.
При запуске программы мы, как правило, передаем ей информацию в командной строке. Например, можно написать
prog -d -o of lie dataO
Фактические параметры являются аргументами функции main() и могут быть получены из массива C-строк с именем argv; мы покажем, как их использовать.
Во всех предыдущих примерах определение main()
содержало пустой список:
int main() { ... }
Развернутая сигнатура main()
позволяет получить доступ к параметрам, которые были заданы пользователем в командной строке:
int main( int argc, char *argv[] ){...}
argc
содержит их количество, а argv – C-строки, представляющие собой отдельные значения (в командной строке они разделяются пробелами). Скажем, при запуске команды
prog -d -o ofile data0
argc
получает значение 5, а argv
включает следующие строки:
argv[ 0 ] = "prog";
argv[ 1 ] = "-d";
argv[ 2 ] = "-o";
argv[ 3 ] = "ofile";
argv[ 4 ] = "dataO";
В argv[0]
всегда входит имя команды (программы). Элементы с индексами от 1 до argc-1
служат параметрами.
Посмотрим, как можно извлечь и использовать значения, помещенные в argv. Пусть программа из нашего примера вызывается таким образом:
|
prog [-d] [-h] [-v] [-o output_file] [-l limit_value] file_name |
[ file_name [file_name [ ... ]]]
Параметры в квадратных скобках являются необязательными. Вот, например, запуск программы с их минимальным количеством – одним лишь именем файла:
prog chap1.doc
Но можно запускать и так:
|
prog -l 1024 -o chap1-2.out chapl.doc chap2.doc prog d chap3.doc |
prog -l 512 -d chap4.doc
При разборе параметров командной строки выполняются следующие основные шаги:
1. По очереди извлечь каждый параметр из argv. Мы используем для этого цикл for с начальным индексом 1
(пропуская, таким образом, имя программы):
|
for ( int ix = 1; ix < argc; ++ix ) { char *pchar = argv[ ix ]; // ... |
}
2. Определить тип параметра. Если строка начинается с дефиса (-), это одна из опций { h, d, v, l, o}. В противном случае это может быть либо значение, ассоциированное с опцией (максимальный размер для -l, имя выходного файла для -o), либо имя входного файла. Чтобы определить, начинается ли строка с дефиса, используем инструкцию switch:
switch ( pchar[ 0 ] ) { case '-': { // -h, -d, -v, -l, -o } default: { // обработаем максимальный размер для опции -1 // имя выходного файла для -o // имена входных файлов ... } |
case '-': { switch( pchar[ 1 ] ) { case 'd': // обработка опции debug break; case 'v': // обработка опции version break; case 'h': // обработка опции help break; case 'o': // приготовимся обработать выходной файл break; case 'l': // приготовимся обработать макс.размер break; default: // неопознанная опция: // сообщить об ошибке и завершить выполнение } |
case 'd': debug_on = true; |
if ( debug_on ) |
case 'v': cout << program_name << "::" << program_version << endl; |
case 'h': // break не нужен: usage() вызывает exit() |
// если ofi1e_on==true, // следующий параметр - имя выходного файла bool ofi1e_on = false; // если ofi1e_on==true, // следующий параметр - максимальный размер |
case 'l': limit_on = true; break; case 'o': ofile_on = true; |
// обработаем максимальный размер для опции -1 // имя выходного файла для -o // имена входных файлов ... default: { // ofile_on включена, если -o встречалась if ( ofile_on ) { // обработаем имя выходного файла // выключим ofile_on } else if ( limit_on ) { // если -l встречалась // обработаем максимальный размер // выключим limit_on } else { // обработаем имя входного файла } |
if ( ofile_on ) { ofile_on = false; ofile = pchar; |
// int limit; else if ( limit_on ) { limit_on = false; limit = atoi( pchar ); if ( limit < 0 ) { cerr << program_name << "::" << program_version << " : error: " << "negative value for limit.\n\n"; usage( -2 ); } |
else |
prog - d dataOl |
#include <iostream> #include <string> #include <vector> #include <ctype.h> const char *const program_name = "comline"; const char *const program_version = "version 0.01 (08/07/97)"; inline void usage( int exit_value = 0 ) { // печатает отформатированное сообщение о порядке вызова // и завершает программу с кодом exit_value ... cerr << "порядок вызова:\n" << program_name << " " << "[-d] [-h] [-v] \n\t" << "[-o output_file] [-l limit] \n\t" << "file_name\n\t[file_name [file_name [ ... ]]]\n\n" << "где [] указывает на необязательность опции:\n\n\t" << "-h: справка.\n\t\t" << "печать этого сообщения и выход\n\n\t" << "-v: версия.\n\t\t" << "печать информации о версии программы и выход\n\n\t" << "-d: отладка.\n\t\t включает отладочную печать\n\n\t" << "-l limit\n\t\t" << "limit должен быть неотрицательным целым числом\n\n\t" << "-o ofile\n\t\t" << "файл, в который выводится результат\n\t\t" << "по умолчанию результат записывается на стандартный вывод\n\n" << "file_name\n\t\t" << "имя подлежащего обработке файла\n\t\t" << "должно быть задано хотя бы одно имя --\n\t\t" << "но максимальное число не ограничено\n\n" << "примеры:\n\t\t" << "$command chapter7.doc\n\t\t" << "$command -d -l 1024 -o test_7_8 " << "chapter7.doc chapter8.doc\n\n"; exit( exit_value ); } int main( int argc, char* argv[] ) { bool debug_on = false; bool ofile_on = false; bool limit_on = false; int limit = -1; string ofile; vector<string> file_names; cout << "демонстрация обработки параметров в командной строке:\n" << "argc: " << argc << endl; for ( int ix = 1; ix < argc; ++ix ) { cout << "argv[ " << ix << " ]: " << argv[ ix ] << endl; char *pchar = argv[ ix ]; switch ( pchar[ 0 ] ) { case '-': { cout << "встретился \'-\'\n"; switch( pchar[ 1 ] ) { case 'd': cout << "встретилась -d: " << "отладочная печать включена\n"; debug_on = true; break; case 'v': cout << "встретилась -v: " << "выводится информация о версии\n"; cout << program_name << " :: " << program_version << endl; return 0; case 'h': cout << "встретилась -h: " << "справка\n"; // break не нужен: usage() завершает программу usage(); case 'o': cout << "встретилась -o: выходной файл\n"; ofile_on = true; break; case 'l': cout << "встретилась -l: " << "ограничение ресурса\n"; limit_on = true; break; default: cerr << program_name << " : ошибка : " << "неопознанная опция: - " << pchar << "\n\n"; // break не нужен: usage() завершает программу usage( -1 ); } break; } default: // либо имя файла cout << "default: параметр без дефиса: " << pchar << endl; if ( ofile_on ) { ofile_on = false; ofile = pchar; } else if ( limit_on ) { limit_on = false; limit = atoi( pchar ); if ( limit < 0 ) { cerr << program_name << " : ошибка : " << "отрицательное значение limit.\n\n"; usage( -2 ); } } else file_names.push_back( string( pchar )); break; } } if ( file_names.empty() ) { cerr << program_name << " : ошибка : " << "не задан ни один входной файл.\n\n"; usage( -3 ); } if ( limit != -1 ) cout << "Заданное пользователем значение limit: " << limit << endl; if ( ! ofile.empty() ) cout << "Заданный пользователем выходной файл: " << ofile << endl; cout << (file_names.size() == 1 ? "Файл, " : "Файлы, ") << "подлежащий(е) обработке:\n"; for ( int inx = 0; inx < file_names.size(); ++inx ) cout << "\t" << file_names[ inx ] << endl; |
Объявление функции в глобальной области видимости вводит глобальную функцию, а объявление переменной – глобальный объект. Глобальный объект существует на протяжении всего времени выполнения программы. Время жизни глобального объекта начинается с момента запуска программы и заканчивается с ее завершением.
Для того чтобы глобальную функцию можно было вызвать или взять ее адрес, она должна иметь определение. Любой глобальный объект, используемый в программе, должен быть определен, причем только один раз. Встроенные функции могут определяться несколько раз, если только все определения совпадают. Такое требование единственности или точного совпадения получило название правила одного определения (ПОО). В этом разделе мы покажем, как следует вводить глобальные объекты и функции в программе, чтобы ПОО соблюдалось.
Прежде чем детально описывать множественное и виртуальное наследование, покажем, зачем оно нужно. Наш первый пример взят из области трехмерной компьютерной графики. Но сначала познакомимся с предметной областью.
В компьютере сцена представляется графом сцены, который содержит информацию о геометрии (трехмерные модели), один или более источников освещения (иначе сцена будет погружена во тьму), камеру (без нее мы не можем смотреть на сцену) и несколько трансформационных узлов, с помощью которых позиционируются элементы.
Процесс применения источников освещения и камеры к геометрической модели для получения двумерного изображения, отображаемого на дисплее, называется рендерингом. В алгоритме рендеринга учитываются два основных аспекта: природа источника освещения сцены и свойства материалов поверхностей объектов, такие, как цвет, шероховатость и прозрачность. Ясно, что перышки на белоснежных крыльях феи выглядят совершенно не так, как капающие из ее глаз слезы, хотя те и другие освещены одним и тем же серебристым светом.
Добавление объектов к сцене, их перемещение, игра с источниками освещения и геометрией– работа компьютерного художника. Наша задача – предоставить интерактивную поддержку для манипуляций с графом сцены на экране. Предположим, что в текущей версии своего инструмента мы решили воспользоваться каркасом приложений Open Inventor для C++ (см. [WERNECKE94]), но с помощью подтипизации расширили его, создав собственные абстракции нужных нам классов. Например, Open Inventor располагает тремя встроенными источниками освещения, производными от абстрактного базового класса SoLight:
|
class SoSpotLight : public SoLight { ... } class SoPointLight : public SoLight { ... } |
class SoDirectionalLight : public SoLight { ... }
Префикс So
служит для того, чтобы дать уникальные имена сущностям, которые в области компьютерной графики весьма распространены (данный каркас приложений проектировался еще до появления пространств имен). Точечный источник (point light) – это источник света, излучающий, как солнце, во всех направлениях. Направленный источник (directional light) – источник света, излучающий в одном направлении. Прожектор
class RiSpotLight : public SoSpotLight { ... } class RiPointLight : public SoPointLight { ... } |
SoLight *plight = next_scene_light(); if ( RiDirectionalLight *pdilite = dynamic_cast<RiDirectionalLight*>( plight )) pdilite->scls.cast_shadow_map(); else if ( RiSpotLight *pslite = dynamic_cast<RiSpotLight*>( plight )) pslite->scls.cast_shadow_map(); |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |

|
class RiDirectionalLight : public SoDirectionalLight, public SCLS { ... }; class RiSpotLight : public SoSpotLight, public SCLS { ... }; // ... SoLight *plight = next_scene_light(); if ( SCLS *pscls = dynamic_cast<SCLS*>(plight)) |
|
class SoLight : public SoNode { public: void cast_shadow_map() { if ( _scls ) _scls->cast_shadow_map(); } // ... protected: SCLS *_scls; }; // ... SdSoLight *plight = next_scene_light(); |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
В разделе 2.4 мы уже упоминали о том, что в объектном проектировании обычно есть один разработчик, который конструирует и реализует класс, и много пользователей, применяющих предоставленный открытый интерфейс. Это разделение ответственности отразилось в концепции открытого и закрытого доступа к членам класса.
Когда используется наследование, у класса оказывается множество разработчиков. Во-первых, тот, кто предоставил реализацию базового класса (и, возможно, некоторых производных от него), а во-вторых, те, кто разрабатывал производные классы на различных уровнях иерархии. Этот род деятельности тоже относится к проектированию. Разработчик подтипа часто (хотя и не всегда) должен иметь доступ к реализации базового класса. Чтобы разрешить такой вид доступа, но все же предотвратить неограниченный доступ к деталям реализации класса, вводится дополнительный уровень доступа– protected (защищенный). Данные и функции-члены, помещенные в секцию protected
некоторого класса, остаются недоступными вызывающей программе, но обращение к ним из производных классов разрешено. (Все находящееся в секции private
базового класса доступно только ему, но не производным.)
Критерии помещения того или иного члена в секцию public одинаковы как для объектного, так и для объектно-ориентированного проектирования. Меняется только точка зрения на то, следует ли объявлять член закрытым или защищенным. Член базового класса объявляется закрытым, если мы не хотим, чтобы производные классы имели к нему прямой доступ; и защищенным, если его семантика такова, что для эффективной реализации производного класса может потребоваться прямой доступ к нему. При проектировании класса, который предполагается использовать в качестве базового, надо также принимать во внимание особенности функций, зависящих от типа, – виртуальных функций в иерархии классов.
На следующем шаге проектирования иерархии классов Query следует ответить на такие вопросы:
(a) Какие операции следует предоставить в открытом интерфейсе иерархии классов Query?
(b) Какие из них следует объявить виртуальными?
(c) Какие дополнительные операции могут потребоваться производным классам?
(d) Какие данные-члены следует объявить в нашем абстрактном базовом классе Query?
(e) Какие данные-члены могут потребоваться производным классам?
К сожалению, однозначно ответить на эти вопросы невозможно. Как мы увидим, процесс объектно-ориентированного проектирования по своей природе итеративен, эволюционирующая иерархия классов требует и добавлений, и модификаций. В оставшейся части этого раздела мы будем постепенно уточнять иерархию классов Query.
RTTI позволяет программам, которые манипулируют объектами через указатели или ссылки на базовые классы, получить истинный производный тип адресуемого объекта. Для поддержки RTTI в языке C++ есть два оператора:
· оператор dynamic_cast
поддерживает преобразования типов во время выполнения, обеспечивая безопасную навигацию по иерархии классов. Он позволяет трансформировать указатель на базовый класс в указатель на производный от него, а также преобразовать l-значение, ссылающееся на базовый класс, в ссылку на производный, но только в том случае, если это завершится успешно;
· оператор typeid
позволяет получить фактический производный тип объекта, адресованного указателем или ссылкой.
Однако для получения информации о типе производного класса операнд любого из операторов dynamic_cast или typeid
должен иметь тип класса, в котором есть хотя бы одна виртуальная функция. Таким образом, операторы RTTI – это события времени выполнения для классов с виртуальными функциями и события времени компиляции для всех остальных типов. В данном разделе мы более подробно познакомимся с их возможностями.
Использование RTTI оказывается необходимым при реализации таких приложений, как отладчики или объектные базы данных, когда тип объектов, которыми манипулирует программа, становится известен только во время выполнения путем исследования RTTI-информации, хранящейся вместе с типами объектов. Однако лучше пользоваться статической системой типов C++, поскольку она безопаснее и эффективнее.
В начале этого раздела мы определили иерархию классов исключений, с помощью которой наша программа сообщает об аномальных ситуациях. В стандартной библиотеке C++ есть аналогичная иерархия, предназначенная для извещения о проблемах при выполнении функций из самой стандартной библиотеки. Эти классы исключений вы можете использовать в своих программах непосредственно или создать производные от них классы для описания собственных специфических исключений.
Корневой класс исключения в стандартной иерархии называется exception. Он определен в стандартном заголовочном файле <exception> и является базовым для всех исключений, возбуждаемых функциями из стандартной библиотеки. Класс exception
имеет следующий интерфейс:
|
namespace std { class exception public: exception() throw(); exception( const exception & ) throw(); exception& operator=( const exception & ) throw(); virtual ~exception() throw(); virtual const char* what() const throw(); }; |
}
Как и всякий другой класс из стандартной библиотеки C++, exception помещен в пространство имен std, чтобы не засорять глобальное пространство имен программы.
Первые четыре функции-члена в определении класса – это конструктор по умолчанию, копирующий конструктор, копирующий оператор присваивания и деструктор. Поскольку все они открыты, любая программа может свободно создавать и копировать объекты-исключения, а также присваивать им значения. Деструктор объявлен виртуальным, чтобы сделать возможным дальнейшее наследование классу exception.
Самой интересной в этом списке является виртуальная функция what(), которая возвращает C-строку с текстовым описанием возбужденного исключения. Классы, производные от exception, могут заместить what()
собственной версией, которая лучше характеризует объект-исключение.
Отметим, что все функции в определении класса exception имеют пустую спецификацию throw(), т.е. не возбуждают никаких исключений. Программа может манипулировать объектами-исключениями (к примеру, внутри catch-обработчиков типа exception), не опасаясь, что функции создания, копирования и уничтожения этих объектов возбудят исключения.
namespace std { class logic_error : public exception { // логическая ошибка public: explicit logic_error( const string &what_arg ); }; class invalid_argument : public logic_error { // неверный аргумент public: explicit invalid_argument( const string &what_arg ); }; class out_of_range : public logic_error { // вне диапазона public: explicit out_of_range( const string &what_arg ); }; class length_error : public logic_error { // неверная длина public: explicit length_error( const string &what_arg ); }; class domain_error : public logic_error { // вне допустимой области public: explicit domain_error( const string &what_arg ); }; |
namespace std { class runtime_error : public exception { // ошибка времени выполнения public: explicit runtime_error( const string &what_arg ); }; class range_error : public runtime_error { // ошибка диапазона public: explicit range_error( const string &what_arg ); }; class overflow_error : public runtime_error { // переполнение public: explicit overflow_error( const string &what_arg ); }; class underflow_error : public runtime_error { // потеря значимости public: explicit underflow_error( const string &what_arg ); }; |
#include <stdexcept> (a) void operate() throw( logic_error ); (b) int mathErr( int ) throw( underflow_error, overflow_error ); |
#include <stdexcept> int main() { try { // использование функций из стандартной библиотеки } catch( exception ) { } catch( runtime_error &re ) { } catch( overflow_error eobj ) { } |
int main() { // использование стандартной библиотеки |
#include <stdexcept> #include <string> template <class elemType> class Array { public: // ... elemType& operator[]( int ix ) const { if ( ix < 0 || ix >= _size ) { string eObj = "ошибка: вне диапазона в Array<elemType>::operator[]()"; throw out_of_range( eObj ); } return _ia[ix]; } // ... private: int _size; elemType *_ia; |
int main() { try { // функция main() такая же, как в разделе 16.2 } catch ( const out_of_range &excep ) { // печатается: // ошибка: вне диапазона в Array<elemType>::operator[]() cerr << excep.what() << "\n"; return -1; } |
Перегружать можно только предопределенные операторы языка C++ (см. табл. 15.1).
Таблица 15.1. Перегружаемые операторы
| + | - | * | / | % | ^ | & | | | ~ | |||||||||
| ! | , | = | < | > | <= | >= | ++ | -- | |||||||||
| << | >> | == | != | && | || | += | -= | /= | |||||||||
| %= | ^= | &= | |= | *= | <<= | >>= | [] | () | |||||||||
| -> | ->* | new | new[] | delete | delete[] |
Проектировщик класса не вправе объявить перегруженным оператор с другим именем. Так, при попытке объявить оператор ** для возведения в степень компилятор выдаст сообщение об ошибке.
Следующие четыре оператора языка C++ не могут быть перегружены:
| // неперегружаемые операторы |
:: .* . ?:
Предопределенное назначение оператора нельзя изменить для встроенных типов. Например, не разрешается переопределить встроенный оператор сложения целых чисел так, чтобы он проверял результат на переполнение.
| // ошибка: нельзя переопределить встроенный оператор сложения int |
int operator+( int, int );
Нельзя также определять дополнительные операторы для встроенных типов данных, например добавить к множеству встроенных операций operator+ для сложения двух массивов.
Перегруженный оператор определяется исключительно для операндов типа класса или перечисления и может быть объявлен только как член класса или пространства имен, принимая хотя бы один параметр типа класса или перечисления (переданный по значению или по ссылке).
Предопределенные приоритеты операторов (см. раздел 4.13) изменить нельзя. Независимо от типа класса и реализации оператора в инструкции
x == y + z;
всегда сначала выполняется operator+, а затем operator==; однако помощью скобок порядок можно изменить.
Предопределенная арность операторов также должна быть сохранена. К примеру, унарный логический оператор НЕ нельзя определить как бинарный оператор для двух объектов класса String. Следующая реализация некорректна и приведет к ошибке компиляции:
| // некорректно: ! - это унарный оператор
bool operator!( const String &s1, const String &s2 ) { return ( strcmp( s1.c_str(), s2.c_str() ) != 0 ); |
}
Для встроенных типов четыре предопределенных оператора ("+", "-", "*" и "&") используются либо как унарные, либо как бинарные. В любом из этих качеств они могут быть перегружены.
Для всех перегруженных операторов, за исключением operator(), недопустимы аргументы по умолчанию.
Имя переменной, или идентификатор, может состоять из латинских букв, цифр и символа подчеркивания. Прописные и строчные буквы в именах различаются. Язык С++ не ограничивает длину идентификатора, однако пользоваться слишком длинными именами типа gosh_this_is_an_impossibly_name_to_type
неудобно.
Некоторые слова являются ключевыми в С++ и не могут быть использованы в качестве идентификаторов; в таблице 3.1 приведен их полный список.
Таблица 3.1. Ключевые слова C++
| asm | auto | bool | break | case | |||||
| catch | char | class | const | const_cast | |||||
| continue | default | delete | do | double | |||||
| dynamic_cast | else | enum | explicit | export | |||||
| extern | false | float | for | friend | |||||
| goto | if | inline | int | long | |||||
| mutable | namespace | new | operator | private | |||||
| protected | public | register | reinterpret_cast | return | |||||
| short | signed | sizeof | static | static_cast | |||||
| struct | switch | template | this | throw | |||||
| true | try | typedef | typeid | typename | |||||
| union | unsigned | using | virtual | void | |||||
| volatile | wchar_t | while |
Чтобы текст вашей программы был более понятным, мы рекомендуем придерживаться общепринятых соглашений об именах объектов:
· имя переменной обычно пишется строчными буквами, например index
(для сравнения: Index – это имя типа, а INDEX – константа, определенная с помощью директивы препроцессора #define);
· идентификатор должен нести какой-либо смысл, поясняя назначение объекта в программе, например: birth_date или salary;
если такое имя состоит из нескольких слов, как, например, birth_date, то принято либо разделять слова символом подчеркивания (birth_date), либо писать каждое следующее слово с большой буквы (birthDate). Замечено, что программисты, привыкшие к ОбъектноОриентированномуПодходу предпочитают выделять слова заглавными буквами, в то время как те_кто_много_писал_на_С используют символ подчеркивания. Какой из двух способов лучше – вопрос вкуса.
Что произойдет, если в объявлении _name
заменить C-строку на тип класса string? Как это повлияет на почленную инициализацию по умолчанию? Как надо будет изменить явный копирующий конструктор? Мы ответим на эти вопросы в данном подразделе.
При почленной инициализации по умолчанию исследуется каждый член. Если он принадлежит к встроенному или составному типу, то такая инициализация применяется непосредственно. Например, в первоначальном определении класса Account
член _name
инициализируется непосредственно, так как это указатель:
newAcct._name = oldAcct._name;
Члены, являющиеся объектами классов, обрабатываются по-другому. В инструкции
Account newAcct( oldAcct );
оба объекта распознаются как экземпляры Account. Если у этого класса есть явный копирующий конструктор, то он и применяется для задания начального значения, в противном случае выполняется почленная инициализация по умолчанию.
Таким образом, если обнаруживается член-объект класса, то описанный выше процесс применяется рекурсивно. У класса есть явный копирующий конструктор? Если да, вызвать его для задания начального значения члена-объекта класса. Иначе применить к этому члену почленную инициализацию по умолчанию. Если все члены этого класса принадлежат к встроенным или составным типам, то каждый инициализируется непосредственно и процесс на этом завершается. Если же некоторые члены сами являются объектами классов, то алгоритм применяется к ним рекурсивно, пока не останется ничего, кроме встроенных и составных типов.
В нашем примере у класса string
есть явный копирующий конструктор, поэтому _name инициализируется с помощью его вызова. Копирующий конструктор по умолчанию для класса Account
выглядит следующим образом (хотя явно он не определен):
|
inline Account:: Account( const Account &rhs ) { _acct_nmbr = rhs._acct_nmbr; _balance = rhs._balance; // Псевдокод на C++ // иллюстрирует вызов копирующего конструктора // для члена, являющегося объектом класса _name.string::string( rhs._name ); |
// не совсем правильно... inline Account:: Account( const Account &rhs ) { _name = rhs._name; _balance = rhs._balance; _acct_nmbr = get_unique_acct_nmbr(); |
inline Account:: Account( const Account &rhs ) : _name( rhs._name ) { _balance = rhs._balance; _acct_nmbr = get_unique_acct_nmbr(); |
(a) class BinStrTreeNode { public: // ... private: string _value; int _count; BinStrTreeNode *_leftchild; BinStrTreeNode *_rightchild; |
(b) class BinStrTree { public: // ... private: BinStrTreeNode *_root; |
(c) class iMatrix { public: // ... private: int _rows; int _cols; int *_matrix; |
(d) class theBigMix { public: // ... private: BinStrTree _bst; iMatrix _im; string _name; vectorMfloat> *_pvec; |
Point global; Point foo_bar( Point arg ) { Point local = arg; Point *heap = new Point( global ); *heap = local; Point pa[ 4 ] = { local, *heap }; return *heap; |
Вспомним, что имя массива без указания индекса элемента интерпретируется как адрес первого элемента. Аналогично имя функции без следующих за ним скобок интерпретируется как указатель на функцию. Например, при вычислении выражения
lexicoCompare;
получается указатель типа
int (*)( const string &, const string & );
Применение оператора взятия адреса к имени функции также дает указатель того же типа, например lexicoCompare и &lexicoCompare. Указатель на функцию инициализируется следующим образом:
|
int (*pfi)( const string &, const string & ) = lexicoCompare; |
int (*pfi2)( const string &, const string & ) = &lexicoCompare;
Ему можно присвоить значение:
|
pfi = lexicoCompare; |
pfi2 = pfi;
Инициализация и присваивание корректны только тогда, когда список параметров и тип значения, которое возвращает функция, адресованная указателем в левой части операции присваивания, в точности соответствуют списку параметров и типу значения, возвращаемого функцией или указателем в правой части. В противном случае выдается сообщение об ошибке компиляции. Никаких неявных преобразований типов для указателей на функции не производится. Например:
|
int calc( int, int ); int (*pfi2s)( const string &, const string & ) = 0; int (*pfi2i)( int, int ) = 0; int main() { pfi2i = calc; // правильно pri2s = calc; // ошибка: несовпадение типов pfi2s = pfi2i; // ошибка: несовпадение типов return 0; |
}
Такой указатель можно инициализировать нулем или присвоить ему нулевое значение, в этом случае он не адресует функцию.
Рассмотрим следующее определение класса:
|
class Data { public: int ival; char *ptr; |
};
Чтобы безопасно пользоваться объектом класса, необходимо правильно инициализировать его члены. Однако смысл этого действия для разных классов различен. Например, может ли ival
содержать отрицательное значение или нуль? Каковы правильные начальные значения обоих членов класса? Мы не ответим на эти вопросы, не понимая абстракции, представляемой классом. Если с его помощью описываются служащие компании, то ptr, вероятно, указывает на фамилию служащего, а ival – его уникальный номер. Тогда отрицательное или нулевое значения ошибочны. Если же класс представляет текущую температуру в городе, то допустимы любые значения ival. Возможно также, что класс Data представляет строку со счетчиком ссылок: в таком случае ival
содержит текущее число ссылок на строку по адресу ptr. При такой абстракции ival
инициализируется значением 1; как только значение становится равным 0, объект класса уничтожается.
Мнемонические имена класса и обоих его членов сделали бы, конечно, его назначение более понятным для читателя программы, но не дали бы никакой дополнительной информации компилятору. Чтобы компилятор понимал наши намерения, мы должны предоставить одну или несколько перегруженных функций инициализации – конструкторов. Подходящий конструктор выбирается в зависимости от множества начальных значений, указанных при определении объекта. Например, любая из приведенных ниже инструкций представляет корректную инициализацию объекта класса Data:
|
Data dat01( "Venus and the Graces", 107925 ); Data dat02( "about" ); Data dat03( 107925 ); |
Data dat04;
Бывают ситуации (как в случае с dat04), когда нам нужен объект класса, но его начальные значения мы еще не знаем. Возможно, они станут известны позже. Однако начальное значение задать необходимо, хотя бы такое, которое показывает, что разумное начальное значение еще не присвоено. Другими словами, инициализация объекта иногда сводится к тому, чтобы показать, что он еще не
int main() { // local1.ival = 0; local1.ptr = 0 Data local1 = { 0, 0 }; // local2.ival = 1024; // local3.ptr = "Anna Livia Plurabelle" Data.local2 - { 1024, "Anna Livia Plurabelle" }; // ... |
// ошибка: ival = "Anna Livia Plurabelle"; // ptr = 1024 |
По умолчанию инициализация массива объектов, распределенного из хипа, проходит в два этапа: выделение памяти для массива, к каждому элементу которого применяется конструктор по умолчанию, если он определен, и последующее присваивание значения каждому элементу.
Чтобы свести инициализацию к одному шагу, программист должен вмешаться и поддержать следующую семантику: задать начальные значения для всех или некоторых элементов массива и гарантировать применение конструктора по умолчанию для тех элементов, начальные значения которых не заданы. Ниже приведено одно из возможных программных решений, где используется оператор размещения new:
|
#include <utility> #include <vector > #include <new> #include <cstddef> #include "Accounts.h" typedef pair<char*, double> value_pair; /* init_heap_array() * объявлена как статическая функция-член * обеспечивает выделение памяти из хипа и инициализацию * массива объектов * init_values: пары начальных значений элементов массива * elem_count: число элементов в массиве * если 0, то размером массива считается размер вектора * init_values */ Account* Account:: init_heap_array( vector<value_pair> &init_values, vector<value_pair>::size_type elem_count = 0 ) { vector<value_pair>::size_type vec_size = init_value.size(); if ( vec_size == 0 && elem_count == 0 ) return 0; // размер массива равен либо elem_count, // либо, если elem_count == 0, размеру вектора ... size_t elems = elem_count ? elem_count : vec_size(); // получить блок памяти для размещения массива char *p = new char[sizeof(Account)*elems]; // по отдельности инициализировать каждый элемент массива int offset = sizeof( Account ); for ( int ix = 0; ix < elems; ++ix ) { // смещение ix-ого элемента // если пара начальных значений задана, // передать ее конструктору; // в противном случае вызвать конструктор по умолчанию if ( ix < vec_size ) new( p+offset*ix ) Account( init_values[ix].first, init_values[ix].second ); else new( p+offset*ix ) Account; } // отлично: элементы распределены и инициализированы; // вернуть указатель на первый элемент return (Account*)p; |
for ( int ix = 0; ix < elems; ++ix ) { if ( ix < vec_size ) new( p+offset*ix ) Account( init_values[ix].first, init_values[ix].second ); else new( p+offset*ix ) Account; |
void Account:: dealloc_heap_array( Account *ps, size_t elems ) { for ( int ix = 0; ix < elems; ++ix ) ps[ix].Account::~Account(); delete [] reinterpret_cast<char*>(ps); |
class Account { public: // ... static Account* init_heap_array( vector<value_pair> &init_values, vector<value_pair>::size_type elem_count = 0 ); static void dealloc_heap_array( Account*, size_t ); // ... |
В этой главе мы детально изучим автоматическую инициализацию, присваивание и уничтожение объектов классов в программе. Для поддержки инициализации служит конструктор– определенная проектировщиком функция (возможно, перегруженная), которая автоматически применяется к каждому объекту класса перед его первым использованием. Парная по отношению к конструктору функция, деструктор, автоматически применяется к каждому объекту класса по окончании его использования и предназначена для освобождения ресурсов, захваченных либо в конструкторе класса, либо на протяжении его жизни.
По умолчанию как инициализация, так и присваивание одного объекта класса другому выполняются почленно, т.е. путем последовательного копирования всех членов. Хотя этого обычно достаточно, при некоторых обстоятельствах такая семантика оказывается неадекватной. Тогда проектировщик класса должен предоставить специальный копирующий конструктор и копирующий оператор присваивания. Самое сложное в поддержке этих функций-членов – понять, что они должны быть написаны.
Мельчайшей независимой частью С++ программы является инструкция. Она соответствует предложению естественного языка, но завершается точкой с запятой (;), а не точкой. Выражение С++ (например, ival + 5) становится простой инструкцией, если после него поставить точку с запятой. Составная инструкция– это последовательность простых, заключенная в фигурные скобки. По умолчанию инструкции выполняются в порядке записи. Как правило, последовательного выполнения недостаточно для решения реальных задач. Специальные управляющие конструкции позволяют менять порядок действий в зависимости от некоторых условий и повторять составную инструкцию определенное количество раз. Инструкции if, if-else и switch
обеспечивают условное выполнение. Повторение обеспечивается инструкциями цикла while, do-while и for.
В С++ определение объекта, например
int ival;
рассматривается как инструкция объявления (хотя в данном случае более правильно было бы сказать определения). Ее можно использовать в любом месте программы, где разрешено употреблять инструкции. В следующем примере объявления помечены комментарием //#n, где n– порядковый номер.
|
#include <fstream> #include <string> #include <vector> int main() { string fileName; // #1 cout << "Введите имя файла: "; cin >> fileName; if ( fileName.empty() ) { // странный случай cerr << "Пустое имя файла. Завершение работы.\n"; return -1; } ifstream inFile( fileName.c_str() ); // #2 if ( ! inFile ) { cerr << "Невозможно открыть файл.\n"; return -2; } string inBuf; // #3 vector< string > text; // #4 while ( inFile >> inBuf ) { for ( int ix = 0; ix < inBuf .size(); ++ix ) // #5 // можно обойтись без ch, // но мы использовали его для иллюстрации if (( char ch = inBuf[ix] )=='.'){ // #6 ch = '_'; inBuf[ix] = ch; } text.push_back( inBuf ); } if ( text.empty() ) return 0; // одна инструкция объявления, // определяющая сразу два объекта vector<string>::iterator iter = text.begin(), // #7 iend = text.end(); while ( iter != -iend ) { cout << *iter << '\n'; ++iter; } return 0; |
}
Программа содержит семь инструкций объявления и восемь определений объектов. Объявления действуют локально; переменная объявляется непосредственно перед первым использованием объекта.
В 70-е годы философия программирования уделяла особое внимание тому, чтобы определения всех объектов находились в начале программы или блока, перед исполняемыми инструкциями. (В С, например, определение переменной не является инструкцией и обязано располагаться в начале блока.) В некотором смысле это была реакция на идиому использования переменных без предварительного объявления, чреватую ошибками. Такую идиому поддерживал, например, FORTRAN.
// одна инструкция объявления, // определяющая сразу два объекта vector<string>::iterator iter = text.begin(), |
vector<string>::iterator iter = text.begin(); |
// то ли хотел определить программист? |
string *ptr1; |
int aCnt=0, eCnt=0, iCnt=0, oCnt=0, uCnt=0; |
Инструкция break
останавливает циклы for, while, do while и блока switch. Выполнение программы продолжается с инструкции, следующей за закрывающей фигурной скобкой цикла или блока. Например, данная функция ищет в массиве целых чисел определенное значение. Если это значение найдено, функция сообщает его индекс, в противном случае она возвращает -1. Вот как выглядит реализация функции:
|
// возвращается индекс элемента или -1 int search( int *ia, int size, int value ) { // проверка что ia != 0 и size > 0 ... int loc = -1; for ( int ix = 0; ix < size; ++ix ) { if ( value == ia[ ix ] ) { // нашли! // запомним индекс и выйдем из цикла 1oc = ix; break; } } // конец цикла // сюда попадаем по break ... return 1oc; |
}
В этом примере break
прекращает выполнение цикла for и передает управление инструкции, следующей за этим циклом, – в нашем случае return. Заметим, что break
выводит из блока, относящегося к инструкции for, а не if, хотя является частью составной инструкции, соответствующей if. Использование break
внутри блока if, не входящего в цикл или в switch, является синтаксической ошибкой:
|
// ошибка: неверное использование break if ( ptr ) { if ( *ptr == "quit" ) break; // ... |
}
Если эта инструкция используется внутри вложенных циклов или инструкций switch, она завершает выполнение того внутреннего блока, в котором находится. Цикл или switch, включающий тот цикл или switch, из которого мы вышли с помощью break, продолжает выполняться. Например:
|
white ( cin >> inBuf ) { switch( inBuf[ 0 ] ) { case '-': for ( int ix = 1; ix < inBuf.size(); ++ix ) { if ( inBuf[ ix ] == ' ' ) break; // #1 // ... // ... } break; // #2 case '+': // ... } |
}
Инструкция break, помеченная // #1, завершает выполнение цикла for внутри ветви case '-'
блока switch, но не сам switch. Аналогично break // #2
завершает выполнение блока switch, но не цикла while, в который тот входит.
Как мы видели, выполнение программы часто состоит в повторении последовательности инструкций – до тех пор, пока некоторое условие остается истинным. Например, мы читаем и обрабатываем записи файла, пока не дойдем до его конца, перебираем элементы массива, пока индекс не станет равным размерности массива минус 1, и т.д. В С++ предусмотрено три инструкции для организации циклов, в частности for и while, которые начинаются проверкой условия. Такая проверка означает, что цикл может закончиться без выполнения связанной с ним простой или составной инструкции. Третий тип цикла, do while, гарантирует, что тело будет выполнено как минимум один раз: условие цикла проверяется по его завершении. (В этом разделе мы детально рассмотрим цикл for; в разделе 5.6 разберем while, а в разделе 5.7 – do while.)
Цикл for
обычно используется для обработки структур данных, имеющих фиксированную длину, таких, как массив или вектор:
|
#include <vector> int main() { int ia[ 10 ]; for ( int ix = 0; ix < 10; ++-ix ) ia[ ix ] = ix; vector<int> ivec( ia, ia+10 ); vector<int>::iterator iter = ivec.begin() ; for ( ; iter != ivec.end(); ++iter ) *iter *= 2; return 0; |
}
Синтаксис цикла for
следующий:
|
for (инструкция-инициализации; условие; выражение ) |
инструкция
инструкция-инициализации
может быть либо выражением, либо инструкцией объявления. Обычно она используется для инициализации переменной значением, которое увеличивается в ходе выполнения цикла. Если такая инициализация не нужна или выполняется где-то в другом месте, эту инструкцию можно заменить пустой (см. второй из приведенных ниже примеров). Вот примеры правильного использования инструкции-инициализации:
|
// index и iter определены в другом месте for ( index =0; ... for ( ; /* пустая инструкция */ ... for ( iter = ivec.begin(); ... for ( int 1o = 0,hi = max; ... |
for ( char *ptr = getStr(); ...
условие служит для управления циклом. Пока условие при вычислении дает true, инструкция
(... index < arraySize; ... ) (... iter != ivec.end(); ... ) (... *stl++ = *st2++; ... ) |
( ... ...; ++-index ) ( ... ...; ptr = ptr->next ) ( ... ...; ++i, --j, ++cnt ) |
const int sz = 24; int ia[ sz ]; vector<int> ivec( sz ); for ( int ix = 0; ix < sz; ++ix ) { ivec[ ix ] = ix; ia[ ix ]= ix; |
for ( int ival = 0, *pi = &ia, &ri = val; ival < size; ++iva1, ++pi, ++ri ) |
#include <iostream> int main() { for ( int ix = 0; bool done = ix == 10; ++ix ) cout << "ix: " << ix << endl; |
int main() { string word; vector< string > text; // ... for ( vector< string >::iterator iter = text.begin(), iter_end = text.end(); iter != text.end(); ++iter ) { if ( *iter == word ) break; // ... } // ошибка: iter и iter_end невидимы if ( iter != iter_end ) |
(a) for ( int *ptr = &ia, ix = 0; ix < size && ptr != ia+size; ++ix, ++ptr ) // ... |
(b) for ( ; ; ) { if ( some_condition ) break; // ... |
(c) for ( int ix = 0; ix < sz; ++ix ) // ... if ( ix != sz ) // ... (d) int ix; for ( ix < sz; ++ix ) // ... (e) for ( int ix = 0; ix < sz; ++ix, ++ sz ) |
bool is_equa1( const vector<int> &vl, |
Инструкция continue
завершает текущую итерацию цикла и передает управление на вычисление условия, после чего цикл может продолжиться. В отличие от инструкции break, завершающей выполнение всего цикла, инструкция continue
завершает выполнение только текущей итерации. Например, следующий фрагмент программы читает из входного потока по одному слову. Если слово начинается с символа подчеркивания, оно обрабатывается, в противном случае программа переходит к новому слову.
|
while ( cin >> inBuf ) { if ( inBuf[0] '= '_' ) continue; // завершение итерации // обработка слова ... |
}
Инструкция continue
может быть использована только внутри цикла.
Представим, что нам надо написать программу, переводящую мили в километры. Структура программы выглядит так:
|
int val; bool more = true; // фиктивное значение, нужное для // начала цикла while ( more ) { val = getValue(); val = convertValue(val); printValue(val); more = doMore(); |
}
Проблема заключается в том, что условие вычисляется в теле цикла. for и while требуют, чтобы значение условия равнялось true до первого вхождения в цикл, иначе тело не выполнится ни разу. Это означает, что мы должны обеспечить такое условие до начала работы цикла. Альтернативой может служить использование do while, гарантирующего выполнение тела цикла хотя бы один раз. Синтаксис цикла do while
таков:
|
do инструкция |
while ( условие );
инструкция
выполняется до первой проверки условия. Если вычисление условия
дает false, цикл останавливается. Вот как выглядит предыдущий пример с использованием цикла do while:
|
do { val = getValue(); val = convertValue(val); printValue(val); |
} while doMore();
В отличие от остальных инструкций циклов, do while не разрешает объявлять объекты в своей части условия. Мы не можем написать:
|
// ошибка: объявление переменной // в условии не разрешается do { // ... mumble( foo ); |
} while ( int foo = get_foo() ) // ошибка
потому что до условной части инструкции do while мы дойдем только после первого выполнения тела цикла.
Упражнение 5.14
Какие ошибки допущены в следующих циклах do while:
(a)
|
do string rsp; int vail, va12; cout << "Введите два числа: "; c-in >> vail >> va12; cout << "Сумма " << vail << " и " << va12 << " = " << vail + va12 << "\n\n" << "Продолжить? [да][нет] "; cin >> rsp; while ( rsp[0] != 'n' ); (b) do { // ... } while ( int iva1 = get_response() ); (c) do { int ival = get_response(); if ( iva1 == some_value() ) break; } while ( iva1 ); if ( !iva1 ) |
// ...
Упражнение 5.15
Напишите небольшую программу, которая запрашивает у пользователя две строки и печатает результат лексикографического сравнения этих строк (строка считается меньшей, если идет раньше при сортировке по алфавиту). Пусть она повторяет эти действия, пока пользователь не даст команду закончить. Используйте тип string, сравнение строк и цикл do while.
Инструкция goto
обеспечивает безусловный переход к другой инструкции внутри той же функции, поэтому современная практика программирования выступает против ее применения.
Синтаксис goto
следующий:
goto метка;
где метка– определенный пользователем идентификатор. Метка ставится перед инструкцией, на которую можно перейти с помощью goto, и должна заканчиваться двоеточием. Нельзя ставить метку непосредственно перед закрывающей фигурной скобкой. Если же это необходимо, их следует разделить пустой инструкцией:
|
end: ; // пустая инструкция |
}
Переход через инструкцию объявления в том же блоке с помощью goto
невозможен. Например, данная функция вызывает ошибку компиляции:
|
int oops_in_error() { // mumble ... goto end; // ошибка: переход через объявление int ix = 10; // ... код, использующий ix end: ; |
}
Правильная реализация функции помещает объявление ix и использующие его инструкции во вложенный блок:
|
int oops_in_error() { // mumble ... goto end; { // правильно: объявление во вложенном блоке int ix = 10; // ... код, использующий ix } end: ; |
}
Причина такого ограничения та же, что и для объявлений внутри блока switch: компилятор должен гарантировать, что для объявленного объекта конструктор и деструктор либо выполняются вместе, либо ни один из них не выполняется. Это и достигается заключением объявления во вложенный блок.
Переход назад через объявление, однако, не считается ошибкой. Почему? Перепрыгнуть через инициализацию объекта нельзя, но проинициализировать один и тот же объект несколько раз вполне допустимо, хотя это может привести к снижению эффективности. Например:
|
// переход назад через объявление не считается ошибкой. void mumble ( int max_size ) { begin: int sz = get_size(); if ( sz <= 0 ) { // выдать предупреждение ... goto end; } else if ( sz > max_size ) // получить новое значение sz goto begin; { // правильно: переход через целый блок int ia = new int[ sz ]; doit( ia, sz ) ; delete [] ia; } end: ; |
}
Использование инструкции goto
резко критикуется во всех современных языках программирования. Ее применение приводит к тому, что ход выполнения программы становится трудно понять и, следовательно, такую программу трудно модифицировать. В большинстве случаев goto
можно заменить на инструкции if или циклы. Если вы все-таки решили использовать goto, не перескакивайте через большой фрагмент кода, чтобы можно было легко найти начало и конец вашего перехода.
Инструкция if
обеспечивает выполнение или пропуск инструкции или блока в зависимости от условия. Ее синтаксис таков:
|
if ( условие ) |
инструкция
условие
заключается в круглые скобки. Оно может быть выражением, как в этом примере:
if(a+b>c) { ... }
или инструкцией объявления с инициализацией:
if ( int ival = compute_value() ){...}
Область видимости объекта, объявленного в условной части, ограничивается ассоциированной с if
инструкцией или блоком. Например, такой код вызывает ошибку компиляции:
|
if ( int ival = compute_value() ) { // область видимости ival // ограничена этим блоком } // ошибка: ival невидим |
if ( ! ival ) ...
Попробуем для иллюстрации применения инструкции if реализовать функцию min(), возвращающую наименьший элемент вектора. Заодно наша функция будет подсчитывать число элементов, равных минимуму. Для каждого элемента вектора мы должны проделать следующее:
1. Сравнить элемент с текущим значением минимума.
2. Если элемент меньше, присвоить текущему минимуму значение элемента и сбросить счетчик в 1.
3. Если элемент равен текущему минимуму, увеличить счетчик на 1.
4. В противном случае ничего не делать.
5. После проверки последнего элемента вернуть значение минимума и счетчика.
Необходимо использовать две инструкции if:
|
if ( minVal > ivec[ i ] )...// новое значение minVal |
if ( minVal == ivec[ i ] )...// одинаковые значения
Довольно часто программист забывает использовать фигурные скобки, если нужно выполнить несколько инструкций в зависимости от условия:
|
if ( minVal > ivec[ i ] ) minVal = ivec[ i ]; |
occurs = 1; // не относится к if!
Такую ошибку трудно увидеть, поскольку отступы в записи подразумевают, что и minVal=ivec[i], и occurs=1
входят в одну инструкцию if. На самом же деле инструкция
occurs = 1;
не является частью if и выполняется безусловно, всегда сбрасывая occurs в 1. Вот как должна быть составлена правильная if-инструкция (точное положение открывающей фигурной скобки является поводом для бесконечных споров):
if ( minVal > ivec[ i ] ) { minVal = ivec[ i ]; occurs = 1; |
if ( minVal == ivec [ i ] ) |
if ( minVal > ivec[ i ] ) { minVal = ivec[ i ]; occurs = 1; } // если minVal только что получила новое значение, // то occurs будет на единицу больше, чем нужно if ( minVal == ivec[ i ] ) |
if ( условие ) инструкция1 else |
if ( minVal == ivec[ i ] ) ++occurs; else if ( minVal > ivec[ i ] ) { minVal = ivec[ i ]; occurs = 1; |
if ( minVal < ivec[ i ] ) {} // пустая инструкция else if ( minVal > ivec[ i ] ) { minVal = ivec[ i ]; occurs = 1; } else // minVal == ivec[ i ] |
if ( minVal <= ivec[ i ] ) if ( minVal == ivec[ i ] ) ++occurs; else { minVal = ivec[ i ]; occurs = 1; |
if ( minVal <= ivec[ i ] ) { if ( minVal == ivec[ i ] ) ++occurs; else { minVal = ivec[ i ]; occurs = 1; } |
if ( minVal <= ivec[ i ] ) { if ( minVal == ivec[ i ] ) ++occurs; } else { minVal = ivec[ i ]; occurs = 1; |
#include <vector> int min( const vector<int> &ivec, int &occurs ) { int minVal = 0; occurs = 0; int size = ivec.size(); for ( int ix = 0; ix < size; ++ix ) { if ( minVal == ivec[ ix ] ) ++occurs; else if ( minVal > ivec[ ix ] ) { minVal = ivec[ ix ]; occurs = 1; } } return minVal; |
int main() { int occur_cnt = 0; vector< int > ivec; // occur_cnt получает значение occurs // из функции min() int minval = min( ivec, occur_cnt ); // ... |
// альтернативная реализация // с помощью пары #include <uti1ity> #include <vector> typedef pair<int,int> min_va1_pair; min_va1_pair min( const vector<int> &ivec ) { int minVal = 0; int occurs = 0; // то же самое ... return make_pair( minVal, occurs ); |
// исправленная версия min() // оставляющая возможность для оптимизации ... int minVal = ivec[0]; occurs = 0; int size = ivec.size(); for ( int ix = 0; ix < size; ++ix ) { if ( minVal == ivec[ ix ] ) ++occurs; |
// оптимизированная версия min(), // к сожалению, содержащая ошибку... int minVal = ivec[0]; occurs = 0; int size = ivec.size(); for ( int ix = 1; ix < size; ++ix ) { if ( minVal == ivec[ ix ] ) ++occurs; |
int minVal = ivec[0]; |
#include <iostream> #include <vector> int min( const vector< int > &ivec, int &occurs ) { int minVal = ivec[ 0 ]; occurs = 1; int size = ivec.size(); for ( int ix = 1; ix < size; ++ix ) { if ( minVal == ivec[ ix ] ) ++occurs; else if ( minVal > ivec[ ix ] ){ minVal = ivec[ ix ]; occurs = 1; } } return minVal; } int main() { int ia[] = { 9,1,7,1,4,8,1,3,7,2,6,1,5,1 }; vector<int> ivec( ia, ia+14 ); int occurs = 0; int minVal = min( ivec, occurs ); cout << "Минимальное значение: " << minVal << " встречается: " << occurs << " раз.\n"; return 0; |
template <class valueType> inline const valueType& min( valueType &vall, valueType &va12 ) { if ( vall < va12 ) return vall; return va12; |
template <class valueType> inline const valueType& min( valueType &vall, valueType &va12 ) { return ( vall < va12 ) ? vall : va12; |
if ( ch == 'a' || ch == 'A' ) ++aCnt; else if ( ch == 'e' || ch == 'E' ) ++eCnt; else if ( ch == 'i' || ch == 'I' ) ++iCnt; else if ( ch == 'o' || ch == '0' ) ++oCnt; else if ( ch == 'u' || ch == 'U' ) |
(a) if ( ivall != iva12 ) ivall = iva12 else ivall = iva12 = 0; (b) if ( ivat < minval ) minvat = ival; occurs = 1; (c) if ( int ival = get_value()) cout << "ival = " << ival << endl; if ( ! ival ) cout << "ival = 0\n"; (d) if ( ival = 0 ) ival = get_value(); (e) if ( iva1 == 0 ) |
Длинные цепочки инструкций if-else, наподобие приведенной в конце предыдущего раздела, трудны для восприятия и потому являются потенциальным источником ошибок. Модифицируя такой код, легко сопоставить, например, разные else и if. Альтернативный метод выбора одного их взаимоисключающих условий предлагает инструкция switch.
Для иллюстрации инструкции switch
рассмотрим следующую задачу. Нам надо подсчитать, сколько раз встречается каждая из гласных букв в указанном отрывке текста. (Общеизвестно, что буква e– наиболее часто встречающаяся гласная в английском языке.) Вот алгоритм программы:
1. Считывать по одному символу из входного потока, пока они не кончатся.
2. Сравнить каждый символ с набором гласных.
3. Если символ равен одной из гласных, прибавить 1 к ее счетчику.
4. Напечатать результат.
Написанная программа была запущена, в качестве контрольного текста использовался раздел из оригинала данной книги. Результаты подтвердили, что буква e действительно самая частая:
aCnt: 394
eCnt: 721
iCnt: 461
oCnt: 349
uCnt: 186
Инструкция switch
состоит из следующих частей:
· ключевого слова switch, за которым в круглых скобках идет выражение, являющееся условием:
|
char ch; while ( cm >> ch ) |
switch( ch )
· набора меток case, состоящих из ключевого слова case и константного выражения, с которым сравнивается условие. В данном случае каждая метка представляет одну из гласных латинского алфавита:
|
case 'a': case 'e': case 'i': case 'o': |
case 'u':
· последовательности инструкций, соотносимых с метками case. В нашем примере с каждой меткой будет сопоставлена инструкция, увеличивающая значение соответствующего счетчика;
default: // любой символ, не являющийся гласной |
// неверные значения меток case 3.14: // не целое |
#include <iostream> int main() { char ch; int aCnt=0, eCnt=0, iCnt=0, oCnt=0, uCnt=0; while ( cin >> ch ) // Внимание! неверная реализация! switch ( ch ) { case 'a': ++aCnt; case 'e': ++eCnt; case 'i': ++iCnt; case 'o': ++oCnt; case 'u': ++uCnt; } cout << "Встретилась a: \t" << aCnt << '\n' << "Встретилась e: \t" << eCnt << '\n' << "Встретилась i: \t" << iCnt << '\n' << "Встретилась o: \t" << oCnt << '\n' << "Встретилась u: \t" << uCnt << '\n'; |
switch ( ch ) { case 'a': ++aCnt; break; case 'e': ++eCnt; break; case 'i': ++iCnt; break; case 'o': ++oCnt; break; case 'u': ++uCnt; break; |
int vowelCnt = 0; // ... switch ( ch ) { // любой из символов a,e,1,o,u // увеличит значение vowelCnt case 'a': case 'e': case 'i': case 'o': case 'u': ++vowe1Cnt; break; |
switch ( ch ) { // допустимый синтаксис case 'a': case 'e': case 'i': case 'o': case 'u': ++vowe1Cnt; break; |
switch ( ch ) { case 'a': case 'A': ++aCnt; break; case 'e': case 'E': ++eCnt; break; case 'i': case 'I': ++iCnt; break; case 'o': case 'O': ++oCnt; break; case 'u': case 'U': ++uCnt; break; |
#include <iostream> #include <ctype.h> int main() { char ch; int aCnt=0, eCnt=0, iCnt=0, oCnt=0, uCnt=0, consonantCount=0; while ( cin >> ch ) switch ( ch ) { case 'a': case 'A': ++aCnt; break; case 'e': case 'E': ++eCnt; break; case 'i': case 'I': ++iCnt; break; case 'o': case 'O': ++oCnt; break; case 'u': case 'U': ++uCnt; break; default: if ( isa1pha( ch ) ) ++consonantCnt; break; } cout << "Встретилась a: \t" << aCnt << '\n' << "Встретилась e: \t" << eCnt << '\n' << "Встретилась i: \t" << iCnt << '\n' << "Встретилась o: \t" << oCnt << '\n' << "Встретилась u: \t" << uCnt << '\n' << "Встретилось согласных: \t" << consonantCnt << '\n'; |
case illegal_definition: // ошибка: объявление не может // употребляться в этом месте string file_name = get_file_name(); // ... |
case ok: { // ок string file_name = get_file_name(); // ... |
switch ( ival ) { case 'a': aCnt++; case 'e': eCnt++; default: iouCnt++; |
switch ( ival ) { case 1: int ix = get_value(); ivec[ ix ] = ival; break; default: ix = ivec.sizeQ-1; ivec[ ix ] = ival; |
switch ( ival ) { case 1, 3, 5, 7, 9: oddcnt++; break; case 2, 4, 6, 8, 10: evencnt++; break; |
int iva1=512 jva1=1024, kva1=4096; int bufsize; // ... switch( swt ) { case ival: bufsize = ival * sizeof( int ); break; case jval: bufsize = jval * sizeof( int ); break; case kval: bufsize = kval * sizeof( int ); break; |
enum { illustrator = 1, photoshop, photostyler = 2 }; switch ( ival ) { case illustrator: --i11us_1icense; break; case photoshop: --pshop_1icense; break; case photostyler: --psty1er_license; |
Синтаксис инструкции while
следующий:
|
while ( условие ) |
инструкция
Пока значением условия
является true, инструкция
выполняется в такой последовательности:
1.
Вычислить условие.
2. Выполнить инструкцию,
если условие
истинно.
3. Если самое первое вычисление условия
дает false, инструкция не выполняется.
Условием
может быть любое выражение:
|
bool quit = false; // ... while ( ! quit ) { // ... quit = do_something(); } string word; |
while ( cin >> word ){ ... }
или объявление с инициализацией:
|
while ( symbol *ptr = search( name )) { // что-то сделать |
}
В последнем случае ptr
видим только в блоке, соответствующем инструкции while, как это было и для инструкций for и switch.
Вот пример цикла while, обходящего множество элементов, адресуемых двумя указателями:
|
int sumit( int *parray_begin, int *parray_end ) { int sum = 0; if ( ! parray_begin || ! parray_end ) return sum; while ( parray_begin != parray_end ) // прибавить к sum // и увеличить указатель sum += *parray_begin++; return sum; } int ia[6] = { 0, 1, 2, 3, 4, 5 }; int main() { int sum = sumit( &ia[0], &ia[ 6 ] ); // ... |
}
Для того чтобы функция sumit()
выполнялась правильно, оба указателя должны адресовать элементы одного и того же массива (parray_end
может указывать на элемент, следующий за последним). В противном случае sumit() будет возвращать бессмысленную величину. Увы, С++ не гарантирует, что два указателя адресуют один и тот же массив. Как мы увидим в главе 12, стандартные универсальные алгоритмы реализованы подобным же образом, они принимают параметрами указатели на первый и последний элементы массива.
Упражнение 5.11
Какие ошибки допущены в следующих циклах while:
|
(a) string bufString, word; while ( cin >> bufString >> word ) |
(b) while ( vector<int>::iterator iter != ivec.end() ) // ... (c) while ( ptr = 0 ) ptr = find_a_value(); (d) while ( bool status = find( word )) { word = get_next_word(); if ( word.empty() ) break; // ... } if ( ! status ) |
Обработка исключений – это стандартное языковое средство для реакции на аномальное поведение программы во время выполнения. C++ поддерживает единообразный синтаксис и стиль обработки исключений, а также способы тонкой настройки этого механизма в специальных ситуациях. Основы его поддержки в языке C++ описаны в главе 11, где показано, как программа может возбудить исключение, передать управление его обработчику (если таковой существует) и как обработчики исключений ассоциируются с try-блоками.
Возможности механизма обработки исключений становятся больше, если в качестве исключений использовать иерархии классов. В этом разделе мы расскажем, как писать программы, которые умеют возбуждать и обрабатывать исключения, принадлежащие таким иерархиям.
С обработкой исключений в программах C++ связано несколько вопросов. Хотя поддержка такой обработки встроена в язык, не стоит использовать ее везде. Обычно она применяется для обмена информацией об ошибках между независимо разработанными частями программы. Например, автор некоторой библиотеки может с помощью исключений сообщать пользователям об ошибках. Если библиотечная функция обнаруживает аномальную ситуацию, которую не способна обработать самостоятельно, она может возбудить исключение для уведомления вызывающей программы.
В нашем примере в библиотеке определен класс iStack и его функции-члены. Разумно предположить, что программист, кодировавший main(), где используется эта библиотека, не разрабатывал ее. Функции-члены класса iStack могут обнаружить, что операция pop()
вызвана, когда стек пуст, или что операция push() вызвана, когда стек полон; однако разработчик библиотеки ничего не знал о программе, пользующейся его функциями, так что не мог разрешить проблему локально. Не сумев обработать ошибку внутри функций-членов, мы решили возбуждать исключения, чтобы известить вызывающую программу.
Хотя C++ поддерживает исключения, следует применять и другие методы обработки ошибок (например, возврат кода ошибки) – там, где это более уместно. Однозначного ответа на вопрос: “Когда ошибку следует трактовать как исключение?” не существует. Ответственность за решение о том, что считать исключительной ситуацией, возлагается на разработчика. Исключения – это часть интерфейса библиотеки, и решение о том, какие исключения она возбуждает, – важный аспект ее дизайна. Если библиотека предназначена для использования в программах, которые не должны аварийно завершаться ни при каких обстоятельствах, то она обязана разбираться с аномалиями сама либо извещать о них вызывающую программу, передавая ей управление. Решение о том, какие ошибки следует обрабатывать как исключения, – трудная часть работы по проектированию библиотеки.
В нашем примере с классом iStack
вопрос, должна ли функция push() возбуждать исключение, если стек полон, является спорным. Альтернативная и, по мнению многих, лучшая реализация push() – локальное решение проблемы: увеличение размера стека при его заполнении. В конце концов, единственное ограничение – это объем доступной программе памяти. Наше решение о возбуждении исключения при попытке поместить значение в полный стек, по-видимому, непродуманно. Можно переделать функцию-член push(), чтобы она в такой ситуации наращивала стек:
void iStack::push( int value ) { // если стек полон, увеличить размер вектора if ( full() ) _stack.resize( 2 * _stack.size() ); _stack[ _top++ ] = value; |
В главе 11 мы использовали два типа класса для описания исключений, возбуждаемых функциями-членами нашего класса iStack:
|
class popOnEmpty { ... }; |
class pushOnFull { ... };
В реальных программах на C++ типы классов, представляющих исключения, чаще всего организуются в группы, или иерархии. Как могла бы выглядеть вся иерархия для этих классов?
Мы можем определить базовый класс Excp, которому наследуют оба наши класса исключений. Он инкапсулирует данные и функции-члены, общие для обоих производных:
|
class Excp { ... }; class popOnEmpty : public Excp { ... }; |
class pushOnFull : public Excp { ... };
Одной из операцией, которые предоставляет базовый класс, является вывод сообщения об ошибке. Эта возможность используется обоими классами, стоящими ниже в иерархии:
|
class Excp { public: // напечатать сообщение об ошибке static void print( string msg ) { cerr << msg << endl; } |
};
Иерархию классов исключений разрешается развивать и дальше. От Excp
можно произвести другие классы для более точного описания исключений, обнаруживаемых программой:
|
class Excp { ... }; class stackExcp : public Excp { ... }; class popOnEmpty : public stackExcp { ... }; |
class pushOnFull : public stackExcp { ... };
|
class mathExcp : public Excp ( ... }; class zeroOp : public mathExcp { ... }; |
class divideByZero : public mathExcp { ... };
Последующие уточнения позволяют более детально идентифицировать аномальные ситуации в работе программы. Дополнительные классы исключений организуются как слои. По мере углубления иерархии каждый новый слой описывает все более специфичные исключения. Например, первый, самый общий слой в приведенной выше иерархии представлен классом Excp. Второй специализирует Excp, выделяя из него два подкласса: stackExcp (для исключений при работе с нашим iStack) и mathExcp
(для исключений, возбуждаемых функциями из математической библиотеки). Третий, самый специализированный слой данной иерархии уточняет классы исключений: popOnEmpty и pushOnFull
определяют два вида исключений работы со стеком, а ZeroOp и divideByZero – два вида исключений математических операций.
В последующих разделах мы рассмотрим, как возбуждаются и обрабатываются исключения, представленные классами в нашей иерархии.
Использование квалифицированных имен при каждом обращении к членам пространств может стать обременительным, особенно если имена пространств достаточно длинны. Если бы удалось сделать их короче, то такие имена проще было бы читать и набивать. Однако употребление коротких имен увеличивает риск их совпадения с другими, поэтому желательно, чтобы в библиотеках применялись пространства с длинными именами.
К счастью, существуют механизмы, облегчающие использование членов пространств имен в программах. Псевдонимы пространства имен, using-объявления и using-директивы помогают преодолеть неудобства работы с очень длинными именами.
Исключениями называют аномальные ситуации, возникающие во время исполнения программы: невозможность открыть нужный файл или получить необходимое количество памяти, использование выходящего за границы индекса для какого-либо массива. Обработка такого рода исключений, как правило, плохо интегрируется в основной алгоритм программы, и программисты вынуждены изобретать разные способы корректной обработки исключения, стараясь в то же время не слишком усложнить программу добавлением всевозможных проверок и дополнительных ветвей алгоритма.
С++ предоставляет стандартный способ реакции на исключения. Благодаря вынесению в отдельную часть программы кода, ответственного за проверку и обработку ошибок, значительно облегчается восприятие текста программы и сокращается ее размер. Единый синтаксис и стиль обработки исключений можно, тем не менее, приспособить к самым разнообразным нуждам и запросам.
Механизм исключений делится на две основные части:
точка программы, в которой произошло исключение. Определение того факта, что при выполнении возникла какая-либо ошибка, влечет за собой возбуждение
исключения. Для этого в С++ предусмотрен специальный оператор throw. Возбуждение исключения в случае невозможности открыть некоторый файл выглядит следующим образом:
|
if ( !infile ) { string errMsg("Невозможно открыть файл: "); errMsg += fileName; throw errMsg; |
}
Место программы, в котором исключение обрабатывается. При возбуждении исключения нормальное выполнение программы приостанавливается и управление передается обработчику исключения. Поиск нужного обработчика часто включает в себя раскрутку так называемого стека вызовов программы. После обработки исключения выполнение программы возобновляется, но не с того места, где произошло исключение, а с точки, следующей за обработчиком. Для определения обработчика исключения в С++ используется ключевое слово catch. Вот как может выглядеть обработчик для примера из предыдущего абзаца:
catch (string exceptionMsg) {
int* stats (const int *ia, int size) |
catch (string exceptionMsg) { // код обработчика |
catch (string exceptionMsg) { |
catch (...) { // обрабатывает любое исключение, // однако ему недоступен объект, переданный // в обработчик в инструкции throw |
Допустим, мы задумали написать книжку для детей и хотим понять, какой словарный состав наиболее подходит для такой цели. Чтобы ответить на этот вопрос, нужно прочитать несколько детских книг, сохранить текст в отдельных векторах строк (см. раздел 6.7) и подвергнуть его следующей обработке:
1.
Создать копию каждого вектора.
2. Слить все векторы в один.
3. Отсортировать его в алфавитном порядке.
4. Удалить все дубликаты.
5. Снова отсортировать, но уже по длине слов.
6. Подсчитать число слов, длина которых больше шести знаков (предполагается, что длина – это некоторая мера сложности, по крайней мере, в терминах словаря).
7. Удалить семантически нейтральные слова (например, союзы and (и), if (если), or (или), but (но) и т.д.).
8. Напечатать получившийся вектор.
На первый взгляд, задача на целую главу. Но с помощью обобщенных алгоритмов мы решим ее в рамках одного подраздела.
Аргументом нашей функции является вектор из векторов строк. Мы принимаем указатель на него, проверяя, не является ли он нулевым:
|
#include <vector> #include <string> typedef vector<string, allocator> textwords; void process_vocab( vector<textwords, allocator> *pvec ) { if ( ! pvec ) { // выдать предупредительное сообщение return; } // ... |
}
Нужно создать один вектор, включающий все элементы исходных векторов. Это делается с помощью обобщенного алгоритма copy()
(для его использования необходимо включить заголовочные файлы algorithm и iterator):
|
#include <algorithm> #include <iterator> void process_vocab( vector<textwords, allocator> *pvec ) { // ... vector< string > texts; vector<textwords, allocator>::iterator iter = pvec->begin(); for ( ; iter != pvec->end(); ++iter ) copy( (*iter).begin(), (*iter).end(), back_inserter( texts )); // ... |
void process_vocab( vector<textwords, allocator> *pvec ) { // ... // отсортировать вектор texts sort( texts.begin(), texts.end() ); // удалить дубликаты vector<string, allocator>::iterator it; it = unique( texts.begin(), texts.end() ); texts.erase( it, texts.end() ); // ... |
bool less_than( const string & s1, const string & s2 ) { return s1.size() < s1.size(); } void process_vocab( vector<textwords, allocator> *pvec ) { // ... // отсортировать элементы вектора texts по длине, // сохранив также прежний порядок stable_sort( texts.begin(), texts.end(), less_than ); // ... |
// объект-функция - операция реализована с помощью перегрузки // оператора operator() class LessThan { public: bool operator()( const string & s1, const string & s2 ) { return s1.size() < s2.size(); } |
string st1( "shakespeare" ); string st2( "marlowe" ); // вызывается lt.operator()( st1, st2 ); |
void process_vocab( vector<textwords, allocator> *pvec ) { // ... stable_sort( texts.begin(), texts.end(), LessThan() ); // ... |
#include <iostream> class GreaterThan { public: GreaterThan( int size = 6 ) : _size( size ){} int size() { return _size; } bool operator()( const string & s1 ) { return s1.size() > 6; } private: int _size; |
void process_vocab( vector<textwords, allocator> *pvec ) { // ... // подсчитать число строк, длина которых больше 6 int cnt = count_if( texts.begin(), texts.end(), GreaterThan() ); cout << "Number of words greater than length six are " << cnt << endl; // ... |
void process_vocab( vector<textwords, allocator> *pvec ) { // ... static string rw[] = { "and", "if", "or", "but", "the" }; vector< string > remove_words( rw, rw+5 ); vector< string >::iterator it2 = remove_words.begin(); for ( ; it2 != remove_words.end(); ++it2 ) { // просто для демонстрации другой формы count() int cnt = count( texts.begin(), texts.end(), *it2 ); cout << cnt << " instances removed: " << (*it2) << endl; texts.erase( remove(texts.begin(),texts.end(),*it2 ), texts.end() ); } // ... |
class PrintElem { public: PrintElem( int lineLen = 8 ) : _line_length( lineLen ), _cnt( 0 ) {} void operator()( const string &elem ) { ++_cnt; if ( _cnt % _line_length == 0 ) { cout << '\n'; } cout << elem << " "; } private: int _line_length; int _cnt; |
void process_vocab( vector<textwords, allocator> *pvec ) { // ... for_each( texts.begin(), texts.end(), PrintElem() ); |
#include <vector> #include <string> #include <algorithm> #include <iterator> // предшествующий принятию стандарта синтаксис <iostream> #include <iostream.h> class GreaterThan { public: GreaterThan( int size = 6 ) : _size( sz ){} int size() { return _size; } bool operator()(const string &s1) { return s1.size() > _size; } private: int _size; }; class PrintElem { public: PrintElem( int lineLen = 8 ) : _line_length( lineLen ), _cnt( 0 ) {} void operator()( const string &elem ) { ++_cnt; if ( _cnt % _line_length == 0 ) { cout << '\n'; } cout << elem << " "; } private: int _line_length; int _cnt; |
class LessThan { public: bool operator()( const string & s1, const string & s2 ) { return s1.size() < s2.size(); } }; typedef vector<string, allocator> textwords; void process_vocab( vector<textwords, allocator> *pvec ) { if ( ! pvec ) { // вывести предупредительное сообщение return; } vector< string, allocator > texts; vector<textwords, allocator>::iterator iter; for ( iter = pvec->begin() ; iter != pvec->end(); ++iter ) copy( (*iter).begin(), (*iter).end(), back_inserter( texts )); // отсортировать вектор texts sort( texts.begin(), texts.end() ); // теперь посмотрим, что получилось for_each( texts.begin(), texts.end(), PrintElem() ); cout << "\n\n"; // разделить части выведенного текста // удалить дубликаты vector<string, allocator>::iterator it; it = unique( texts.begin(), texts.end() ); texts.erase( it, texts.end() ); // посмотрим, что осталось for_each( texts.begin(), texts.end(), PrintElem() ); cout << "\n\n"; // отсортировать элементы // stable_sort сохраняет относительный порядок равных элементов stable_sort( texts.begin(), texts.end(), LessThan() ); for_each( texts.begin(), texts.end(), PrintElem() ); cout << "\n\n"; // подсчитать число строк, длина которых больше 6 int cnt = 0; // устаревшая форма count - в стандарте используется другая count_if( texts.begin(), texts.end(), GreaterThan(), cnt ); cout << "Number of words greater than length six are " << cnt << endl; static string rw[] = { "and", "if", "or", "but", "the" }; vector<string,allocator> remove_words( rw, rw+5 ); vector<string, allocator>::iterator it2 = remove_words.begin(); for ( ; it2 != remove_words.end(); ++it2 ) { int cnt = 0; // устаревшая форма count - в стандарте используется другая count( texts.begin(), texts.end(), *it2, cnt ); cout << cnt << " instances removed: " << (*it2) << endl; texts.erase( remove(texts.begin(),texts.end(),*it2), texts.end() ); } cout << "\n\n"; for_each( texts.begin(), texts.end(), PrintElem() ); } // difference_type - это тип, с помощью которого можно хранить результат // вычитания двух итераторов одного и того же контейнера // - в данном случае вектора строк ... // обычно это предполагается по умолчанию typedef vector<string,allocator>::difference_type diff_type; // предшествующий принятию стандарта синтаксис для <fstream> #include <fstream.h> main() { vector<textwords, allocator> sample; vector<string,allocator> t1, t2; string t1fn, t2fn; // запросить у пользователя имена входных файлов ... // в реальной программе надо бы выполнить проверку cout << "text file #1: "; cin >> t1fn; cout << "text file #2: "; cin >> t2fn; // открыть файлы ifstream infile1( t1fn.c_str()); ifstream infile2( t2fn.c_str()); // специальная форма итератора // обычно diff_type подразумевается по умолчанию ... istream_iterator< string, diff_type > input_set1( infile1 ), eos; istream_iterator< string, diff_type > input_set2( infile2 ); // специальная форма итератора copy( input_set1, eos, back_inserter( t1 )); copy( input_set2, eos, back_inserter( t2 )); sample.push_back( t1 ); sample.push_back( t2 ); process_vocab( &sample ); |
Предположим, что мы хотим предоставить в общее пользование наш класс Array, разработанный в предыдущих примерах. Однако не мы одни занимались этой проблемой; возможно, кем-то где-то, скажем, в одном из подразделений компании Intel был создан одноименный класс. Из-за того что имена этих классов совпадают, потенциальные пользователи не могут задействовать оба класса одновременно, они должны выбрать один из них. Эта проблема решается добавлением к имени класса некоторой строки, идентифицирующей его разработчиков, скажем,
class Cplusplus_Primer_Third_Edition_Array { ... };
Конечно, это тоже не гарантирует уникальность имени, но с большой вероятностью избавит пользователя от данной проблемы. Как, однако, неудобно пользоваться столь длинными именами!
Стандарт С++ предлагает для решения проблемы совпадения имен механизм, называемый пространством имен. Каждый производитель программного обеспечения может заключить свои классы, функции и другие объекты в свое собственное пространство имен. Вот как выглядит, например, объявление нашего класса Array:
|
namespace Cplusplus_Primer_3E { template <class elemType> class Array { ... }; |
}
Ключевое слово namespace
задает пространство имен, определяющее видимость нашего класса и названное в данном случае Cplusplus_Primer_3E. Предположим, что у нас есть классы от других разработчиков, помещенные в другие пространства имен:
namespace IBM_Canada_Laboratory {
template <class elemType> class Array { ... };
class Matrix { ... };
}
namespace Disney_Feature_Animation {
class Point { ... };
template <class elemType> class Array { ... };
}
По умолчанию в программе видны объекты, объявленные без явного указания пространства имен; они относятся к глобальному пространству имен. Для того чтобы обратиться к объекту из другого пространства, нужно использовать его квалифицированное имя, которое состоит из идентификатора пространства имен и идентификатора объекта, разделенных оператором разрешения области видимости (::). Вот как выглядят обращения к объектам приведенных выше примеров:
Cplusplus_Primer_3E::Array<string> text; |
#include <string> |
namespace Exercize { |
int main() { |
Наш класс IntArray
служит хорошей альтернативой встроенному массиву целых чисел. Но в жизни могут потребоваться массивы для самых разных типов данных. Можно предположить, что единственным отличием массива элементов типа double от нашего является тип данных в объявлениях, весь остальной код совпадает буквально.
Для решения данной проблемы в С++ введен механизм шаблонов. В объявлениях классов и функций допускается использование параметризованных типов. Типы-параметры заменяются в процессе компиляции настоящими типами, встроенными или определенными пользователем. Мы можем создать шаблон класса Array, заменив в классе IntArray тип элементов int на обобщенный тип-параметр. Позже мы конкретизируем
типы-параметры, подставляя вместо них реальные типы int, double и string. В результате появится способ использовать эти конкретизации так, как будто мы на самом деле определили три разных класса для этих трех типов данных.
Вот как может выглядеть шаблон класса Array:
|
template <class elemType> class Array { public: explicit Array( int sz = DefaultArraySize ); Array( const elemType *ar, int sz ); Array( const Array &iA ); virtual ~Array() { delete[] _ia; } Array& operator=( const Array & ); int size() const { return _size; } virtual elemType& operator[]( int ix ) { return _ia[ix]; } virtual void sort( int,int ); virtual int find( const elemType& ); virtual elemType min(); virtual elemType max(); protected: void init( const elemType*, int ); void swap( int, int ); static const int DefaultArraySize = 12; int _size; elemType *_ia; |
};
Ключевое слово template
говорит о том, что задается шаблон,
параметры которого заключаются в угловые скобки (<>). В нашем случае имеется лишь один параметр elemType; ключевое слово class
перед его именем сообщает, что этот параметр представляет собой тип.
При конкретизации класса-шаблона Array
#include <iostream> #include "Array.h" int main() { const int array_size = 4; // elemType заменяется на int Array<int> ia(array_size); // elemType заменяется на double Array<double> da(array_size); // elemType заменяется на char Array<char> ca(array_size); int ix; for ( ix = 0; ix < array_size; ++ix ) { ia[ix] = ix; da[ix] = ix * 1.75; ca[ix] = ix + 'a'; } for ( ix = 0; ix < array_size; ++ix ) cout << "[ " << ix << " ] ia: " << ia[ix] << "\tca: " << ca[ix] << "\tda: " << da[ix] << endl; return 0; |
Array<int> ia(array_size); |
// Array<double> da(array_size); |
#include <cassert> |
// функцию swap() тоже следует сделать шаблоном |
#include "Array.h" |
#include <iostream> |
template<class elemType> class Array; |
(a) Array< int*& > pri(1024); (b) Array< Array<int> > aai(1024); (c) Array< complex< double > > acd(1024); (d) Array< Status > as(1024); |
class example1 { public: example1 (double min, double max); example1 (const double *array, int size); double& operator[] (int index); bool operator== (const example1&) const; bool insert (const double*, int); bool insert (double); double min (double) const { return _min; }; double max (double) const { return _max; }; void min (double); void max (double); int count (double value) const; private: int size; double *parray; double _min; double _max; |
template <class elemType> class Example2 { |
(a) Example2<Array<int>*> ex1; (b) ex1.min (&ex1); (c) Example2<int> sa(1024),sb; (d) sa = sb; (e) Example2<string> exs("Walden"); |
В общем виде объявление потокового итератора чтения istream_iterator
имеет форму:
istream_iterator<Type> identifier( istream& );1[O.A.3].
где Type – это любой встроенный или пользовательский тип класса, для которого определен оператор ввода. Аргументом конструктора может быть объект либо класса istream, например cin, либо производного от него класса с открытым типом наследования – ifstream:
|
#include <iterator> #include <fstream> #include <string> #include <complex> // прочитать последовательность объектов типа complex // из стандартного ввода istream_iterator< complex > is_complex( cin ); // прочитать последовательность строк из именованного файла ifstream infile( "C++Primer" ); |
istream_iterator< string > is_string( infile );
При каждом применении оператора инкремента к объекту типа istream_iterator
читается следующий элемент из входного потока, для чего используется оператор operator>>(). Чтобы сделать то же самое в обобщенных алгоритмах, необходимо предоставить пару итераторов, обозначающих начальную и конечную позицию в файле. Начальную позицию дает istream_iterator, инициализированный объектом istream, – такой, скажем, как is_string. Для получения конечной позиции мы используем специальный конструктор по умолчанию класса istream_iterator:
|
// конструирует итератор end_of_stream, который будет служить маркером // конца потока в итераторной паре istream_iterator< string > end_of_stream vector<string> text; // правильно: передаем пару итераторов copy( is_string, end_of_stream, inserter( text, text.begin() )); |
Объявление потокового итератора записи ostream_iterator
может быть представлено в двух формах:
|
ostream_iterator<Type> identifier( ostream& ) |
ostream_iterator<Type> identifier( ostream&, char * delimiter )
где Type – это любой встроенный или пользовательский тип класса, для которого определен оператор вывода (operator<<). Во второй форме delimiter – это разделитель, то есть C-строка символов, которая выводится в файл после каждого элемента. Такая строка должна заканчиваться двоичным нулем, иначе поведение программы не определено (скорее всего, она аварийно завершит выполнение). В качестве аргумента ostream может выступать объект класса ostream, например cout, либо производного от него класса с открытым типом наследования, скажем ofstream:
|
#include <iterator> #include <fstream> #include <string> #include <complex> // записать последовательность объектов типа complex // в стандартный вывод, разделяя элементы пробелами ostream_iterator< complex > os_complex( cin, " " ); // записать последовательность строк в именованный файл ofstream outfile( "dictionary" ); |
ostream_iterator< string > os_string( outfile, "\n" );
Вот простой пример чтения из стандартного ввода и копирования на стандартный вывод с помощью безымянных потоковых итераторов и обобщенного алгоритма copy():
|
#include <iterator> #include <algorithm> #include <iostream> int main() { copy( istream_iterator< int >( cin ), istream_iterator< int >(), ostream_iterator< int >( cout, " " )); |
}
Ниже приведена небольшая программа, которая открывает указанный пользователем файл и копирует его на стандартный вывод, применяя для этого алгоритм copy() и потоковый итератор записи ostream_iterator:
|
#include <string> #include <algorithm> #include <fstream> #include <iterator> main() { string file_name; cout << "please enter a file to open: "; cin >> file_name; if ( file_name.empty() || !cin ) { cerr << "unable to read file name\n"; return -1; } ifstream infile( file_name.c_str()); if ( !infile ) { cerr << "unable to open " << file_name << endl; return -2; } istream_iterator< string > ins( infile ), eos; ostream_iterator< string > outs( cout, " " ); copy( ins, eos, outs ); |
}
Итератор предоставляет обобщенный способ перебора элементов любого контейнера– как последовательного, так и ассоциативного. Пусть iter
является итератором для какого-либо контейнера. Тогда
++iter;
перемещает итератор так, что он указывает на следующий элемент контейнера, а
*iter;
разыменовывает итератор, возвращая элемент, на который он указывает.
Все контейнеры имеют функции-члены begin() и end().
· begin()
возвращает итератор, указывающий на первый элемент контейнера.
· end()
возвращает итератор, указывающий на элемент, следующий за последним в контейнере.
Чтобы перебрать все элементы контейнера, нужно написать:
|
for ( iter = container. begin(); iter != container.end(); ++iter ) |
do_something_with_element( *iter );
Объявление итератора выглядит слишком сложным. Вот определение пары итераторов вектора типа string:
|
// vector<string> vec; vector<string>::iterator iter = vec.begin(); |
vector<string>::iterator iter_end = vec.end();
В классе vector для определения iterator
используется typedef. Синтаксис
vector<string>::iterator
ссылается на iterator, определенный с помощью
typedef
внутри класса vector, содержащего элементы типа string.
Для того чтобы напечатать все элементы вектора, нужно написать:
|
for( ; iter != iter_end; ++iter ) |
cout << *iter << '\n';
Здесь значением *iter
выражения является, конечно, элемент вектора.
В дополнение к типу iterator в каждом контейнере определен тип const_iterator, который необходим для навигации по контейнеру, объявленному как const. const_iterator позволяет только читать элементы контейнера:
|
#include <vector> void even_odd( const vector<int> *pvec, vector<int> *pvec_even, vector<int> *pvec_odd ) { // const_iterator необходим для навигации по pvec vector<int>::const_iterator c_iter = pvec->begin(); vector<int>::const_1terator c_iter_end = pvec->end(); for ( ; c_iter != c_iter_end; ++c_iter ) if ( *c_iter % 2 ) pvec_even->push_back( *c_iter ); else pvec_odd->push_back( *c_iter ); |
#include <vector> #include <string> #include <iostream> int main() { vector<string> svec; string intext; while ( cin >> intext ) svec.push_back( intext ); // обработать svec ... |
int main() { vector<string> svec; // ... // инициализация svec2 всеми элементами svec vector<string> svec2( svec.begin(), svec.end() ); // инициализация svec3 первой половиной svec vector<string>::iterator it = svec.begin() + svec.size()/2; vector<string> svec3 ( svec.begin(), it ); // ... |
#include <vector> #include <string> #include <iterator> int mainQ { // привязка istream_iterator к стандартному вводу istream_iterator<string> infile( cin ); // istream_iterator, отмечающий конец потока istream_iterator<string> eos; // инициализация svec элементами, считываемыми из cin; vector<string> svec( infile, eos ); // ... |
#include <string> string words[4] = { "stately", "plump", "buck", "mulligan" |
int ia[6] = { 0, 1, 2, 3, 4, 5 }; |
const vector< int > ivec; vector< string > svec; list< int > ilist; (a) vector<int>::iterator it = ivec.begin(); (b) list<int>::iterator it = ilist.begin()+2; (c) vector<string>::iterator it = &svec[0]; (d) for ( vector<string>::iterator it = svec.begin(); it != 0; ++it ) |
int ia[7] = { 0, 1, 1, 2, 3, 5, 8 }; string sa[6] = { "Fort Sumter", "Manassas", "Perryville", "Vicksburg", "Meridian", "Chancellorsvine" }; (a) vector<string> svec( sa, &sa[6] ); (b) list<int> ilist( ia+4, ia+6 ); (c) list<int> ilist2( ilist.begin(), ilist.begin()+2 ); (d) vector<int> ivec( &ia[0], ia+8 ); (e) list<string> slist( sa+6, sa ); |
Вот еще один фрагмент программы, в котором есть тонкая, но серьезная ошибка. Видите ли вы, в чем она заключается?
|
int ia[] = { 0, 1, 1, 2, 3, 5, 5, 8 }; vector< int > ivec( ia, ia+8 ), vres; // ... // поведение программы во время выполнения не определено |
unique_copy( ivec.begin(), ivec.end(), vres.begin() );
Проблема вызвана тем, что алгоритм unique_copy() использует присваивание для копирования значения каждого элемента из вектора ivec, но эта операция завершится неудачно, поскольку в vres не выделено место для хранения девяти целых чисел.
Можно было бы написать две версии алгоритма unique_copy(): одна присваивает элементы, а вторая вставляет их. Эта последняя версия должна, в таком случае, поддерживать вставку в начало, в конец или в произвольное место контейнера.
Альтернативный подход, принятый в стандартной библиотеке, заключается в определении трех адаптеров, которые возвращают специальные итераторы вставки:
· back_inserter()
вызывает определенную для контейнера операцию вставки push_back()
вместо оператора присваивания. Аргументом back_inserter() является сам контейнер. Например, вызов unique_copy()
можно исправить, написав:
|
// правильно: теперь unique_copy() вставляет элементы с помощью // vres.push_back()... unique_copy( ivec.begin(), ivec.end(), |
back_inserter( vres ) );
· front_inserter()
вызывает определенную для контейнера операцию вставки push_front()
вместо оператора присваивания. Аргументом front_inserter()
тоже является сам контейнер. Заметьте, однако, что класс vector не поддерживает push_front(), так что использовать такой адаптер для вектора нельзя:
|
// увы, ошибка: // класс vector не поддерживает операцию push_front() // следует использовать контейнеры deque или list unique_copy( ivec.begin(), ivec.end(), |
front_inserter( vres ) );
· inserter()
unique_copy( ivec.begin(), ivec.end(), |
vector< int >::iterator iter = vres.begin(), iter2 = ivec.begin(); for ( ; iter2 != ivec.end() ++ iter, ++iter2 ) |