Представим себе, что мы решаем задачу возведения 2 в степень 10. Пишем:
|
#include <iostream> |
int main() {
// a first solution
cout << "2 raised to the power of 10: ";
cout << 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2;
cout << endl;
return 0;
}
Задача решена, хотя нам и пришлось неоднократно проверять, действительно ли 10 раз повторяется литерал 2. Мы не ошиблись в написании этой длинной последовательности двоек, и программа выдала правильный результат – 1024.
Но теперь нас попросили возвести 2 в 17 степень, а потом в 23. Чрезвычайно неудобно каждый раз модифицировать текст программы! И, что еще хуже, очень просто ошибиться, написав лишнюю двойку или пропустив ее... А что делать, если нужно напечатать таблицу степеней двойки от 0 до 15? 16 раз повторить две строки, имеющие общий вид:
|
cout << "2 в степени X\t"; |
cout << 2 * ... * 2;
где Х последовательно увеличивается на 1, а вместо отточия подставляется нужное число литералов?
Да, мы справились с задачей. Заказчик вряд ли будет вникать в детали, удовлетворившись полученным результатом. В реальной жизни такой подход достаточно часто срабатывает, более того, бывает оправдан: задача решена далеко не самым изящным способом, зато в желаемый срок. Искать более красивый и грамотный вариант может оказаться непрактичной тратой времени.
В данном случае “метод грубой силы” дает правильный ответ, но как же неприятно и скучно решать задачу подобным образом! Мы точно знаем, какие шаги нужно сделать, но сами эти шаги просты и однообразны.
Привлечение более сложных механизмов для той же задачи, как правило, значительно увеличивает время подготовительного этапа. Кроме того, чем более сложные механизмы применяются, тем больше вероятность ошибок. Но даже несмотря на неизбежные ошибки и неверные ходы, применение “высоких технологий” может принести выигрыш в скорости разработки, не говоря уже о том, что эти технологии значительно расширяют наши возможности. И – что интересно! – сам процесс решения может стать привлекательным.
#include <iostream> int main() { // objects of type int int value = 2; int pow = 10; cout << value << " в степени " << pow << ": \t"; int res = 1; // оператор цикла: // повторить вычисление res // до тех пор пока cnt не станет больше pow for ( int cnt=1; cnt <= pow; ++cnt ) res = res * value; cout << res << endl; |
int pow( int val, int exp ) |
#include <iostream> extern int pow(int,int); |
Частью стандартной библиотеки С++ является библиотека iostream, которая реализована как иерархия классов и обеспечивает базовые возможности ввода/вывода.
Ввод с терминала, называемый стандартным вводом, “привязан” к предопределенному объекту cin. Вывод на терминал, или стандартный вывод, привязан к объекту cout. Третий предопределенный объект, cerr, представляет собой стандартный вывод для ошибок. Обычно он используется для вывода сообщений об ошибках и предупреждений.
Для использования библиотеки ввода/вывода необходимо включить соответствующий заголовочный файл:
#include <iostream>
Чтобы
значение поступило в стандартный вывод или в стандартный вывод для ошибок используется оператор <<:
|
int v1, v2; // ... cout << "сумма v1 и v2 = "; cout << v1 + v2; |
cout << "\n";
Последовательность "\n"
представляет собой символ перехода на новую строку. Вместо "\n" мы можем использовать предопределенный манипулятор endl.
cout << endl;
Манипулятор endl не просто выводит данные (символ перехода на новую строку), но и производит сброс буфера вывода. (Предопределенные манипуляторы рассматриваются в главе 20.)
Операторы вывода можно сцеплять. Так, три строки в предыдущем примере заменяются одной:
cout << "сумма v1 и v2 = " << v1 + v2 << "\n";
Для чтения значения из стандартного ввода применяется оператор ввода (>>):
|
string file_name; // ... cout << "Введите имя файла: "; |
cin >> file_name;
Операторы ввода, как и операторы вывода, можно сцеплять:
|
string ifile, ofile; |
// ...
cout << "Введите имя входного и выходного файлов: ";
cin >> ifile >> ofile;
Каким образом ввести заранее неизвестное число значений? Мы вернемся к этому вопросу в конце раздела 2.2, а пока скажем, что последовательность инструкций
|
string word; |
while ( cin >> word )
// ...
считывает по одному слову из стандартного ввода до тех пор, пока не считаны все слова. Выражение
#include <iostream> #include <string> int main () { string word; while ( cin >> word ) cout << "Прочитано слово: " << word << "\n"; cout << "Все слова прочитаны!"; |
Прочитано слово: riverrun, Прочитано слово: past Прочитано слово: Eve, Прочитано слово: and Прочитано слово: Adam's |
Для поддержки полного набора обобщенных алгоритмов стандартная библиотека определяет пять категорий итераторов, положив в основу классификации множество операций. Это итераторы чтения (InputIterator), записи (OutputIterator), однонаправленные (ForwardIterator) и двунаправленные итераторы (BidirectionalIterator), а также итераторы с произвольным доступом (RandomAccessIterators). Ниже приводится краткое обсуждение характеристик каждой категории:
·
итератор чтения можно использовать для получения элементов из контейнера, но поддержка записи в контейнер не гарантируется. Такой итератор должен обеспечивать следующие операции (итераторы, поддерживающие также дополнительные операции, можно употреблять в качестве итераторов чтения при условии, что они удовлетворяют минимальным требованиям): сравнение двух итераторов на равенство и неравенство, префиксная и постфиксная форма инкремента итератора для адресации следующего элемента (оператор ++), чтение элемента с помощью оператора разыменования (*). Такого уровня поддержки требуют, в частности, алгоритмы find(), accumulate() и equal(). Любому алгоритму, которому необходим итератор чтения, можно передавать также и итераторы категорий, описанных в пунктах 3, 4 и 5;
· итератор записи можно представлять себе как противоположный по функциональности итератору чтения. Иными словами, его можно использовать для записи элементов контейнера, но поддержка чтения из контейнера не гарантируется. Такие итераторы обычно применяются в качестве третьего аргумента алгоритма (например, copy()) и указывают на позицию, с которой надо начинать копировать. Любому алгоритму, которому необходим итератор записи, можно передавать также и итераторы других категорий, перечисленных в пунктах 3, 4 и 5;
· однонаправленный итератор можно использовать для чтения и записи в контейнер, но только в одном направлении обхода (обход в обоих направлениях поддерживается итераторами следующей категории). К числу обобщенных алгоритмов, требующих как минимум однонаправленного итератора, относятся adjacent_find(), swap_range() и replace(). Конечно, любому алгоритму, которому необходим подобный итератор, можно передавать также и итераторы описанных ниже категорий;
(a) const vector<string> file_names( sa, sa+6 ); vector<string>::iterator it = file_names.begin()+2; (b) const vector<int> ivec; fill( ivec.begin(), ivec.end(), ival ); (c) sort( ivec.begin(), ivec.end() ); (d) list<int> ilist( ia, ia+6 ); binary_search( ilist.begin(), ilist.end() ); |
Таблица 4.3. Побитовые операторы
| Символ операции | Значение | Использование | |||
| ~ | Побитовое НЕ | ~expr | |||
| << | Сдвиг влево | expr1 << expr2 | |||
| >> | Сдвиг вправо | expr1 >> expr2 | |||
| & | Побитовое И | expr1 & expr2 | |||
| ^ | Побитовое ИСКЛЮЧАЮЩЕЕ ИЛИ | expr1 ^ expr2 | |||
| | | Побитовое ИЛИ | expr1 | expr2 | |||
| &= | Побитовое И с присваиванием | expr1 &= expr2 | |||
| ^= | Побитовое ИСКЛЮЧАЮЩЕЕ ИЛИ с присваиванием | expr1 ^= expr2 | |||
| |= | Побитовое ИЛИ с присваиванием | expr1 |= expr2 | |||
| <<= | Сдвиг влево с присваиванием | expr1 <<= expr2 | |||
| >>= | Сдвиг вправо с присваиванием | expr1 >>= expr2 |
Побитовые операции рассматривают операнды как упорядоченные наборы битов, каждый бит может иметь одно из двух значений – 0 или 1. Такие операции позволяют программисту манипулировать значениями отдельных битов. Объект, содержащий набор битов, иногда называют битовым вектором. Он позволяет компактно хранить набор флагов – переменных, принимающих значение “да” “нет”. Например, компиляторы зачастую помещают в битовые векторы спецификаторы типов, такие, как const и volatile. Библиотека iostream
использует эти векторы для хранения состояния формата вывода.
Как мы видели, в С++ существуют два способа работы со строками: использование C-строк и объектов типа string стандартной библиотеки – и два подхода к массивам: массивы встроенного типа и объект vector. При работе с битовыми векторами также можно применять подход, заимствованный из С, – использовать для представления такого вектора объект встроенного целого типа, обычно unsigned int, или
класс bitset
стандартной библиотеки С++. Этот класс инкапсулирует семантику вектора, предоставляя операции для манипулирования отдельными битами. Кроме того, он позволяет ответить на вопросы типа: есть ли “взведенные” биты (со значением 1) в векторе? Сколько битов “взведено”?
В общем случае предпочтительнее пользоваться классом bitset, однако, понимание работы с битовыми векторами на уровне встроенных типов данных очень полезно. В этом разделе мы рассмотрим применение встроенных типов для представления битовых векторов, а в следующем – класс bitset.
inline boo1 bit_on (unsigned int ui, int pos) { return u1 & ( 1 << pos ); |
enum students { Danny = 1, Jeffrey, Ethan, Zev, Ebie, // ... AnnaP = 26, AnnaL = 27 }; const int student_size = 27; // наш битовый вектор начинается с 1 boo1 has_passed_quiz[ student_size+l ]; for ( int index = 1; index <= student_size; ++-index ) |
Инициализация одного объекта класса другим объектом того же класса, как, например:
|
Account oldAcct( "Anna Livia Plurabelle" ); |
Account newAcct( oldAcct );
называется почленной инициализацией по умолчанию. По умолчанию – потому, что она производится автоматически, независимо от того, есть явный конструктор или нет. Почленной – потому, что единицей инициализации является отдельный нестатический член, а не побитовая копия всего объекта класса.
Такую инициализацию проще всего представить, если считать, что компилятор создает специальный внутренний копирующий конструктор, где поочередно, в порядке объявления, инициализируются все нестатические члены. Если рассмотреть первое определение нашего класса Account:
|
class Account { public: // ... private: char *_name; unsigned int _acct_nmbr; double _balance; |
};
то можно представить, что копирующий конструктор по умолчанию определен так:
|
inline Account:: Account( const Account &rhs ) { _name = rhs._name; _acct_nmbr = rhs._acct_nmbr; _balance = rhs._balance; |
}
Почленная инициализация одного объекта класса другим встречается в следующих ситуациях:
· явная инициализация одного объекта другим:
Account newAcct( oldAcct );
· передача объекта класса в качестве аргумента функции:
|
extern bool cash_on_hand( Account acct ); if ( cash_on_hand( oldAcct )) |
// ...
· передача объекта класса в качестве возвращаемого функцией значения:
|
extern Account consolidate_accts( const vector< Account >& ) { Account final_acct; // выполнить финансовую операцию return final_acct; |
}
· определение непустого последовательного контейнера:
// вызывается пять копирующих конструкторов класса string |
inline Account:: Account( const Account &rhs ) { // решить проблему псевдонима указателя _name = new char[ strlen(rhs._name)+1 ]; strcpy( _name, rhs._name ); // решить проблему уникальности номера счета _acct_nmbr = get_unique_acct_nmbr(); // копирование этого члена и так работает _balance = rhs._balance; |
class Account { public: Account(); Account( const char*, double=0.0 ); // ... private: Account( const Account& ); // ... |
При проектировании класса мы должны позаботиться о том, чтобы почленная инициализация (см. раздел 14.6) и почленное присваивание (см. раздел 14.7) были реализованы правильно и эффективно. Рассмотрим связь этих операций с наследованием.
До сих пор мы не занимались явной обработкой почленной инициализации. Посмотрим, что происходит в нашей иерархии классов Query по умолчанию.
В абстрактном базовом классе Query
определены три нестатических члена:
|
class Query { public: // ... protected: int _paren; set<short> *_solition; vector<location> _loc; // ... |
};
Член _solution, если он установлен, адресует множество, память для которого выделена в хипе функцией-членом _vec2set(). Деструктор Query применяет к _solution
оператор delete.
Класс Query
должен предоставлять как явный копирующий конструктор, так и явный копирующий оператор присваивания. (Если вам это непонятно, перечитайте раздел 14.6.) Но сначала посмотрим, как почленное копирование по умолчанию происходит без них.
Производный класс NameQuery
содержит объект-член типа string и подобъект базового Query. Если есть объект folk класса NameQuery:
NameQuery folk( "folk" );
то инициализация music с помощью folk
NameQuery music = folk;
осуществляется так:
1. Компилятор проверяет, есть ли в NameQuery
явный копирующий конструктор. (Его нет. Поэтому необходимо применить почленную инициализацию по умолчанию.)
2. Далее компилятор проверяет, содержит ли объект NameQuery
подобъекты базового класса. (Да, в нем имеется подобъект Query.)
3. Компилятор проверяет, определен ли в классе Query
явный копирующий конструктор. (Нет, поэтому компилятор применит почленную инициализацию по умолчанию.)
4. Компилятор проверяет, содержит ли объект Query
подобъекты базового класса. (Нет.)
5. Компилятор просматривает все нестатические члены Query
inline NotQuery:: NotQuery( const NotQuery &rhs ) // вызывается Query::Query( const Query &rhs ) : Query( rhs ) |
#include "Query.h" int main() { NameQuery nm( "alice" ); NameQuery nm( "emma" ); NotQuery nq1( &nm ); cout << "notQuery 1: " << nq1 << endl; NotQuery nq2( nq1 ); cout << "notQuery 2: " << nq2 << endl; NotQuery nq3( &nm2 ); cout << "notQuery 3: " << nq3 << endl; nq3 = nq2; cout << "notQuery 3 присвоено значение nq2: " << nq3 << endl; AndQuery aq( &nq1, &nm2 ); cout << "AndQuery : " << aq << endl; AndQuery aq2( aq ); cout << "AndQuery 2: " << aq2 << endl; AndQuery aq3( &nm, &nm2 ); cout << "AndQuery 3: " << aq3 << endl; aq2 = aq3; cout << "AndQuery 2 после присваивания: " << aq2 << endl; |
Query& Query:: operator=( const Query &rhs ) { // предотвратить присваивание самому себе if ( &rhs != this ) { _paren = rhs._paren; _loc = rhs._loc; delete _solution; _solution = 0; } return *this; |
inline NotQuery& NotQuery:: operator=( const NotQuery &rhs ) { // предотвратить присваивание самому себе if ( &rhs != this ) { // вызвать копирующий оператор присваивания Query this->Query::operator=( rhs ); // скопировать операнд _op = rhs._op->clone(); } return *this; |
Присваивание одному объекту класса значения другого объекта того же класса реализуется почленным присваиванием по умолчанию. От почленной инициализации по умолчанию оно отличается только использованием копирующего оператора присваивания вместо копирующего конструктора:
newAcct = oldAcct;
по умолчанию присваивает каждому нестатическому члену newAcct значение соответственного члена oldAcct. Компилятор генерирует следующий копирующий оператор присваивания:
|
inline Account& Account:: operator=( const Account &rhs ) { _name = rhs._name; _balance = rhs._balance; _acct_nmbr = rhs._acct_nmbr; |
}
Как правило, если для класса не подходит почленная инициализация по умолчанию, то не подходит и почленное присваивание по умолчанию. Например, для первоначального определения класса Account, где член _name был объявлен как char*, такое присваивание не годится ни для _name, ни для _acct_nmbr.
Мы можем подавить его, если предоставим явный копирующий оператор присваивания, где будет реализована подходящая для класса семантика:
|
// общий вид копирующего оператора присваивания className& className:: operator=( const className &rhs ) { // не надо присваивать самому себе if ( this != &rhs ) { // здесь реализуется семантика копирования класса } // вернуть объект, которому присвоено значение return *this; |
}
Здесь условная инструкция
if ( this != &rhs )
предотвращает присваивание объекта класса самому себе, что особенно неприятно в ситуации, когда копирующий оператор присваивания сначала освобождает некоторый ресурс, ассоциированный с объектом в левой части, чтобы назначить вместо него ресурс, ассоциированный с объектом в правой части. Рассмотрим копирующий оператор присваивания для класса Account:
|
Account& Account:: operator=( const Account &rhs ) { // не надо присваивать самому себе if ( this != &rhs ) { delete [] _name; _name = new char[strlen(rhs._name)+1]; strcpy( _name,rhs._name ); _balance = rhs._balance; _acct_nmbr = rhs._acct_nmbr; } return *this; |
inline Account& Account:: operator=( const Account &rhs ) { _balance = rhs._balance; _acct_nmbr = rhs._acct_nmbr; // этот вызов правилен и с точки зрения программиста name.string::operator=( rhs._name ); |
Account& Account:: operator=( const Account &rhs ) { // не надо присваивать самому себе if ( this != &rhs ) { // вызывается string::operator=( const string& ) _name = rhs._name; _balance = rhs._balance; } return *this; |
Если дан указатель на один из конкретных подтипов запроса, то разместить в хипе дубликат объекта несложно:
|
NotQuery *pnq; // установить pnq ... // оператор new вызывает // копирующий конструктор NotQuery ... |
NotQuery *pnq2 = new NotQuery( *pnq );
Если же у нас есть только указатель на абстрактный класс Query, то задача создания дубликата становится куда менее тривиальной:
|
const Query *pq = pnq->op(); |
// как получить дубликат pq?
Если бы позволялось объявить виртуальный экземпляр оператора new, то проблема была бы решена, поскольку автоматически вызывался бы нужный экземпляр. К сожалению, это невозможно: new – статическая функция-член, которая применяется к неструктурированной памяти еще до конструирования объекта класса (см. раздел 15.8).
Но хотя оператор new
нельзя сделать виртуальным, разрешается создать его суррогат, который будет выделять память из хипа и копировать туда объекты, – clone():
|
class Query { public: virtual Query *clone() = 0; // ... |
};
Вот как он может быть реализован в классе NameQuery:
|
class NameQuery : public Query { public: virtual Query *clone() // вызывается копирующий конструктор класса NameQuery { return new NameQuery( *this ); } // ... |
};
Это работает правильно, если тип целевого указателя Query*:
|
Query *pq = new NameQuery( "valery" ); |
Query *pq2 = pq->clone();
Если же его тип равен NameQuery*, нужно привести возвращенный указатель типа Query* назад к типу NameQuery*:
|
NameQuery *pnq = new NameQuery( "Rilke" ); NameQuery *pnq2 = |
static_cast<NameQuery*>( pnq->clone() );
(Причина, по которой необходимо преобразование типа, объясняется в разделе 19.1.1.)
Как правило, тип значения, возвращаемого реализацией виртуальной функции в производном классе, должен совпадать с типом, возвращаемым ее реализацией в базовом. Исключение, о котором мы уже упоминали, призвано поддержать рассмотренную ситуацию. Если виртуальная функция в базовом классе возвращает значение некоторого типа класса (либо указатель или ссылку на тип класса), то ее реализация в производном может возвращать значение, тип которого является производным от этого класса с открытым типом наследования (то же относится к ссылкам и указателям):
class NameQuery : public Query { public: virtual NameQuery *clone() { return new NameQuery( *this ); } // ... |
// Query *pq = new NameQuery( "Broch" ); Query *pq2 = pq->clone(); // правильно // NameQuery *pnq = new NameQuery( "Rilke" ); |
class NotQuery : public Query { public: virtual NotQuery *clone() { return new NotQuery( *this ); } // ... |
Под расширением типа понимается одно из следующих преобразований:
·
фактический аргумент типа char, unsigned char или short
расширяется до типа int. Фактический аргумент типа unsigned short
расширяется до типа int, если машинный размер int
больше, чем размер short, и до типа unsigned int в противном случае;
· аргумент типа float
расширяется до типа double;
· аргумент перечислимого типа расширяется до первого из следующих типов, который способен представить все значения элементов перечисления: int, unsigned int, long, unsigned long;
· аргумент типа bool
расширяется до типа int.
Подобное расширение применяется, когда тип фактического аргумента совпадает с одним из только что перечисленных типов, а формальный параметр относится к соответствующему расширенному типу:
|
extern void manip( int ); int main() { manip( 'a' ); // тип char расширяется до int return 0; |
}
Символьный литерал имеет тип char. Он расширяется до int. Поскольку расширенный тип соответствует типу формального параметра функции manip(), мы говорим, что ее вызов требует расширения типа аргумента.
Рассмотрим следующий пример:
|
extern void print( unsigned int ); extern void print( int ); extern void print( char ); unsigned char uc; |
print( uc ); // print( int ); для uc требуется только расширение типа
Для аппаратной платформы, на которой unsigned char
занимает один байт памяти, а int – четыре байта, расширение преобразует unsigned char в int, так как с его помощью можно представить все значения типа unsigned char. Для такой машинной архитектуры из приведенного в примере множества перегруженных функций наилучшее соответствие аргументу типа unsigned char
enum Stat ( Fail, Pass ); extern void ff( int ); extern void ff( char ); int main() { // правильно: элемент перечисления Pass расширяется до типа int ff( Pass ); // ff( int ) ff( 0 ); // ff( int ) |
#include <string> string format( int ); string format( unsigned int ); int main() { format(a1); // вызывается format( int ) format(a2); // вызывается format( unsigned int ) return 0; |
Имеется пять видов стандартных преобразований, а именно:
1. преобразования целых типов: приведение от целого типа или перечисления к любому другому целому типу (исключая трансформации, которые выше были отнесены к категории расширения типов);
2. преобразования типов с плавающей точкой: приведение от любого типа с плавающей точкой к любому другому типу с плавающей точкой (исключая трансформации, которые выше были отнесены к категории расширения типов);
3. преобразования между целым типом и типом с плавающей точкой: приведение от любого типа с плавающей точкой к любому целому типу или наоборот;
4. преобразования указателей: приведение целого значения 0 к типу указателя или трансформация указателя любого типа в тип void*;
5. преобразования в тип bool: приведение от любого целого типа, типа с плавающей точкой, перечислимого типа или указательного типа к типу bool.
Вот несколько примеров:
|
extern void print( void* ); extern void print( double ); int main() { int i; print( i ); // соответствует print( double ); // i подвергается стандартному преобразованию из int в double print( &i ); // соответствует print( void* ); // &i подвергается стандартному преобразованию // из int* в void* return 0; |
}
Преобразования, относящиеся к группам 1, 2 и 3, потенциально опасны, так как целевой тип может и не обеспечивать представления всех значений исходного. Например, с помощью float
нельзя адекватно представить все значения типа int. Именно по этой причине трансформации, входящие в эти группы, отнесены к категории стандартных преобразований, а не расширений типов.
|
int i; void calc( float ); int main() { calc( i ); // стандартное преобразование между целым типом и типом с // плавающей точкой потенциально опасно в зависимости от // значения i return 0; |
extern void manip( long ); |
int main() { manip( 3.14 ); // ошибка: неоднозначность // manip( float ) не лучше, чем manip( int ) return 0; |
extern void farith( unsigned int ); extern void farith( float ); int main() { // каждый из последующих вызовов неоднозначен farith( 'a' ); // аргумент имеет тип char farith( 0 ); // аргумент имеет тип int farith( 2uL ); // аргумент имеет тип unsigned long farith( 3.14159 ); // аргумент имеет тип double farith( true ); // аргумент имеет тип bool |
void set(int*); int main() { // преобразование указателя из 0 в int* применяется к аргументам // в обоих вызовах set( 0L ); set( 0x00 ); return 0; |
enum EN { zr = 0 }; |
void print( int ); void print( void * ); void set( const char * ); void set( char * ); int main () { print( 0 ); // вызывается print( int ); set( 0 ); // неоднозначность return 0; |
#include <string> extern void reset( void * ); void func( int *pi, string *ps ) { // ... reset( pi ); // преобразование указателя: int* в void* /// ... reset( ps ); // преобразование указателя: string* в void* |
typedef int (*PFV)(); extern PFV testCases[10]; // массив указателей на функции extern void reset( void * ); int main() { // ... reset( textCases[0] ); // ошибка: нет стандартного преобразования // между int(*)() и void* return 0; |
Самый простой случай возникает тогда, когда типы фактических аргументов совпадают с типами формальных параметров. Например, есть две показанные ниже перегруженные функции max(). Тогда каждый из вызовов max() точно соответствует одному из объявлений:
|
int max( int, int ); double max( double, double ); int i1; void calc( double d1 ) { max( 56, i1 ); // точно соответствует max( int, int ); max( d1, 66.9 ); // точно соответствует max( double, double ); |
}
Перечислимый тип точно соответствует только определенным в нем элементам перечисления, а также объектам, которые объявлены как принадлежащие к этому типу:
|
enum Tokens { INLINE = 128; VIRTUAL = 129; }; Tokens curTok = INLINE; enum Stat { Fail, Pass }; extern void ff( Tokens ); extern void ff( Stat ); extern void ff( int ); int main() { ff( Pass ); // точно соответствует ff( Stat ) ff( 0 ); // точно соответствует ff( int ) ff( curTok ); // точно соответствует ff( Tokens ) // ... |
}
Выше уже упоминалось, что фактический аргумент может точно соответствовать формальному параметру, даже если для приведения их типов необходимо некоторое тривиальное преобразование, первое из которых – преобразование l-значения в r-значение. Под l-значением понимается объект, удовлетворяющий следующим условиям:
· можно получить адрес объекта;
· можно получить значение объекта;
· это значение легко модифицировать (если только в объявлении объекта нет спецификатора const).
Напротив, r-значение – это выражение, значение которого вычисляется, или выражение, обозначающее временный объект, для которого нельзя получить адрес и значение которого нельзя модифицировать. Вот простой пример:
int calc( int ); int main() { int lval, res; lval = 5; // lvalue: lval; rvalue: 5 res = calc( lval ); // lvalue: res // rvalue: временный объект для хранения значения, // возвращаемого функцией calc() return 0; |
int obj1; int obj2; int main() { // ... int local = obj1 + obj2; return 0; |
#include <string> string color( "purple" ); void print( string ); int main() { print( color ); // точное соответствие: преобразование lvalue // в rvalue return 0; |
#include <list> |
list<int> li(20); int main() { // ... print( li ); // точное соответствие: нет преобразования lvalue в // rvalue return 0; |
int ai[3]; void putValues(int *); int main() { // ... putValues(ai); // точное соответствие: преобразование массива в // указатель return 0; |
int lexicoCompare( const string &, const string & ); typedef int (*PFI)( const string &, const string & ); void sort( string *, string *, PFI ); string as[10]; int main() { // ... sort( as, as + sizeof(as)/sizeof(as[0] - 1 ), lexicoCompare // точное соответствие // преобразование функции в указатель ); return 0; |
int a[5] = { 4454, 7864, 92, 421, 938 }; int *pi = a; bool is_equal( const int * , const int * ); void func( int *parm ) { // точное соответствие между pi и parm: преобразование спецификаторов if ( is_equal( pi, parm ) ) // ... return 0; |
extern void takeCI( const int ); int main() { int ii = ...; takeCI(ii); // преобразование спецификаторов не применяется return 0; |
extern void init( int *const ); extern int *pi; int main() { // ... init(pi); // преобразование спецификаторов не применяется return 0; |
extern void ff(int); |
Оператор взятия индекса является простейшим способом извлечения элемента. Например:
|
// map<string,int> word_count; |
int count = word_count[ "wrinkles" ];
Однако этот способ работает так, как надо, только при условии, что запрашиваемый ключ действительно содержится в отображении. Иначе оператор взятия индекса поместит в отображение элемент с таким ключом. В данном случае в word_count
занесется пара
string( "wrinkles" ), 0
Класс map
предоставляет две операции для того, чтобы выяснить, содержится ли в нем определенное значение ключа.
· count(keyValue): функция-член count()
возвращает количество элементов с данным ключом. (Для отображения оно равно только 0 или 1). Если count()
вернула 1, мы можем смело использовать индексацию:
|
int count = 0; if ( word_count.count( "wrinkles" )) |
count = word_count[ "wrinkles" ];
· find(keyValue): функция-член find()
возвращает итератор, указывающий на элемент, если ключ найден, и итератор end() в противном случае. Например:
|
int count = 0; map<string,int>::iterator it = word_count.find( "wrinkles" ); if ( it != word_count.end() ) |
count = (*it).second;
Значением итератора является указатель на объект pair, в котором first
содержит ключ, а second – значение. (Мы вернемся к этому в следующем подразделе.)
Две операции, позволяющие отыскать в наборе определенное значение,– это find() и count(). find() возвращает итератор, указывающий на найденный элемент, или значение, равное end(), если он отсутствует. count() возвращает 1 при наличии элемента и 0 в противном случае. Добавим проверку на существование в функцию build_word_map():
|
if ( exclusion_set.count( textword )) continue; |
// добавим отсутствующее слово
Как уже было сказано, определение пространства имен может состоять из разрозненных частей и размещаться в разных файлах. Следовательно, член пространства разрешено объявлять во многих файлах. Например:
|
// primer.h namespace cplusplus_primer { // ... void inverse( matrix & ); } // usel.C #include "primer.h" // объявление cplusplus_primer::inverse() в use1.C // use2.C #include "primer.h" |
// объявление cplusplus_primer::inverse() в use2.C
Объявление cplusplus::inverse() в primer.h
ссылается на одну и ту же функцию в обоих исходных файлах use1.C и use2.C.
Член пространства имен является глобальной сущностью, хотя его имя квалифицировано. Требование ПОО (правило одного определения, см. раздел 8.2) распространяется и на него. Чтобы удовлетворить этому требованию, программы, в которых используются пространства имен, обычно организуют следующим образом:
1. Объявления функций и объектов, являющихся членами пространства имен, помещают в заголовочный файл, который включается в каждый исходный файл, где они используются.
|
// ---- primer.h ---- namespace cplusplus_primer { class matrix { /* ... */ }; // объявления функций extern matrix operator+ ( const matrix &m1, const matrix &m2 ); extern void inverse( matrix & ); // объявления объектов extern bool error_state; |
}
2. Определения этих членов помещают в исходный файл, содержащий реализацию:
|
// ---- primer.C ---- #include "primer.h" namespace cplusplus_primer { // определения функций void inverse( matrix & ) { /* ... */ } matrix operator+ ( const matrix &ml, const matrix &m2 ) { /" ... */ } // определения объектов bool error_state = false; |
}
Для объявления объекта без его определения используется ключевое слово extern, как и в случае такого объявления в глобальной области видимости.
По умолчанию инструкции программы выполняются одна за другой, последовательно. В программе
|
int main() { readIn(); sort(); compact(); print(); return 0; |
}
первой будет выполнена инструкция readIn(), за ней sort(), compact() и наконец print().
Однако представим себе ситуацию, когда количество продаж невелико: оно равно 1 или даже 0. Вряд ли стоит вызывать функции sort() и compact() для такого случая. Но вывести результат все-таки нужно, поэтому функцию print() следует вызывать в любом случае. Для этого случая мы можем использовать условную инструкцию if. Нам придется переписать функцию readIn() так, чтобы она возвращала количество прочитанных записей:
|
// readIn() возвращает количество прочитанных записей // возвращаемое значение имеет тип int int readIn() { ... } // ... int main() { int count = readIn(); // если количество записей больше 1, // то вызвать sort() и compact() if ( count > 1 ) { sort(); compact(); } if ( count == 0 ) cout << "Продаж не было\n"; else print(); return 0; |
}
Первая инструкция if
обеспечивает условное выполнение блока программы: функции sort() и compact()
вызываются только в том случае, если count больше 1. Согласно второй инструкции if на терминал выводится сообщение “Продаж не было”, если условие истинно, т.е. значение count
равно 0. Если же это условие ложно, производится вызов функции print(). (Детальное описание инструкции if
приводится в разделе 5.3.)
Другим распространенным способом непоследовательного выполнения программы является итерация, или инструкция цикла. Такая инструкция предписывает повторять блок программы до тех пор, пока некоторое условие не изменится с true на false. Например:
|
int main() { int iterations = 0; bool continue_loop = true; while ( continue_loop != false ) { iterations++; cout << "Цикл был выполнен " << iterations << "раз\n"; if ( iterations == 5 ) continue_loop = false; } return 0; |
}
В этом надуманном примере цикл while выполняется пять раз, до тех пор пока переменная iterations не получит значение 5 и переменная continue_loop не станет равной false. Инструкция
iterations++;
увеличивает значение переменной iterations на единицу. (Инструкции цикла детально рассматриваются в главе 5.)
Виртуальные базовые классы всегда конструируются перед невиртуальными, вне зависимости от их расположения в иерархии наследования. Например, в приведенной иерархии у класса TeddyBear
(плюшевый мишка) есть два виртуальных базовых: непосредственный– ToyAnimal
(игрушечное животное) и экземпляр ZooAnimal, от которого унаследован класс Bear:
| class Character { ... }; // персонаж
class BookCharacter : public Character { ... }; // литературный персонаж class ToyAnimal { ... }; // игрушка class TeddyBear : public BookCharacter, public Bear, public virtual ToyAnimal |
{ ... };
Эта иерархия изображена на рис. 18.5, где виртуальное наследование показано пунктирной стрелкой, а невиртуальное – сплошной.
![]() |
![]() |
![]() |
Character ZooAnimal ToyAnimal
![]() |
BookCharacter Bear

TeddyBear
¾¾> невиртуальное наследование
- - - -> виртуальноe наследование
Рис. 18.5. Иерархия виртуального наследования класса TeddyBear
Непосредственные базовые классы просматриваются в порядке их объявления при поиске среди них виртуальных. В нашем примере сначала анализируется поддерево наследования BookCharacter, затем Bear и наконец ToyAnimal. Каждое поддерево обходится в глубину, т.е. поиск начинается с корневого класса и продвигается вниз. Так, для поддерева BookCharacter сначала просматривается Character, а затем BookCharacter. Для поддерева Bear
– ZooAnimal, а потом Bear.
При описанном алгоритме поиска порядок вызова конструкторов виртуальных базовых классов для TeddyBear
таков: ZooAnimal, потом ToyAnimal.
После того как вызваны конструкторы виртуальных базовых классов , настает черед конструкторов невиртуальных, которые вызываются в порядке объявления: BookCharacter, затем Bear. Перед выполнением конструктора BookCharacter вызывается конструктор его базового класса Character.
В функции try_array() из раздела 16.13, предназначенной для тестирования нашей предыдущей реализации шаблона класса Array, есть две инструкции:
|
int index = iA.find( find_val ); |
Type value = iA[ index ];
find()
возвращает индекс первого вхождения значения find_val или -1, если значение в массиве не найдено. Этот код некорректен, поскольку в нем не проверяется, что не была возвращена -1. Поскольку -1
находится за границей массива, то каждая инициализация value
может привести к ошибке. Поэтому мы создадим подтип Array, который будет контролировать выход за границы массива, – Array_RC и поместим его определение в заголовочный файл Array_RC.h:
|
#ifndef ARRAY_RC_H #define ARRAY_RC_H #include "Array.h" template <class Type> class Array_RC : public virtual Array<Type> { public: Array_RC( int sz = ArraySize ) : Array<Type>( sz ) {} Array_RC( const Array_RC& r ); Array_RC( const Type *ar, int sz ); Type& operator[]( int ix ); }; |
#endif
Внутри определения производного класса каждая ссылка на спецификатор типа шаблона базового должна быть квалифицирована списком формальных параметров:
|
Array_RC( int sz = ArraySize ) |
: Array<Type>( sz ) {}
Такая запись неправильна:
|
// ошибка: Array - это не спецификатор типа |
Array_RC( int sz = ArraySize ) : Array( sz ) {}
Единственное отличие поведения класса Array_RC от базового состоит в том, что оператор взятия индекса контролирует выход за границы массива. Во всех остальных отношениях можно воспользоваться уже имеющейся реализацией шаблона класса Array. Напомним, однако, что конструкторы не наследуются, поэтому в Array_RC
определен собственный набор из трех конструкторов. Мы сделали класс Array_RC
виртуальным наследником класса Array, поскольку предвидели необходимость множественного наследования.
Вот полная реализация функций-членов Array_RC, находящаяся в файле Array_RC.C
(определения функций класса Array помещены в заголовочный файл Array.C, поскольку мы пользуемся моделью конкретизации шаблонов с включением, описанной в разделе 16.18):
#include "Array_RC.h" #include "Array.C" #include <assert.h> template <class Type> Array_RC<Type>::Array_RC( const Array_RC<Type> &r ) : Array<Type>( r ) {} template <class Type> Array_RC<Type>::Array_RC( const Type *ar, int sz ) : Array<Type>( ar, sz ) {} template <class Type> Type &Array_RC<Type>::operator[]( int ix ) { assert( ix >= 0 && ix < Array<Type>::_size ); return ia[ ix ]; |
#include "Array_RC.C" #include "try_array.C" int main() { static int ia[] = { 12,7,14,9,128,17,6,3,27,5 }; cout << "конкретизация шаблона класса Array_RC<int>\n"; try_array( iA ); return 0; |
Вторая наша специализация класса Array– отсортированный подтип Array_Sort. Мы поместим его определение в заголовочный файл Array_S.h:
|
#ifndef ARRAY_S_H_ #define ARRAY_S_H_ #include "Array.h" template <class Type> class Array_Sort : public virtual Array<Type> { protected: void set_bit() { dirty_bit = true; } void clear_bit() { dirty_bit = false; } void check_bit() { if ( dirty_bit ) { sort( 0, Array<Type>::_size-1 ); clear_bit(); } } public: Array_Sort( const Array_Sort& ); Array_Sort( int sz = Array<Type>::ArraySize ) : Array<Type>( sz ) { clear_bit(); } Array_Sort( const Type* arr, int sz ) : Array<Type>( arr, sz ) { sort( 0,Array<Type>::_size-1 ); clear_bit(); } Type& operator[]( int ix ) { set_bit(); return ia[ ix ]; } void print( ostream& os = cout ) const { check_bit(); Array<Type>::print( os ); } Type min() { check_bit(); return ia[ 0 ]; } Type max() { check_bit(); return ia[ Array<Type>::_size-1 ]; } bool is_dirty() const { return dirty_bit; } int find( Type ); void grow(); protected: bool dirty_bit; }; |
#endif
Array_Sort
включает дополнительный член – dirty_bit. Если он установлен в true, то не гарантируется, что массив по-прежнему отсортирован. Предоставляется также ряд вспомогательных функций доступа: is_dirty() возвращает значение dirty_bit; set_bit()
устанавливает dirty_bit в true; clear_bit() сбрасывает dirty_bit в false; check_bit()
пересортировывает массив, если dirty_bit равно true, после чего сбрасывает его в false. Все операции, которые потенциально могут перевести массив в неотсортированное состояние, вызывают set_bit().
При каждом обращении к шаблону Array
template <class Type> Array_Sort<Type>:: |
template <class Type> Array_Sort<Type>:: |
if ( as.is_dirty() ) |
// альтернативная реализация template <class Type> Array_Sort<Type>:: Array_Sort( const Array_Sort<Type> &as ) : Array<Type>( as ) { dirty_bit = as.dirty_bit; clear_bit(); |
template <class Type> void Array_Sort<Type>::grow() { Array<Type>::grow(); sort( 0, Array<Type>::_size-1 ); clear_bit(); |
template <class Type> int Array_Sort<Type>::find( const Type &val ) { int low = 0; int high = Array<Type>::_size-1; check_bit(); while ( low <= high ) { int mid = ( low + high )/2; if ( val == ia[ mid ] ) return mid; if ( val < ia[ mid ] ) high = mid-1; else low = mid+1; } return -1; |
#include "Array_S.C" #include "try_array.C" #include <string> main() { static int ia[ 10 ] = { 12,7,14,9,128,17,6,3,27,5 }; static string sa[ 7 ] = { "Eeyore", "Pooh", "Tigger", "Piglet", "Owl", "Gopher", "Heffalump" }; Array_Sort<int> iA( ia,10 ); Array_Sort<string> SA( sa,7 ); cout << "êîíêðåòèçàöèÿ êëàññà Array_Sort<int>" << endl; try_array( iA ); cout << "êîíêðåòèçàöèÿ êëàññà Array_Sort<string>" << endl; try_array( SA ); return 0; |
Отображение состоит из пар ключ/значение. Множество (set), напротив, содержит неупорядоченную совокупность ключей. Например, бизнесмен может составить “черный список” bad_checks, содержащий имена лиц, в течение последних двух лет присылавших фальшивые чеки. Множество полезно тогда, когда нужно узнать, содержится ли определенное значение в списке. Скажем, наш бизнесмен, принимая чек от кого-либо, может проверить, есть ли его имя в bad_checks.
Для нашей поисковой системы мы построим набор стоп-слов– слов, имеющих семантически нейтральное значение (артикли, союзы, предлоги), таких, как the, and, into, with, but и т.д. (это улучшает качество системы, однако мы уже не сможем найти первое предложение из знаменитого монолога Гамлета: “To be or not to be?”). Прежде чем добавлять слово к word_map, проверим, не содержится ли оно в списке стоп-слов. Если содержится, проигнорируем его.
Стандартная библиотека предоставляет средства для работы потоковых итераторов чтения и записи совместно со стандартными контейнерами и обобщенными алгоритмами. Класс istream_iterator
поддерживает итераторные операции с классом istream или одним из производных от него, например ifstream для работы с потоком ввода из файла. Аналогично ostream_iterator
поддерживает итераторные операции с классом ostream или одним из производных от него, например ofstream для работы с потоком вывода в файл. Для использования любого из этих итераторов следует включить заголовочный файл
#include <iterator>
В следующей программе мы пользуемся потоковым итератором чтения для получения из стандартного ввода последовательности целых чисел в вектор, а затем применяем потоковый итератор записи в качестве целевого в обобщенном алгоритме unique_copy():
|
#include <iostream> #include <iterator> #include <algorithm> #include <vector> #include <functional> /* * вход: * 23 109 45 89 6 34 12 90 34 23 56 23 8 89 23 * * выход: * 109 90 89 56 45 34 23 12 8 6 */ int main() { istream_iterator< int > input( cin ); istream_iterator< int > end_of_stream; vector<int> vec; copy ( input, end_of_stream, inserter( vec, vec.begin() )); sort( vec.begin(), vec.end(), greater<int>() ); ostream_iterator< int > output( cout, " " ); unique_copy( vec.begin(), vec.end(), output ); |
}
Может оказаться так, что в одном предложении catch не удалось полностью обработать исключение. Выполнив некоторые корректирующие действия, catch-обработчик может решить, что дальнейшую обработку следует поручить функции, расположенной “выше” в цепочке вызовов. Передать исключение другому catch-обработчику можно с помощью повторного возбуждения исключения. Для этой цели в языке предусмотрена конструкция
throw;
которая вновь генерирует объект-исключение. Повторное возбуждение возможно только внутри составной инструкции, являющейся частью catch-обработчика:
|
catch ( exception eObj ) { if ( canHandle( eObj ) ) // обработать исключение return; else // повторно возбудить исключение, чтобы его перехватил другой // catch-обработчик throw; |
}
При повторном возбуждении новый объект-исключение не создается. Это имеет значение, если catch-обработчик модифицирует объект, прежде чем возбудить исключение повторно. В следующем фрагменте исходный объект-исключение не изменяется. Почему?
|
enum EHstate { noErr, zeroOp, negativeOp, severeError }; void calculate( int op ) { try { // исключение, возбужденное mathFunc(), имеет значение zeroOp mathFunc( op ); } catch ( EHstate eObj ) { // что-то исправить // пытаемся модифицировать объект-исключение eObj = severeErr; // предполагалось, что повторно возбужденное исключение будет // иметь значение severeErr throw; } |
}
Так как eObj не является ссылкой, то catch-обработчик получает копию объекта-исключения, так что любые модификации eObj
относятся к локальной копии и не отражаются на исходном объекте-исключении, передаваемом при повторном возбуждении. Таким образом, переданный далее объект по-прежнему имеет тип zeroOp.
Чтобы модифицировать исходный объект-исключение, в объявлении исключения внутри catch-обработчика должна фигурировать ссылка:
|
catch ( EHstate &eObj ) { // модифицируем объект-исключение eObj = severeErr; // повторно возбужденное исключение имеет значение severeErr throw; |
}
Теперь eObj
ссылается на объект-исключение, созданный выражением throw, так что все изменения относятся непосредственно к исходному объекту. Поэтому при повторном возбуждении исключения далее передается модифицированный объект.
Таким образом, другая причина для объявления ссылки в catch-обработчике заключается в том, что сделанные внутри обработчика модификации объекта-исключения в таком случае будут видны при повторном возбуждении исключения. (Третья причина будет рассмотрена в разделе 19.2, где мы расскажем, как catch-обработчик вызывает виртуальные функции класса.)
Предопределенные объекты-функции подразделяются на арифметические, логические и сравнительные. Каждый объект– это шаблон класса, параметризованный типами операндов. Для использования любого из них необходимо включить заголовочный файл:
#include <functional>
Например, объект-функция, поддерживающий сложение, – это шаблон класса с именем plus. Для определения экземпляра, способного складывать два целых числа, нужно написать:
|
#include <functional> |
plus< int > intAdd;
Для выполнения операции сложения мы применяем перегруженный оператор вызова к intAdd
точно так же, как и к классу AddImage в предыдущем разделе:
|
int ival1 = 10, ival2 = 20; // эквивалентно int sum = ival1 + ival2; |
int sum = intAdd( ival1, ival2 );
Реализация шаблона класса plus
вызывает оператор сложения, ассоциированный с типом своего параметра – int. Этот и другие предопределенные объекты-функции применяются прежде всего в качестве аргументов обобщенных алгоритмов и обычно замещают подразумеваемую по умолчанию операцию. Например, по умолчанию алгоритм sort() располагает элементы контейнера в порядке возрастания с помощью оператора “меньше” базового типа. Для сортировки по убыванию мы передаем предопределенный шаблон класса greater, который вызывает оператор “больше”:
|
vector< string > svec; // ... |
sort( svec.begin(), svec.end(), greater<string>() );
Предопределенные объекты-функции перечислены в следующих разделах и разбиты на категории: арифметические, логические и сравнительные. Применение каждого из них иллюстрируется как в качестве именованного, так и в качестве безымянного объекта, передаваемого функции. Мы пользуемся следующими определениями объектов, включая и определение простого класса (перегрузка операторов подробно рассматривается в главе 15):
|
class Int { public: Int( int ival = 0 ) : _val( ival ) {} int operator-() { return -_val; } int operator%(int ival) { return -_val % ival; } bool operator<(int ival) { return -_val < ival; } bool operator!() { return -_val == 0; } private: int _val; }; vector< string > svec; string sval1, sval2, sres; complex cval1, cval2, cres; int ival1, ival2, ires; Int Ival1, Ival2, Ires; |
double dval1, dval2, dres;
Кроме того, мы определяем два шаблона функций, которым передаем различные безымянные объекты-функции:
|
template <class FuncObject, class Type> Type UnaryFunc( FuncObject fob, const Type &val ) { return fob( val ); } template <class FuncObject, class Type> Type BinaryFunc( FuncObject fob, const Type &val1, const Type &val2 ) |
{ return fob( val1, val2 ); }
Представим себе следующий оператор присваивания:
|
int ival = 0; // обычно компилируется с предупреждением |
ival = 3.541 + 3;
В результате ival
получит значение 6. Вот что происходит: мы складываем литералы разных типов – 3.541
типа double и 3
типа int. C++ не может непосредственно сложить подобные операнды, сначала ему нужно привести их к одному типу. Для этого существуют правила преобразования арифметических типов. Общий принцип таков: перейти от операнда меньшего типа к большему, чтобы не потерять точность вычислений.
В нашем случае целое значение 3 трансформируется в тип double, и только после этого производится сложение. Такое преобразование выполняется независимо от желания программиста, поэтому оно получило название неявного преобразования типов.
Результат сложения двух чисел типа double тоже имеет тип double. Значение равно 6.541. Теперь его нужно присвоить переменной ival. Типы переменной и результата 6.541 не совпадают, следовательно, тип этого значения приводится к типу переменной слева от знака равенства. В нашем случае это int. Преобразование double в int
производится автоматически, отбрасыванием дробной части (а не округлением). Таким образом, 6.541
превращается в 6, и этот результат присваивается переменной ival. Поскольку при таком преобразовании может быть потеряна точность, большинство компиляторов выдают предупреждение.
Так как компилятор не округляет числа при преобразовании double в int, при необходимости мы должны позаботиться об этом сами. Например:
|
double dva1 = 8.6; int iva1 = 5; |
ival += dva1 + 0.5; // преобразование с округлением
При желании мы можем произвести явное преобразование типов:
|
// инструкция компилятору привести double к int |
ival = static_cast< int >( 3.541 ) + 3;
В этом примере мы явно даем указание компилятору привести величину 3.541 к типу int, а не следовать правилам по умолчанию.
В этом разделе мы детально обсудим вопросы и неявного (как в первом примере), и явного преобразования типов (как во втором).
На втором шаге процесса разрешения перегрузки функции компилятор идентифицирует и ранжирует преобразования, которые следует применить к каждому фактическому аргументу вызванной функции для приведения его к типу соответствующего формального параметра любой из устоявших функций. Ранжирование может дать один из трех возможных результатов:
·
точное соответствие. Тип фактического аргумента точно соответствует типу формального параметра. Например, если в множестве перегруженных функций print()
есть такие:
|
void print( unsigned int ); void print( const char* ); |
void print( char );
то каждый из следующих трех вызовов дает точное соответствие:
|
unsigned int a; print( 'a' ); // соответствует print( char ); print( "a" ); // соответствует print( const char* ); |
print( a ); // соответствует print( unsigned int );
· соответствие с
преобразованием типа. Тип фактического аргумента не соответствует типу формального параметра, но может быть преобразован в него:
|
void ff( char ); |
ff( 0 ); // аргумент типа int приводится к типу char
· отсутствие соответствия. Тип фактического аргумента не может быть приведен к типу формального параметра в объявлении функции, поскольку необходимого преобразования не существует. Для каждого из следующих двух вызовов функции print() соответствия нет:
|
// функции print() объявлены так же, как и выше int *ip; class SmallInt { /* ... */ }; SmallInt si; print( ip ); // ошибка: нет соответствия |
print( si ); // ошибка: нет соответствия
Для установления точного соответствия тип фактического аргумента необязательно должен совпадать с типом формального параметра. К аргументу могут быть применены некоторые тривиальные преобразования, а именно:
· преобразование l-значения в r-значение;
При использовании наследования указатель или ссылка на тип базового класса способен адресовать объект любого производного от него класса. Возможность манипулировать такими указателями или ссылками независимо от фактического типа адресуемого объекта называется полиморфизмом. В этой главе мы рассмотрим три функции языка, обеспечивающие специальную поддержку полиморфизма. Сначала мы познакомимся с идентификацией типов во время выполнения (RTTI– Run-time Type Identification), которая позволяет программе узнать истинный производный тип объекта, адресованного ссылкой или указателем на тип базового класса. Затем расскажем о влиянии наследования на обработку исключений: покажем, как можно определять их в виде иерархии классов и как обработчики для типа базового класса могут перехватывать исключения производных типов. В конце главы мы вернемся к правилам разрешения перегрузки функций и посмотрим, как наследование влияет на то, какие преобразования типов можно применять к аргументам функции, и на выбор наилучшей из устоявших.
Мы продемонстрируем определение и использование множественного виртуального наследования, реализовав иерархию шаблонов классов Array (см. раздел 2.4) на основе шаблона Array
(см. главу 16), модифицированного так, чтобы он стал конкретным базовым классом. Перед тем как приступать к реализации, поговорим о взаимосвязях между шаблонами классов и наследованием.
Конкретизированный экземпляр такого шаблона может выступать в роли явного базового класса:
class IntStack : private Array<int> {};
Разрешается также произвести его от не шаблонного базового класса:
|
class Base {}; template <class Type> |
class Derived : public Base {};
Шаблон может выступать одновременно в роли базового и производного классов:
|
template <class Type> |
class Array_RC : public virtual Array<Type> {};
В первом примере конкретизированный типом int шаблон Array
служит закрытым базовым классом для не шаблонного IntStack. Во втором примере не шаблонный Base служит базовым для любого класса, конкретизированного из шаблона Derived. В третьем примере любой конкретизированный из шаблона Array_RC класс является производным от класса, конкретизированного из шаблона Array. Так, инструкция
Array_RC<int> ia;
конкретизирует экземпляры шаблонов Array и Array_RC.
Кроме того, сам параметр-шаблон может служить базовым классом [MURRAY93]:
|
template < typename Type > |
class Persistent : public Type { ... };
в данном примере определяется производный устойчивый (persistent) подтип для любого конкретизированного типа. Как отмечает Мюррей (Murray), на Type
налагается неявное ограничение: он должен быть типом класса. Например, инструкция
Persistent< int > pi; // ошибка
приводит к ошибке компиляции, поскольку встроенный тип не может быть объектом наследования.
Шаблон, выступающий в роли базового класса, должен квалифицироваться полным списком параметров. Если имеется определение:
template <class T> class Base {};
то необходимо писать:
template < class Type > |
// ошибка: Base - это шаблон, // так что должны быть заданы его аргументы template < class Type > |
#ifndef ARRAY_H #define ARRAY_H #include <iostream> // необходимо для опережающего объявления operator<< template <class Type> class Array; template <class Type> ostream& operator<<( ostream &, Array<Type> & ); template <class Type> class Array { static const int ArraySize = 12; public: explicit Array( int sz = ArraySize ) { init( 0, sz ); } Array( const Type *ar, int sz ) { init( ar, sz ); } Array( const Array &iA ) { init( iA.ia, iA.size()); } virtual ~Array() { delete[] ia; } Array& operator=( const Array & ); int size() const { return _size; } virtual void grow(); virtual void print( ostream& = cout ); Type at( int ix ) const { return ia[ ix ]; } virtual Type& operator[]( int ix ) { return ia[ix]; } virtual void sort( int,int ); virtual int find( Type ); virtual Type min(); virtual Type max(); protected: void swap( int, int ); void init( const Type*, int ); int _size; Type *ia; }; |
int find( const Array< int > &ia, int value ) { for ( int ix = 0; ix < ia.size(); ++ix ) // а теперь вызов виртуальной функции if ( ia[ ix ] == value ) return ix; return -1; |
Описывая операции инкремента и декремента, для иллюстрации применения их префиксной и постфиксной формы мы ввели понятие стека. Данная глава завершается примером реализации класса iStack – стека, позволяющего хранить элементы типа int.
Как уже было сказано, с этой структурой возможны две основные операции – поместить элемент (push) и извлечь (pop) его. Другие операции позволяют получить информацию о текущем состоянии стека – пуст он (empty()) или полон (full()), сколько элементов в нем содержится (size()). Для начала наш стек будет предназначен лишь для элементов типа int. Вот объявление нашего класса:
|
#include <vector> class iStack { public: iStack( int capacity ) : _stack( capacity ), _top( 0 ) {} bool pop( int &va1ue ); boot push( int value ); bool full(); bool empty(); void display(); int size(); private: int _top; vector< int > _stack; |
};
В данном случае мы используем вектор фиксированного размера: для иллюстрации использования префиксных и постфиксных операций инкремента и декремента этого достаточно. (В главе 6 мы модифицируем наш стек, придав ему возможность динамически меняться.)
Элементы стека хранятся в векторе _stack. Переменная _top
содержит индекс первой свободной ячейки стека. Этот индекс одновременно представляет количество заполненных ячеек. Отсюда реализация функции size(): она должна просто возвращать текущее значение _top.
inline int iStack::size() { return _top; };
empty()
возвращает true, если _top
равняется 0; full()
возвращает true, если _top
равен _stack.size()-1
(напомним, что индексация вектора начинается с 0, поэтому мы должны вычесть 1).
|
inline bool iStack::empty() { return _top ? false : true; } inline bool iStack::full() { return _top < _stack.size()-l ? false : true; |
}
Вот реализация функций pop() и push(). Мы добавили операторы вывода в каждую из них, чтобы следить за ходом выполнения:
bool iStack::pop( int &top_va1ue ) { if ( empty() ) return false; top_value = _stack[ --_top ]; cout << "iStack::pop(): " << top_value << endl; return true; } bool iStack::push( int value ) { cout << "iStack::push( " << value << " )\n"; if ( full() ) return false; _stack[ _top++ ] = value; return true; |
void iStack::display() { cout << "( " << size() << " )( bot: "; for ( int ix = 0; ix < _top; ++ix ) cout << _stack[ ix ] << " "; cout << " :top )\n"; |
#inc1ude <iostream> #inc1ude "iStack.h" int main() { iStack stack( 32 ) ; stack.display(); for ( int ix = 1; ix < 51; ++ix ) { if ( ix%2 == 0 ) stack.push( ix ); if ( ix%5 == 0 ) stack.display(); if ( ix%10 == 0 ) { int dummy; stack.pop( dummy ); stack.pop( dummy ); stack.display(); } |
В этом разделе приводится пример, показывающий, как можно определять и использовать шаблоны функций. Здесь определяется шаблон sort(), который затем применяется для сортировки элементов массива. Сам массив представлен шаблоном класса Array
(см. раздел 2.5). Таким образом, шаблоном sort() можно пользоваться для сортировки массивов элементов любого типа.
В главе 6 мы видели, что в стандартной библиотеке C++ определен контейнерный тип vector, который ведет себя во многом аналогично типу Array. В главе 12 рассматриваются обобщенные алгоритмы, способные манипулировать контейнерами, описанными в главе 6. Один из таких алгоритмов, sort(), служит для сортировки содержимого вектора. В этом разделе мы определим собственный “обобщенный алгоритм sort()” для манипулирования классом Array, упрощенной версии алгоритма из стандартной библиотеки C++.
Шаблон функции sort() для шаблона класса Array
определен следующим образом:
|
template <class elemType> void sort( Array<elemType> &array, int low, int high ) { if ( low < high ) { int lo = low; int hi = high + 1; elemType elem = array[lo]; for (;;) { while ( min( array[++lo], elem ) != elem && lo < high ) ; while ( min( array[--hi], elem ) == elem && hi > low ) ; if (lo < hi) swap( array, lo, hi ); else break; } swap( array, low, hi ); sort( array, low, hi-1 ); sort( array, hi+1, high ); } |
}
В sort()
используются две вспомогательные функции: min() и swap(). Обе они должны определяться как шаблоны, чтобы иметь возможность обрабатывать любые типы фактических аргументов, с которыми может быть конкретизирован шаблон sort(). min()
определена как шаблон функции для поиска минимального из двух значений любого типа:
|
template <class Type> Type min( Type a, Type b ) { return a < b ? a : b; |
}
swap() – шаблон функции для перестановки двух элементов массива любого типа:
template <class elemType> void swap( Array<elemType> &array, int i, int j ) { elemType tmp = array[ i ]; array[ i ] = array[ j ]; array[ j ] = tmp; |
#include <iostream> template <class elemType> void display( Array<elemType> &array ) { //формат отображения: < 0 1 2 3 4 5 > cout << "< "; for ( int ix = 0; ix < array.size(); ++ix ) cout << array[ix] << " "; cout << ">\n"; |
#include <iostream> #include <string> #include "Array.h" double da[10] = { 26.7, 5.7, 37.7, 1.7, 61.7, 11.7, 59.7, 15.7, 48.7, 19.7 }; int ia[16] = { 503, 87, 512, 61, 908, 170, 897, 275, 653, 426, 154, 509, 612, 677, 765, 703 }; string sa[11] = { "a", "heavy", "snow", "was", "falling", "when", "they", "left", "the", "police", "station" }; int main() { // вызвать конструктор для инициализации arrd Array<double> arrd( da, sizeof(da)/sizeof(da[0]) ); // вызвать конструктор для инициализации arri Array<int> arri( ia, sizeof(ia)/sizeof(ia[0]) ); // вызвать конструктор для инициализации arrs Array<string> arrs( sa, sizeof(sa)/sizeof(sa[0]) ); cout << "sort array of doubles (size == " << arrd.size() << ")" << endl; sort(arrd, 0, arrd.size()-1 ); display(arrd); cout << "sort array of ints (size == " << arri.size() << ")" << endl; sort(arri, 0, arri.size()-1 ); display(arri); cout << "sort array of strings (size == " << arrs.size() << ")" << endl; sort(arrs, 0, arrs.size()-1 ); display(arrs); return 0; |
Мы завершали главы 3 и 4 примерами для введения читателя в механизм классов С++. В конце этого раздела мы покажем, как разработать класс, представляющий собой односвязный список. (В главе 6 мы рассмотрим двусвязный список, являющийся частью стандартной библиотеки.) Если вы в первый раз читаете эту книгу, то можете пропустить данный раздел и вернуться к нему после чтения главы 13. (Для усвоения этого материала нужно представлять себе механизм классов С++, конструкторы, деструкторы и т.д. Если вы плохо знаете классы, но все же хотите продолжить чтение данного раздела, мы рекомендуем прочесть пункты 2.3 и 3.15.
Список представляет собой последовательность элементов, каждый из которых содержит значение некоторого типа и адрес следующего элемента (причем для последнего из них адрес может быть нулевым). К любой такой последовательности всегда можно добавить еще один элемент (хотя реальная попытка подобного добавления может закончиться неудачно, если отведенная программе свободная память исчерпана). Список, в котором нет ни одного элемента, называется пустым.
Какие операции должен поддерживать список? Добавление (insert), удаление (remove) и поиск (find) определенных элементов. Кроме того, можно запрашивать размер списка (size), распечатывать его содержимое (display), проверять равенство двух списков. Мы покажем также, как инвертировать (reverse) и сцеплять (concatenate) списки.
Простейшая реализация операции size()
перебирает все элементы, подсчитывая их количество. Более сложная реализация сохраняет размер как член данных; она намного эффективнее, однако требует некоторого усложнения операций insert() и remove() для поддержки размера в актуальном состоянии.
Мы выбрали второй вариант реализации функции size() и храним размер списка в члене данных. Мы предполагаем, что пользователи будут достаточно часто применять эту операцию, поэтому ее необходимо реализовать как можно более эффективно.
(Одним из преимуществ отделения открытого интерфейса от скрытой реализации является то, что если наше предположение окажется неверным, мы сможем переписать реализацию, сохранив открытый интерфейс – в данном случае тип возвращаемого значения и набор параметров функции size() – и программы, использующие эту функцию, не нужно будет модифицировать.)
insert_front( value ); |
remove( value ); remove_front(); |
class ilist_item; class ilist { public: // конструктор по умолчанию ilist() : _at_front( 0 ), _at_end( 0 ), _size( 0 ) {} // ... private: ilist_item *_at_front; ilist_item *_at_end; int _size; |
class ilist { public: // определения не показаны ilist(); int size(); // ... private: // запрещаем инициализацию // и присваивание одного списка другому ilist( const ilist& ); ilist& operator=( const ilist& ); // данные-члены без изменения |
int main() { ilist yourlist( mylist ); // ошибка mylist = mylist; // ошибка |
class ilist_item { public: // ... private: int _value; ilist_item *_next; |
class ilist_item { public: ilist_item( int value, ilist_-item *item_to_link_to = 0 ); // ... }; inline ilist_item:: ilist_item( int value, ilist_item *item ) : _value( value ) { if ( item ) _next = 0; else { _next = item->_next; item->_next = this; |
inline void ilist:: insert( ilist_item *ptr, int value ) { new ilist_item( value, ptr ); ++_size; |
#include <cstdlib> // ... if ( ! ptr ) |
#include <cassert> // ... |
if ( ! ptr ) |
if ( ! ptr ) |
inline void ilist::bump_up_size() { ++_size; } |
inline void ilist:: insert( ilist_item *ptr, int value ) if ( !ptr ) insert_front( value ); else { bump_up_size(); new ilist_item( value, ptr ); } |
inline void ilist:: insert_front( int value ) { ilist_item *ptr = new ilist_item( value ); if ( !_at_front ) _at_front = _at_end = ptr; else { ptr->next( _at_front ); _at_front = ptr; } bump_up_size(); } inl-ine void ilist:: insert_end( int value ) { if ( !_at_end ) _at_end = _at_front = new ilist_item( value ); else _at_end = new ilist_item( value, _at_end ); bump_up_s-ize(); |
ilist_item* ilist:: find( int value ) { ilist_item *ptr = _at_front; while ( ptr ) { if ( ptr->value() == value ) break; ptr = ptr->next(); } return ptr; |
ilist_item *ptr = mylist.find( 8 ); |
// не работает правильно! for ( ilist_item *iter = _at_front; // начнем с первого iter != _at_end; // пока не последний ++iter ) // возьмем следующий cout << iter->value() << ' '; // теперь напечатаем последний |
class ilist_item { public: ilist_item( int value, ilist_item *item_to_link_to = 0 ); int value() { return _value; } iilst_item* next() { return _next; } void next( ilist_item *link ) { _next = link; } void value( int new_value ) { _value = new_value; } private: int _value; ilist_item *_next; |
#include <iostream> class ilist { public: void display( ostream &os = cout ); // ... }; void ilist:: display( ostream &os ) { os << "\n( " << _size << " )( "; ilist_item *ptr = _at_front; while ( ptr ) { os << ptr->value() << " "; ptr = ptr->next(); } os << ")\n"; |
#include <iostream> #include "ilist.h" int main() { ilist mylist; for ( int ix = 0; ix < 10; ++ix ) { mylist.insert_front( ix ); mylist.insert_end( ix ); } cout << "Ok: после insert_front() и insert_end()\n"; mylist.display(); ilist_item *it = mylist.find( 8 ); cout << "\n" << "Ищем значение 8: нашли?" << ( it ? " да!\n" : " нет!\n" ); mylist.insert( it, 1024 ); cout << "\n" << "Вставка элемента 1024 после 8\n"; mylist.display(); int elem_cnt = mylist.remove( 8 ); cout << "\n" << "Удалено " << elem_cnt << " элемент(ов) со значением 8\n"; mylist.display(); cout << "\n" << "Удален первый элемент\n"; mylist.remove_front(); mylist.display(); cout << "\n" << "Удалены все элементы\n"; mylist.remove_all(); mylist.display(); |
void remove_front(); void remove_all (); |
inline void i1ist:: remove_front() { if ( _at_front ) { ilist_item *ptr = _at_front; _at_front = _at_front->next(); bump_down_size() ; delete ptr; } |
void ilist:: remove_all() { while ( _at_front ) remove_front(); _size = 0; _at_front = _at_end = 0; |
int ilist:: remove( int value ) { ilist_item *plist = _at_front; int elem_cnt = 0; while ( plist && plist->value() == value ) { plist = plist->next(); remove_front(); ++elem_cnt; } if ( ! plist ) return elem_cnt; ilist_item *prev = plist; plist = plist->next(); while ( plist ) { if ( plist->value() == value ) { prev->next( plist->next() ); delete plist; ++elem_cnt; bump_down_size(); plist = prev->next(); if ( ! plist ) { _at_end = prev; return elem_cnt; } } else { prev = plist; plist = plist->next(); } return elem_cnt; |
#include <iostream> #include "ilist.h" int main() { ilist mylist; cout << "\n-----------------------------------------------\n" << "тест #1: - элементы в конце\n" << "-----------------------------------------------\n"; mylist.insert_front( 1 ); mylist.insert_front( 1 ); mylist.insert_front( 1 ); my1ist.insert_front( 2 ); mylist.insert_front( 3 ); my1ist.insert_front( 4 ); mylist.display(); int elem_cnt = mylist.remove( 1 ); cout << "\n" << "Удалено " << elem_cnt << " элемент(ов) со значением 1\n"; mylist.display(); mylist.remove_all(); cout << "\n-----------------------------------------------\n" << "тест #2: - элементы в начале\n" << "-----------------------------------------------\n"; mylist.insert_front( 1 ); mylist.insert_front( 1 ); mylist.insert_front( 1 ); mylist.display(); elem_cnt = mylist.remove( 1 ); cout << "\n" << "Удалено " << elem_cnt << " элемент(ов) со значением 1\n"; mylist.display(); mylist.remove_all () ; cout << "\n-----------------------------------------------\n" << "тест #3: - элементов нет в списке\n" << "-----------------------------------------------\n"; mylist.insert_front( 0 ); mylist.insert_front( 2 ); mylist.insert_front( 4 ); mylist.display(); elem_cnt = mylist.remove( 1 ); cout << "\n" << "Удалено " << elem_cnt << " элемент(ов) со значением 1\n"; mylist.display(); mylist.remove_all () ; cout << "\n-----------------------------------------------\n" << "тест #4: - элементы в конце и в начале\n" << "-----------------------------------------------\n"; my1ist.insert_front( 1 ); mylist.insert_front( 1 ); my1ist.insert_front( 1 ); my1ist.insert_front( 0 ); mylist.insert_front( 2 ); my1ist.insert_front( 4 ); mylist.insert_front( 1 ); my1ist.insert_front( 1 ); mylist.insert_front( 1 ); mylist.display() ; elem_cnt = mylist.remove( 1 ); cout << "\n" << "Удалено " << elem_cnt << " элемент(ов) со значением 1\n"; mylist.display(); |
void ilist::concat( const ilist &i1 ) { if ( ! _at_end ) _at_front = i1._at_front; else _at_end->next( i1._at_front ); _at_end = i1._at_end; |
void ilist:: concat( const ilist &i1 ) { i1ist_item *ptr = i1._at_front; while ( ptr ) { insert_end( ptr->value() ); ptr = ptr->next(); } |
void ilist:: reverse() { ilist_item *ptr = _at_front; ilist_item *prev = 0; _at_front = _at_end; _at_end = ptr; while ( ptr != _at_front ) { ilist_item *tmp = ptr->next(); ptr->next( prev ); prev = ptr; ptr = tmp; } _at_front->next( prev ); |
#include <iostream> #include "ilist.h" int main() { ilist mylist; for ( int ix = 0; ix < 10; ++ix ) { mylist.insert_front( ix ); } mylist.display(); cout << "\n" << "инвертирование списка\n"; mylist.reverse(); mylist.display(); ilist mylist_too; mylist_too.insert_end(0); mylist_too.insert_end(1); mylist_too.insert_end(1); mylist_too.insert_end(2); mylist_too.insert_end(3); mylist_too.insert_end(5); cout << "\n" << "mylist_too:\n"; mylist_too.display(); mylist.concat( mylist_too ); cout << "\n" << "mylist после concat с mylist_too:\n"; mylist.disp1ay(); |
ilist::ilist( const ilist &rhs ) { ilist_item *pt = rhs._at_front; while ( pt ) { insert_end( pt->value() ); pt = pt->next(); } |
void ilist::insert_all ( const ilist &rhs ) { ilist_item *pt = rhs._at_front; while ( pt ) { insert_end( pt->value() ); pt = pt->next(); } |
inline ilist::ilist( const ilist &rhs ) : _at_front( 0 ), _at_end( 0 ) { insert_all ( rhs ); } inline ilist& ilist::operator=( const ilist &rhs ) { remove_all(); insert_all( rhs ); return *this; |
ilist_item *pt = mylist.front(); while ( pt ) { do_something( pt->value() ); pt = pt->next(); |
for ( ilist_item *iter = mylist.init_iter(); iter; iter = mylist.next_iter() ) |
class ilist { public: // ... init_iter( ilist_item *it = 0 ); private: //... ilist_item *_current; |
inline ilist_item* ilist::init_iter( i1ist_item *it ) { return _current = it ? it : _at_front; |
inline ilist_item* ilist:: next_iter() { ilist_item *next = _current ? _current = _current->next() : _current; return next; |
inline void ilist::remove_front() { if ( _at_front ) { ilist_item *ptr = _at_front; _at_front = _at_front->next(); // _current не должен указывать на удаленный элемент if ( _current == ptr ) _current = _at_front; bump_down_size(); delete ptr; } |
while ( plist ) { if ( plist->value() == value ) { prev->next( plist->next() ); if ( _current == plist ) |
#include <iostream> #include "ilist.h" int main() { ilist mylist; for ( int ix = 0; ix < 10; ++ix ) { mylist.insert_front( ix ); mylist.insert_end( ix ); } cout << "\n" << "Применение init_iter() и next_iter() " << "для обхода всех элементов списка:\n"; ilist_item *iter; for ( iter = mylist.init_iter(); iter; iter = mylist.next_iter() ) cout << iter->value() << " "; cout << "\n" << "Применение копирующего конструктора\n"; ilist mylist2( mylist ); mylist.remove_all(); for ( iter = mylist2.init_iter(); iter; iter = mylist2.next_iter() ) cout << iter->value() << " "; cout << "\n" << "Применение копирующего оператора присваивания\n"; mylist = mylist2; for ( iter = mylist.init_iter(); iter; iter = mylist.next_iter() ) cout << iter->value() << " "; cout << "\n"; |
Приоритеты операций задают последовательность вычислений в сложном выражении. Например, какое значение получит ival?
int ival = 6 + 3 * 4 / 2 + 2;
Если вычислять операции слева направо, получится 20. Среди других возможных результатов будут 9, 14 и 36. Правильный ответ: 14.
В С++ умножение и деление имеют более высокий приоритет, чем сложение, поэтому они будут вычислены раньше. Их собственные приоритеты равны, поэтому умножение и деление будут вычисляться слева направо. Таким образом, порядок вычисления данного выражения таков:
1. 3 * 4 => 12
2. 12 / 2 => 6
3. 6 + 6 => 12
4. 12 + 2 => 14
Следующая конструкция ведет себя не так, как можно было бы ожидать. Приоритет операции присваивания меньше, чем операции сравнения:
while ( ch = nextChar() != '\n' )
Программист хотел присвоить переменной ch
значение, а затем проверить, равно ли оно символу новой строки. Однако на самом деле выражение сначала сравнивает значение, полученное от nextChar(), с '\n', и результат – true или false – присваивает переменной ch.
Приоритеты операций можно изменить с помощью скобок. Выражения в скобках вычисляются в первую очередь. Например:
4 * 5 + 7 * 2 ==> 34
4 * ( 5 + 7 * 2 ) ==> 76
4 * ( (5 + 7) * 2 ) ==> 96
Вот как с помощью скобок исправить поведение предыдущего примера:
while ( (ch = nextChar()) != '\n' )
Операторы обладают и приоритетом, и ассоциативностью. Оператор присваивания правоассоциативен, поэтому вычисляется справа налево:
ival = jval = kva1 = lval
Сначала kval
получает значение lval, затем jval – значение результата этого присваивания, и в конце концов ival
получает значение jval .
Арифметические операции, наоборот, левоассоциативны. Следовательно, в выражении
ival + jval + kva1 + 1va1
сначала складываются ival и jval,
потом к результату прибавляется kval, а затем и lval.
В таблице 4.4 приведен полный список операторов С++ в порядке уменьшения их приоритета. Операторы внутри одной секции таблицы имеют равные приоритеты. Все операторы некоторой секции имеют более высокий приоритет, чем операторы из секций, следующих за ней. Так, операции умножения и деления имеют одинаковый приоритет, и он выше приоритета любой из операций сравнения.
|
(a) ! ptr == ptr->next (b) ~ uc ^ 0377 & ui << 4 |
| (a) int i = doSomething(), 0; |
|
Оператор |
Значение |
Использование |
|
|
:: |
Глобальная область видимости |
::name |
|
|
:: |
Область видимости класса |
class::name |
|
|
:: |
Область видимости пространства имен |
namespace::name |
|
|
. |
Доступ к члену |
object.member |
|
|
-> |
Доступ к члену по указателю |
pointer->member |
|
|
[] |
Взятие индекса |
variable[expr] |
|
|
() |
Вызов функции |
name(expr_list) |
|
|
() |
Построение значения |
type(expr_list) |
|
|
++ |
постфиксный инкремент |
lvalue++ |
|
|
-- |
постфиксный декремент |
lvalue-- |
|
|
typeid |
идентификатор типа |
typeid(type) |
|
|
typeid |
идентификатор типа выражения |
typeid(expr) |
|
|
const_cast |
преобразование типа |
const_cast<type>(expr) |
|
|
dynamic_cast |
преобразование типа |
dynamic_cast<type>(expr) |
|
|
reinterpret_cast |
приведение типа |
reinterpret_cast<type> (expr) |
|
|
static_cast |
приведение типа |
static_cast<type>(expr) |
|
|
sizeof |
размер объекта |
sizeof expr |
|
|
sizeof |
размер типа |
sizeof( type) |
|
|
++ |
префиксный инкремент |
++lvalue |
|
|
-- |
префиксный декремент |
--lvalue |
|
|
~ |
побитовое НЕ |
~expr |
|
|
! |
логическое НЕ |
!expr |
|
|
- |
унарный минус |
-expr |
|
|
+ |
унарный плюс |
+expr |
|
|
* |
разыменование |
*expr |
|
|
& |
адрес |
&expr |
|
|
() |
приведение типа |
(type)expr |
|
|
new |
выделение памяти |
new type |
|
|
new |
выделение памяти и инициализация |
new type(exprlist) |
|
|
new |
выделение памяти |
new (exprlist) type(exprlist) |
|
|
new |
выделение памяти под массив |
все формы |
|
|
delete |
освобождение памяти |
все формы |
|
|
delete |
освобождение памяти из-под массива |
все формы |
|
|
->* |
доступ к члену классу по указателю |
pointer-> *pointer_to_member |
|
|
.* |
доступ к члену класса по указателю |
object.*pointer_to_member |
|
|
* |
умножение |
expr * expr |
|
|
/ |
деление |
expr / expr |
|
|
% |
деление по модулю |
expr % expr |
|
|
+ |
сложение |
expr + expr |
|
|
- |
вычитание |
expr - expr |
|
|
<< |
сдвиг влево |
expr << expr |
|
|
>> |
сдвиг вправо |
expr >> expr |
|
|
< |
меньше |
expr < expr |
|
|
<= |
меньше или равно |
expr <= expr |
|
|
> |
больше |
expr > expr |
|
|
>= |
больше или равно |
expr >= expr |
|
|
== |
равно |
expr == expr |
|
|
!= |
не равно |
expr != expr |
|
|
& |
побитовое И |
expr & expr |
|
|
^ |
побитовое ИСКЛЮЧАЮЩЕЕ ИЛИ |
expr ^ expr |
|
|
| |
побитовое ИЛИ |
expr | expr |
|
|
&& |
логическое И |
expr && expr |
|
|
|| |
логическое ИЛИ |
expr || expr |
|
|
?: |
условный оператор |
expr ? expr * expr |
|
|
= |
присваивание |
l-значение = expr |
|
|
=, *=, /=, %=, +=, -=, <<=, >>=, &=, |=, ^= |
составное присваивание |
l-значение += expr и т.д. |
|
|
throw |
возбуждение исключения |
throw expr |
|
|
, |
запятая |
expr, expr |
Что происходит, если мы присваиваем один контейнер другому? Оператор присваивания копирует элементы из контейнера, стоящего справа, в контейнер, стоящий слева от знака равенства. А если эти контейнеры имеют разный размер? Например:
|
// svecl содержит 10 элементов // svec2 содержит 24 элемента // после присваивания оба содержат по 24 элемента |
svecl = svec2;
Контейнер-адресат (svec1) теперь содержит столько же элементов, сколько контейнер-источник (svec2). 10 элементов, изначально содержавшихся в svec1, удаляются (для каждого из них вызывается деструктор класса string).
Функция обмена swap()
может рассматриваться как дополнение к операции присваивания. Когда мы пишем:
svecl.swap( svec2 );
svec1
после вызова функции содержит 24 элемента, которые он получил бы в результате присваивания:
svecl = svec2;
но зато теперь svec2
получает 10 элементов, ранее находившихся в svec1. Контейнеры “обмениваются” своим содержимым.
Одной из проблем при разработке текстовых поисковых систем является необходимость распознавать слова в различных словоформах, такие, как cry, cries и cried, baby и babies, и, что гораздо проще, написанные заглавными и строчными буквами, например home и Home. Первая задача, распознавание словоформ, слишком сложна, поэтому мы приведем здесь ее заведомо неполное решение. Сначала заменим все прописные буквы строчными:
|
void strip_caps( vector<string,allocator> *words ) { vector<string,allocator>::iterator iter=words->begin() ; vector<string,allocator>::iterator iter_end=words->end() ; string caps( "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ); while ( iter != iter_end ) { string::size_type pos = 0; while (( pos = (*iter).find_first_of( caps, pos )) != string::npos ) (*iter)[ pos ] = to1ower( (*iter)[pos] ); ++iter; } |
}
Функция
to1ower( (*iter)[pos] );
входит в стандартную библиотеку С. Она заменяет прописную букву соответствующей ей строчной. Для использования tolower()
необходимо включить заголовочный файл:
#include <ctype.h>
(В этом файле объявлены и другие функции, такие, как isalpha(), isdigit(), ispunct(), isspace(), toupper(). Полное описание этих функций см. [PLAUGER92]. Стандартная библиотека С++ включает класс ctype, который инкапсулирует всю функциональность стандартной библиотеки Си, а также набор функций, не являющихся членами, например toupper(), tolower() и т.д. Для их использования нужно включить заголовочный файл
#include <locale>
Однако наша реализация компилятора еще не поддерживала класс ctype, и нам пришлось использовать стандартную библиотеку Си.)
Проблема словоформ слишком сложна для того, чтобы пытаться решить ее в общем виде. Но даже самый примитивный вариант способен значительно улучшить работу нашей поисковой системы. Все, что мы сделаем в данном направлении, – удалим букву 's' на концах слов:
|
void suffix_text( vector<string,allocator> *words ) { vector<string,allocator>::iterator iter = words->begin(), iter_end = words->end(); while ( iter != iter_end ) { // оставим слова короче трех букв как есть if ( (*iter).size() <= 3 ) { ++iter; continue; } if ( (*iter)[ (*iter).size()-1 ] == 's' ) suffix_s( *iter ); // здесь мы могли бы обработать суффиксы // ed, ing, 1y ++iter; } |
string::size_type pos() = word.size()-3; string ies( "ies" ); if ( ! word.compare( pos3, 3, ies )) { word.replace( pos3, 3, 1, 'у' ); return; |
string ses( "ses" ); if ( ! word.compare( pos3, 3, ses )) { word.erase( pos3+l, 2 ); return; |
string::size_type spos = 0; string::size_type pos3 = word.size()-3; // "ous", "ss", "is", "ius" string suffixes( "oussisius" ); if ( ! word.compare( pos3, 3, suffixes, spos, 3 ) || // ous ! word.compare( pos3, 3, suffixes, spos+6, 3 ) || // ius ! word.compare( pos3+l, 2, suffixes, spos+2, 2 ) || // ss ! word.compare( pos3+l, 2, suffixes, spos+4, 2 ) ) // is |
// удалим последнее 's' |
В С++ действие называется выражением, а
выражение, заканчивающееся точкой с запятой, – инструкцией.
Инструкция – это атомарная часть С++ программы, которой в программе на С++ соответствует предложение естественного языка. Вот примеры инструкций С++:
|
int book_count = 0; book_count = books_on_shelf + books_on_order; |
cout << "значение переменной book_count: " << book_count;
Первая из приведенных инструкций является инструкцией объявления. book_count
можно назвать идентификатором, символической переменной
(или просто переменной) или объектом. Переменной соответствует область в памяти компьютера, соотнесенная с определенным именем (в данном случае book_count), в которой хранится значение типа (в нашем случае целого). 0 – это константа. Переменная book_count инициализирована
значением 0.
Вторая инструкция – присваивание. Она помещает в область памяти, отведенную переменной book_count, результат сложения двух других переменных – books_on_shelf и books_on_order. Предполагается, что эти две целочисленные переменные определены где-то ранее в программе и им присвоены некоторые значения.
Третья инструкция является инструкцией вывода. cout – это выходной поток, направленный на терминал, << – оператор вывода. Эта инструкция выводит в cout – то есть на терминал – сначала символьную константу, заключенную в двойные кавычки ("значение переменной book_count:
"), затем значение, содержащееся в области памяти, отведенном под переменную book_count. В результате выполнения данной инструкции мы получим на терминале сообщение:
значение переменной book_count: 11273
если значение book_count
равно 11273 в данной точке выполнения программы.
Инструкции часто объединяются в именованные группы, называемые функциями. Так, группа инструкций, необходимых для чтения исходного файла, объединена в функцию readIn(). Аналогичным образом инструкции для выполнения оставшихся подзадач сгруппированы в функции sort(), compact() и print().
int main() { readIn(); sort(); compact(); print(); return 0; |
void readIn() { cout << "readIn()\n"; } void sort() { cout << "sort()\n"; } |
#include <iostream> using namespace std; void readIn() { cout << "readIn()\n"; } void sort() { cout << "sort()\n"; } void compact() { cout << "compact()\n"; } void print() { cout << "print ()\n"; } int main() { readIn(); sort(); compact(); print(); return 0; |
int main( { // ошибка – пропущена ')' readIn(): // ошибка – недопустимый символ ':' sort(); compact(); print(); return 0 // ошибка – пропущен символ ';' |
readIn() sort() compact() |
int age = 10; double price = 19.99; char delimiter = ' '; |
// заголовочный файл с определением типа string #include <string> string current_chapter = "Начинаем"; // заголовочный файл с определением типа vector #include <vector> |
Как и любое другое глобальное определение, шаблон функции может быть помещен в пространство имен (см. обсуждение пространств имен в разделах 8.5 и 8.6). Мы получили бы ту же семантику, если бы определили шаблон в глобальной области видимости, скрыв его имя внутри пространства имен. При использовании вне этого пространства необходимо либо квалифицировать имя шаблона именем пространства имен, либо использовать using-объявление:
|
// ---- primer.h ---- namespace cplusplus_primer { // определение шаблона скрыто в пространстве имен template <class Type> Type min( Type* array, int size ) { /* ... */ } } // ---- user.C ---- #include <primer.h> int ai[4] = { 12, 8, 73, 45 }; int main() { int size = sizeof(ai) / sizeof(ai[0]); // ошибка: функция min() не найдена min( &ai[0], size ); using cplusplus_primer::min; // using-объявление // правильно: относится к min() в пространстве имен cplusplus_primer min( &ai[0], size ); |
}
Что произойдет, если наша программа использует шаблон, определенный в пространстве имен, и мы хотим предоставить для него специализацию? (Явные специализации шаблонов рассматривались в разделе 10.6.) Допустим, мы хотим использовать шаблон min(), определенный в cplusplus_primer, для нахождения минимального значения в массиве объектов типа SmallInt. Однако мы осознаем, что имеющееся определение шаблона не вполне подходит, поскольку сравнение в нем выглядит так:
if ( array[i] < min_val )
В этой инструкции два объекта класса SmallInt
сравниваются с помощью оператора <. Но этот оператор неприменим к объектам, если только не перегружен в классе SmallInt (мы покажем, как определять перегруженные операторы в главе 15). Предположим, что мы хотели бы определить специализацию шаблона min(), чтобы она пользовалась функцией compareLess() для сравнения двух подобных объектов. Вот ее объявление:
|
// функция сравнения объектов SmallInt // возвращает true, если parm1 меньше parm2 |
class SmallInt { public: SmallInt( int ival ) : value( ival ) {} friend bool compareLess( const SmallInt &, const SmallInt & ); private: int value; // член |
// конструктор класса SmallInt |
// возвращает true, если parm1 меньше parm2 bool compareLess( const SmallInt &parm1, const SmallInt &parm2 ) { return parm1.value < parm2.value; |
// специализация min() для массива объектов SmallInt template<> SmallInt min<smallInt>( SmallInt* array, int size ) { SmallInt min_val = array[0]; for (int i = 1; i < size; ++i) // при сравнении используется функция compareLess() if ( compareLess( array[i], min_val ) ) min_val = array[i]; print( "Minimum value found: " ); print( min_val ); return min_val; |
// ---- primer.h ---- namespace cplusplus_primer { // определение шаблона скрыто в пространстве имен template <class Type> Type min( Type* array, int size ) { /* ... */ } } // ---- user.h ---- class SmallInt { /* ... */ }; void print( const SmallInt & ); bool compareLess( const SmallInt &, const SmallInt & ); // ---- user.C ---- #include <primer.h> #include "user.h" // ошибка: это не специализация для cplusplus_primer::min() template<> SmallInt min<smallInt>( SmallInt* array, int size ) { /* ... */ } |
// ---- user.C ---- #include <primer.h> #include "user.h" namespace cplusplus_primer { // специализация для cplusplus_primer::min() template<> SmallInt min<smallInt>( SmallInt* array, int size ) { /* ... */ } } SmallInt asi[4]; int main() { // задать значения элементов массива asi с помощью функции-члена set() using cplusplus_primer::min; // using-объявление int size = sizeof(asi) / sizeof(SmallInt); // конкретизируется min(SmallInt*,int) min( &asi[0], size ); |
// ---- user.C ---- #include <primer.h> #include "user.h" // специализация для cplusplus_primer::min() // имя специализации квалифицируется namespace { template<> SmallInt cplusplus_primer:: min<smallInt>( SmallInt* array, int size ) { /* ... */ } |
// функция сравнения объектов класса LongDouble // возвращает true, если parm1 больше parm2 bool compareGreater( const LongDouble &parm1, |
class LongDouble { public: LongDouble(double dval) : value(ival) {} friend bool compareGreater( const LongDouble &, const LongDouble & ); private: double value; |
Как и любое определение в глобальной области видимости, определение шаблона класса можно поместить внутрь пространства имен. (Пространства имен рассматривались в разделах 8.5 и 8.6.) Наш шаблон будет скрыт в данном пространстве имен; лишь в этом отличие от ситуации, когда шаблон определен в глобальной области видимости. При употреблении вне пространства имя шаблона следует либо квалифицировать его именем, либо воспользоваться using-объявлением:
|
#include <iostream> #include <cstdlib> namespace cplusplus_primer { template <class Type> class Queue { // ... }; template <class Type> Type Queue<Type>::remove() { // ... } |
}
Если имя Queue
шаблона класса используется вне пространства имен cplusplus_primer, то оно должно быть квалифицировано этим именем или введено с помощью using-объявления. Во всех остальных отношениях шаблон Queue используется так, как описано выше: конкретизируется, может иметь функции-члены, статические члены, вложенные типы и т.д. Например:
|
int main() { using cplusplus_primer Queue; // using-объявление // ссылается на шаблон класса в пространстве имен cplusplus_primer Queue<int> *p_qi = new Queue<int>; // ... p_qi->remove(); |
}
Шаблон cplusplus_primer::Queue<int>
конкретизируется, так как использован в выражении new:
... = new Queue<int>;
p_qi – это указатель на тип класса cplusplus_primer::Queue<int>. Когда он применяется для адресации функции-члена remove(), то речь идет о члене именно этого конкретизированного экземпляра класса.
Объявление шаблона класса в пространстве имен влияет также на объявления специализаций и частичных специализаций шаблона класса и его членов (см. разделы 16.9 и 16.10). Такая специализация должна быть объявлена в том же пространстве имен, где и общий шаблон.
В следующем примере в пространстве имен cplusplus_primer
объявляются специализации типа класса Queue<char *> и функции-члена remove()
#include <iostream> #include <cstdlib> namespace cplusplus_primer { template <class Type> class Queue { ... }; template <class Type> Type Queue<Type>::remove() { ... } // объявление специализации // для cplusplus_primer::Queue<char *> template<> class Queue<char*> { ... }; // объявление специализации // для функции-члена cplusplus_primer::Queue<double>::remove() template<> double Queue<double>::remove() { ... } |
namespace cplusplus_primer { // определение Queue и его функций-членов } // объявление специализации // cplusplus_primer::Queue<char*> template<> class cplusplus_primer::Queue<char*> { ... }; // объявление специализации функции-члена // cplusplus_primer::Queue<double>::remove() template<> double cplusplus_primer::Queue<double>::remove() |
Простейшей формой является пустая инструкция. Вот как она выглядит:
; // пустая инструкция
Пустая инструкция используется там, где синтаксис С++ требует употребления инструкции, а логика программы – нет. Например, в следующем цикле while, копирующем одну строку в другую, все необходимые действия производятся внутри круглых скобок (условной части инструкции). Однако согласно правилам синтаксиса С++ после while
должна идти инструкция. Поскольку нам нечего поместить сюда (вся работа уже выполнена), приходится оставить это место пустым:
|
while ( *string++ = inBuf++ ) |
; // пустая инструкция
Случайное появление лишней пустой инструкции не вызывает ошибки компиляции. Например, такая строка
ival = dval + sval;; // правильно: лишняя пустая инструкция
состоит из двух инструкций – сложения двух величин с присваиванием результата переменной ival и пустой.
Простая инструкция состоит из выражения, за которым следует точка с запятой. Например:
|
// простые инструкции int ival = 1024; // инструкция определения переменной ival; // выражение ival + 5; // еще одно выражение |
ival = ival +5; // присваивание
Условные инструкции и инструкции цикла синтаксически требуют употребления единственной инструкции, связанной с ними. Однако, как правило, этого недостаточно. В таких случаях употребляются составные инструкции – последовательность простых, заключенная в фигурные скобки:
|
if ( ival0 > ival1 ) { // составная инструкция, состоящая // из объявления и двух присваиваний int temp = ivalO; ivalO = ival1; ival1 = temp; |
}
Составная инструкция может употребляться там же, где простая, и не нуждается в завершающей точке с запятой.
Пустая составная инструкция эквивалентна пустой простой. Приведенный выше пример с пустой инструкцией можно переписать так:
|
while ( *string++ = *inBuf++ ) |
{} // пустая инструкция
Составную инструкцию, содержащую определения переменных, часто называют блоком. Блок задает локальную область видимости в программе – идентификаторы, объявленные внутри блока (как temp в предыдущем примере), видны только в нем. (Блоки, области видимости и время жизни объектов рассматриваются в главе 8.)
Прототип функции описывает ее интерфейс и состоит из типа возвращаемого функцией значения, имени и списка параметров. В данном разделе мы детально рассмотрим эти характеристики.
Функция gcd()
объявлена следующим образом:
int gcd( int, int );
Объявление говорит о том, что имеется два параметра типа int. Список формальных параметров предоставляет компилятору информацию, с помощью которой тот может проверить типы передаваемых функции фактических аргументов.
Что будет, если попытаться вызвать функцию gcd() с аргументами типа char*?
gcd( "hello", "world" );
А если передать этой функции не два аргумента, а только один? Или больше двух? Что случится, если потеряется запятая между числами 24 и 312?
gcd( 24312 );
Единственное разумное поведение компилятора– сообщение об ошибке, поскольку попытка выполнить такую программу чревата весьма серьезными последствиями. С++ действительно не пропустит подобные вызовы. Текст сообщения будет выглядеть примерно так:
// gcd( "hello", "world" )
error: invalid argument types ( const char *, const char * ) --
expecting ( int, int )
ошибка: неверные типы аргументов ( const char *, const char * ) --
ожидается ( int, int )
// gcd( 24312 )
error: missing value for second argument
ошибка: пропущено значение второго аргумента
А если вызвать эту функцию с аргументами типа double? Должен ли этот вызов расцениваться как ошибочный?
gcd( 3.14, 6.29 );
Как было сказано в разделе 4.14, значение типа double может быть преобразовано в int. Следовательно, считать такой вызов ошибочным было бы слишком сурово. Вместо этого аргументы неявно преобразуются в int (отбрасыванием дробной части) и таким образом требования, налагаемые на типы параметров, выполняются. Поскольку при подобном преобразовании возможна потеря точности, хороший компилятор выдаст предупреждение. Вызов превращается в
gcd( 3, 6 );
что дает в результате 3.
С++ является строго типизированным языком. Компилятор проверяет аргументы на соответствие типов в каждом вызове функции. Если тип фактического аргумента не соответствует типу формального параметра, то производится попытка неявного преобразования. Если же это оказывается невозможным или число аргументов неверно, компилятор выдает сообщение об ошибке. Именно поэтому функция должна быть объявлена до того, как программа впервые обратится к ней: без объявления компилятор не обладает информацией для проверки типов.
(a) set( int *, int ); (b) void func(); (c) string error( int ); |
double calc( double ); int count( const string &, char ); void sum( vector<int> &, int ); |
(a) calc( 23.4, 55.1 ); (b) count( "abcda", 'a' ); (c) sum( vec, 43.8 ); |
Псевдоним пространства имен
используется для задания короткого синонима имени пространства. Например, длинное имя
|
namespace International_Business_Machines |
{ /* ... */ }
может быть ассоциировано с более коротким синонимом:
namespace IBM = International_Business_Machines;
Объявление псевдонима начинается ключевым словом namespace, за которым следует короткий псевдоним, а за ним– знак равенства и исходное полное имя пространства. Если полное имя не соответствует никакому известному пространству, это ошибка.
Псевдоним может относиться и к вложенному пространству имен. Вспомним слишком длинное определение функции func()
выше:
|
#include "primer.h" // трудно читать! void func( cplusplus_primer::MatrixLib::matrix &m ) { // ... cplusplLis_primer::MatrixLib::inverse( m ); return m; |
}
Разрешается задать псевдоним для обозначения вложенного cplusplLis_primer::MatrixLib, сделав определение функции более удобным для восприятия:
|
#include "primer.h" // более короткий псевдоним namespace mlib = cplusplus_primer::MatrixLib; // читать проще! void func( mlib::matrix &m ) { // ... mlib::inverse( m ); return m; |
}
Одно пространство имен может иметь несколько взаимозаменяемых псевдонимов. Например, если псевдоним Lib
ссылается на cplusplus_primer, то определение функции func() может выглядеть и так:
|
// псевдоним alias относится к пространству имен cplusplus_primer namespace alias = Lib; void func( cplusplus_primer::matrix &m ) { // ... alias::inverse( m ); return m; |
}
К указателям на члены класса можно обращаться только с помощью конкретного объекта или указателя на объект типа класса. Для этого применяется любой из двух операторов доступа (.* для объектов класса и ссылок на них или ->* для указателей). Например, так вызывается функция-член через указатель на нее:
|
int (Screen::*pmfi)() = &Screen::height; Screen& (Screen::*pmfS)( const Screen& ) = &Screen::copy; Screen myScreen, *bufScreen; // прямой вызов функции-члена if ( myScreen.height() == bufScreen->height() ) bufScreen->copy( myScreen ); // эквивалентный вызов по указателю if ( (myScreen.*pmfi)() == (bufScreen->*pmfi)() ) |
(bufScreen->*pmfS)( myScreen );
Вызовы
|
(myScreen.*pmfi)() |
(bufScreen->*pmfi)();
требуют скобок, поскольку приоритет оператора вызова () выше, чем приоритет взятия указателя на функцию-член. Без скобок
myScreen.*pmfi()
интерпретируется как
myScreen.*(pmfi())
Это означает вызов функции pmfi() и привязку возвращенного ей значения к оператору (.*). Разумеется, тип pmfi не поддерживает такого использования, так что компилятор выдаст сообщение об ошибке.
Указатели на данные-члены используются аналогично:
|
typedef short Screen::*ps_Screen; Screen myScreen, *tmpScreen = new Screen( 10, 10 ); ps_Screen pH = &Screen::_height; ps_Screen pW = &Screen::_width; tmpScreen->*pH = myScreen.*pH; |
tmpScreen->*pW = myScreen.*pW;
Приведем реализацию функции-члена repeat(), которую мы обсуждали в начале этого раздела. Теперь она будет принимать указатель на функцию-член:
|
typedef Screen& (Screen::Action)(); Screen& Screen::repeat( Action op, int times ) { for ( int i = 0; i < times; ++i ) (this->*op)(); return *this; |
}
Параметр op – это указатель на функцию-член, которая должна вызываться times
раз.
Если бы нужно было задать значения аргументов по умолчанию, то объявление repeat() выглядело бы следующим образом:
class Screen { public: Screen &repeat( Action = &Screen::forward, int = 1 ); // ... |
Screen myScreen; myScreen.repeat(); // repeat( &Screen::forward, 1 ); |
Action::Menu() = { &Screen::home, &Screen::forward, &Screen::back, &Screen::up, &Screen::down, &Screen::end }; enum CursorMovements { HOME, FORWARD, BACK, UP, DOWN, END |
Screen& Screen::move( CursorMovements cm ) { ( this->*Menu[ cm ] )(); return *this; |
Фактический аргумент функции может быть неявно приведен к типу формального параметра с помощью последовательности определенных пользователем преобразований. Как это влияет на разрешение перегрузки? Например, если имеется следующий вызов calc(), то какая функция будет вызвана?
|
class SmallInt { public: SmallInt( int ); }; extern void calc( double ); extern void calc( SmallInt ); int ival; int main() { calc( ival ); // какая calc() вызывается? |
}
Выбирается функция, формальные параметры которой лучше всего соответствуют типам фактических аргументов. Она называется лучшим соответствием или наилучшей из устоявших функций. Для выбора такой функции неявные преобразования, примененные к фактическим аргументам, подвергаются ранжированию. Лучшей из устоявших считается та, для которой примененные к аргументам изменения не хуже, чем для любой другой устоявшей, а хотя бы для одного аргумента они лучше, чем для всех остальных функций.
Последовательность стандартных преобразований всегда лучше последовательности определенных пользователем преобразований. Так, при вызове calc() из примера выше обе функции calc() являются устоявшими. calc(double)
устояла потому, что существует стандартное преобразование типа фактического аргумента int в тип формального параметра double, а calc(SmallInt) – потому, что имеется определенное пользователем преобразование из int в SmallInt, которое использует конструктор SmallInt(int). Следовательно, наилучшей из устоявших функций будет calc(double).
А как сравниваются две последовательности определенных пользователем преобразований? Если в них используются разные конвертеры или разные конструкторы, то обе такие последовательности считаются одинаково хорошими:
|
class Number { public: operator SmallInt(); operator int(); // ... }; extern void calc( int ); extern void calc( SmallInt ); extern Number num; |
calc( num ); // ошибка: неоднозначность
Устоявшими окажутся и calc(int), и calc(SmallInt); первая – поскольку конвертер Number::operator int()преобразует фактический аргумент типа Number в формальный параметр типа int, а вторая потому, что конвертер Number::operator SmallInt() преобразует фактический аргумент типа Number в формальный параметр типа SmallInt. Так как последовательности определенных пользователем преобразований всегда имеют одинаковый ранг, то компилятор не может выбрать, какая из них лучше. Таким образом, этот вызов функции неоднозначен и приводит к ошибке компиляции.
// явное указание преобразования устраняет неоднозначность |
// определен только Number::operator SmallInt() |
class SmallInt { public: operator int(); // ... |
class SmallInt { public: operator int(); // ... |
void manip( int ); void manip( char ); SmallInt si ( 68 ); main() { manip( si ); // вызывается manip( int ) |
compute(float) : operator float()->точное соответствие |
istream& operator>>( istream &is, SmallInt &si ) { return ( is >> is.value ); |
class LongDouble { operator double(); operator float(); }; |
(a) int ex1 = ldObj; |
class LongDouble { public: LongDouble( double ); // ... }; extern void calc( int ); extern void calc( LongDouble ); double dval; int main() { calc( dval ); // какая функция? |
manip(int) : operator int()->точное соответствие |
class SmallInt { public: operator int(); operator float(); // ... |
void compute( float ); void compute( char ); SmallInt si ( 68 ); main() { compute( si ); // неоднозначность |
Поиск catch-обработчикадля возбужденного исключения происходит следующим образом. Когда выражение throw
находится в try-блоке, все ассоциированные с ним предложения catch
исследуются с точки зрения того, могут ли они обработать исключение. Если подходящее предложение catch найдено, то исключение обрабатывается. В противном случае поиск продолжается в вызывающей функции. Предположим, что вызов функции, выполнение которой прекратилось в результате исключения, погружен в try-блок; в такой ситуации исследуются все предложения catch, ассоциированные с этим блоком. Если один из них может обработать исключение, то процесс заканчивается. В противном случае переходим к следующей по порядку вызывающей функции. Этот поиск последовательно проводится во всей цепочке вложенных вызовов. Как только будет найдено подходящее предложение, управление передается в соответствующий обработчик.
В нашем примере первая функция, для которой нужен catch-обработчик,– это функция-член pop()
класса iStack. Поскольку выражение throw
внутри pop() не находится в try-блоке, то программа покидает pop(), не обработав исключение. Следующей рассматривается функция, вызвавшая pop(), то есть main(). Вызов pop()
внутри main()
находится в try-блоке, и далее исследуется, может ли хотя бы одно ассоциированное с ним предложение catch обработать исключение. Поскольку обработчик исключения popOnEmpty
имеется, то управление попадает в него.
Процесс, в результате которого программа последовательно покидает составные инструкции и определения функций в поисках предложения catch, способного обработать возникшее исключение, называется раскруткой стека. По мере раскрутки прекращают существование локальные объекты, объявленные в составных инструкциях и определениях функций, из которых произошел выход. C++ гарантирует, что во время описанного процесса вызываются деструкторы локальных объектов классов, хотя они исчезают из-за возбужденного исключения. (Подробнее мы поговорим об этом в главе 19.)
Если в программе нет предложения catch, способного обработать исключение, оно остается необработанным. Но исключение – это настолько серьезная ошибка, что программа не может продолжать выполнение. Поэтому, если обработчик не найден, вызывается функция terminate() из стандартной библиотеки C++. По умолчанию terminate()
Когда возбуждается исключение, поиск его catch-обработчика– раскрутка стека – начинается с функции, возбудившей исключение, и продолжается вверх по цепочке вложенных вызовов (см. раздел 11.3).
Во время раскрутки поочередно происходят аномальные выходы из просмотренных функций. Если функция захватила некоторый ресурс (например, открыла файл или выделила из хипа память), он в таком случае не освобождается.
Существует прием, позволяющий решить эту проблему. Всякий раз, когда во время поиска обработчика происходит выход из составной инструкции или блока, где определен некоторый локальный объект, для этого объекта автоматически вызывается деструктор. (Локальные объекты рассматривались в разделе 8.1.)
Например, следующий класс инкапсулирует выделение памяти для массива целых в конструкторе и ее освобождение в деструкторе:
|
class PTR { public: PTR() { ptr = new int[ chunk ]; } ~PTR { delete[] ptr; } private: int *ptr; |
};
Локальный объект такого типа создается в функции manip() перед вызовом mathFunc():
|
void manip( int parm ) { PTR localPtr; // ... mathFunc( parm ); // возбуждает исключение divideByZero // ... |
}
Если mathFunc()
возбуждает исключение типа divideByZero, то начинается раскрутка стека. В процессе поиска подходящего catch-обработчика проверяется и функция manip(). Поскольку вызов mathFunc() не заключен в try-блок, то manip() нужного обработчика не содержит. Поэтому стек раскручивается дальше по цепочке вызовов. Но перед выходом из manip() с необработанным исключением процесс раскрутки уничтожает все объекты типа классов, которые локальны в ней и были созданы до вызова mathFunc(). Таким образом, локальный объект localPtr уничтожается до того, как поиск пойдет дальше, а следовательно, память, на которую он указывает, будет освобождена и утечки не произойдет.
Поэтому говорят, что процесс обработки исключений в C++ поддерживает технику программирования, основной принцип которой можно сформулировать так: “захват ресурса – это инициализация; освобождение ресурса – это уничтожение”. Если ресурс реализован в виде класса и, значит, действия по его захвату сосредоточены в конструкторе, а действия по освобождению – в деструкторе (как,
например, в классе PTR
выше), то локальный для функции объект такого класса автоматически уничтожается при выходе из функции в результате необработанного исключения. Действия, которые должны быть выполнены для освобождения ресурса, не будут пропущены при раскрутке стека, если они инкапсулированы в деструкторы, вызываемые для локальных объектов.
Класс auto_ptr, определенный в стандартной библиотеке (см. раздел 8.4), ведет себя почти так же, как наш класс PTR. Это средство для инкапсуляции выделения памяти в конструкторе и ее освобождения в деструкторе. Если для выделения одиночного объекта из хипа используется auto_ptr, то гарантируется, что при выходе из составной инструкции или функции из-за необработанного исключения память будет освобождена.
Операторы присваивания, взятия адреса и оператор “запятая” имеют предопределенный смысл, если операндами являются объекты типа класса. Но их можно и перегружать. Семантика всех остальных операторов, когда они применяются к таким операндам, должна быть явно задана разработчиком. Выбор предоставляемых операторов зависит от ожидаемого использования класса.
Начинать следует с определения его открытого интерфейса. Набор открытых функций-членов формируется с учетом операций, которые класс должен предоставлять пользователям. Затем принимается решение, какие функции стоит реализовать в виде перегруженных операторов.
После определения открытого интерфейса класса проверьте, есть ли логическое соответствие между операциями и операторами:
· isEmpty()
становится оператором “ЛОГИЧЕСКОЕ НЕ”, operator!().
· isEqual()
становится оператором равенства, operator==().
· copy()
становится оператором присваивания, operator=().
У каждого оператора есть некоторая естественная семантика. Так, бинарный +
всегда ассоциируется со сложением, а его отображение на аналогичную операцию с классом может оказаться удобной и краткой нотацией. Например, для матричного типа сложение двух матриц является вполне подходящим расширением бинарного плюса.
Примером неправильного использования перегрузки операторов является определение operator+() как операции вычитания, что бессмысленно: не согласующаяся с интуицией семантика опасна.
Такой оператор одинаково хорошо поддерживает несколько различных интерпретаций. Безупречно четкое и обоснованное объяснение того, что делает operator+(), вряд ли устроит пользователей класса String, полагающих, что он служит для конкатенации строк. Если семантика перегруженного оператора неочевидна, то лучше его не предоставлять.
String s1( "C" ); String s2( "++" ); |
String != String String != С-строка |
Конечно, имена, используемые в области видимости класса, не обязаны быть именами членов класса. В процессе разрешения в этой области ведется поиск имен, объявленных и в других областях. Если имя, употребленное в области видимости класса, не разрешается именем члена класса, то компилятор ищет его в областях, включающих определение класса или члена. В этом подразделе мы покажем, как разрешаются имена, встречающиеся в области видимости класса.
Имя, использованное внутри определения класса (за исключением определений встроенных функций-членов и аргументов по умолчанию), разрешается следующим образом:
1. Просматриваются объявления членов класса, появляющиеся перед употреблением имени.
2. Если на шаге 1 разрешение не привело к успеху, то просматриваются объявления в пространстве имен перед определением класса. Напомним, что глобальная область видимости – это тоже область видимости пространства имен. (О пространствах имен речь шла в разделе 8.5.)
Например:
|
typedef double Money; class Account { // ... private: static Money _interestRate; static Money initInterest(); // ... |
};
Сначала компилятор ищет объявление Money в области видимости класса Account. При этом учитываются только те объявления, которые встречаются перед использованием Money. Поскольку таких объявлений нет, далее поиск ведется в глобальной области видимости. Объявление глобального typedef Money найдено, именно этот тип и используется в объявлениях _interestRate и initInterest().
Имя, встретившееся в определении функции-члена класса, разрешается следующим образом:
1. Сначала просматриваются объявления в локальных областях видимости функции-члена. (О локальных областях видимости и локальных объявлениях говорилось в разделе 8.1.)
2. Если шаг 1 не привел к успеху, то просматриваются объявления для всех членов класса.
3. Если и этого оказалось недостаточно, просматриваются объявления в пространстве имен перед определением функции-члена.
class Screen { public: // ... void setHeight( int ); private: short _height; }; int verify(int); void Screen::setHeight( int var ) { // var: относится к параметру // _height: относится к члену класса // verify: относится к глобальной функции _height = verify( var ); |
typedef int Type; Type initVal(); class Exercise { public: // ... typedef double Type; Type setVal( Type ); Type initVal(); private: int val; }; Type Exercise::setVal( Type parm ) { val = parm + initVal(); |
Посмотрим, как разрешаются имена в определениях вложенного класса и его членов.
Имя, встречающееся в определении вложенного класса (кроме тех, которые употребляются во встроенных функциях-членах и аргументах по умолчанию) разрешается следующим образом:
1. Просматриваются члены вложенного класса, расположенные перед употреблением имени.
2. Если шаг 1 не привел к успеху, то просматриваются объявления членов объемлющего класса, расположенные перед употреблением имени.
3. Если и этого недостаточно, то просматриваются объявления, расположенные в области видимости пространства имен перед определением вложенного класса.
Например:
|
enum ListStatus { Good, Empty, Corrupted }; class List { public: // ... private: class ListItem { public: // Смотрим в: // 1) List::ListItem // 2) List // 3) глобальной области видимости ListStatus status; // относится к глобальному перечислению // ... }; // ... |
};
Сначала компилятор ищет объявление ListStatus в области видимости класса ListItem. Поскольку его там нет, поиск продолжается в области видимости List, а затем в глобальной. При этом во всех трех областях просматриваются только объявления, предшествующие использованию ListStatus. В конце концов находится глобальное объявление перечисления ListStatus – оно и будет типом, использованным в объявлении status.
Если вложенный класс ListItem
определен в глобальной области видимости, вне тела объемлющего класса List, то все члены List уже были объявлены:
|
class List { private: class ListItem { //... public: enum ListStatus { Good, Empty, Corrupted }; // ... }; class List::ListItem { public: // Смотрим в: // 1) List::ListItem // 2) List // 3) глобальной области видимости ListStatus status; // относится к глобальному перечислению // ... |
};
При разрешении имени ListStatus
class List { public: enum ListStatus { Good, Empty, Corrupted }; // ... private: class ListItem { public: void check_status(); ListStatus status; // правильно //... }; ListItem *list; }; int list = 0; void List::ListItem::check_status() { int value = list; // какой list? |
void List::ListItem::check_status() { int value = ::list; // правильно |
class List { public: // ... private: class ListItem { public: // ошибка: нет видимого объявления для ::list void check_status() { int value = ::lis; } //... }; ListItem *list; // ... }; |
Внутри определения шаблона смысл некоторых конструкций может различаться в зависимости от конкретизации, тогда как смысл других всегда остается неизменным. Главную роль играет наличие в конструкции формального параметра шаблона:
|
template <typename Type> Type min( Type* array, int size ) { Type min_val = array[0]; for (int i = 1; i < size; ++i) if ( array[i] < min_val ) min_val = array[i]; print( "Minimum value found: "); print( min_val ); return min_val; |
}
В функции min()
типы переменных array и min_val
зависят от фактического типа, которым будет заменен Type при конкретизации шаблона, тогда как тип переменной size
останется int при любом типе параметра шаблона. Следовательно, типы array и min_val в разных конкретизациях различны. Поэтому мы говорим, что типы этих переменных зависят от параметра шаблона, тогда как тип size от него не зависит.
Так как тип min_val
неизвестен, то неизвестна и операция, которая будет использоваться при появлении min_val в выражении. Например, какая функция print() будет вызвана при обращении print(min_val)? С типом аргумента int? Или float? Будет ли вызов ошибочным, поскольку не существует функции, которая может быть вызвана с аргументом того же типа, что и min_val? Принимая все это во внимание, мы говорим, что и вызов print(min_val)
зависит от параметра шаблона.
Такие вопросы не возникают для тех конструкций внутри min(), которые не зависят от параметров шаблона. Например, всегда известно, какая функция должна быть вызвана для print( "Minimum value found: "). Это функция печати строк символов. В данном случае print()
остается одной и той же при любой конкретизации шаблона, то есть не зависит от его параметров.
В главе 7 мы видели, что в C++ функция должна быть объявлена до ее вызова. Нужно ли объявлять функцию, вызываемую внутри шаблона, до того, как компилятор увидит его определение? Должны ли мы объявить функцию print() в предыдущем примере до определения шаблона min()? Ответ зависит от особенностей имени, на которое мы ссылаемся. Конструкцию, не зависящую от параметров шаблона, следует объявить перед ее использованием в шаблоне. Представленное выше определение шаблона функции min() некорректно. Поскольку вызов
// ---- primer.h ---- template <typename Type> Type min( Type* array, int size ) { Type min_val = array[0]; // ... // ошибка: функция print( const char* ) не найдена print( "Minimum value found: " ); // правильно: зависит от параметра шаблона print( min_val ); // ... } // ---- user.C ---- #include <primer.h> // это объявление print( const char* ) игнорируется void print( const char* ); void print( int ); int ai[4] = {12, 8, 73, 45 }; int main() { int size = sizeof(ai) / sizeof(int); // конкретизируется min( int*, int ) min( &ai[0], size ); |
// ... int main() { // ... // использование min(SmallInt*,int) min( &asi[0], size ); } // точка конкретизации min(SmallInt*,int) // как будто объявление конкретизированной функции выглядит так: SmallInt min( SmallInt* array, int size ) |
#include <primer.h> void another(); SmallInt asi[4]; int main() { // задать значения элементов массива asi int size = sizeof(asi) / sizeof(SmallInt); min( &asi[0], size ); another(); // ... } // точка конкретизации здесь? void another() { int size = sizeof(asi) / sizeof(SmallInt); min( &asi[0], size ); } |
#include <primer.h> // user.h содержит объявления, необходимые при конкретизации #include "user.h" void another(); SmallInt asi[4]; int main() { // ... } // первая точка конкретизации min(SmallInt*,int) void another() { // ... } |
// ---- exercise.h ---- void display( const void* ); typedef unsigned int SIZE; template <typename Type> Type max( Type* array, SIZE size ) { Type max_val = array[0]; for ( SIZE i = 1; i < size; ++i ) if ( array[i] > max_val ) max_val = array[i]; display( "Maximum value found: " ); display( max_val ); return max_val; } // ---- user.h ---- class LongDouble { /* ... */ }; void display( const LongDouble & ); void display( const char * ); typedef int SIZE; // ---- user.C ---- #include <exercize.h> #include "user.h" LongDouble ad[7]; int main() { // задать значения элементов массива ad // конкретизируется max( LongDouble*, SIZE ) SIZE size = sizeof(ad) / sizeof(LongDouble); max( &ad[0], size ); |
При обсуждении разрешения имен в шаблонах функций (см. раздел 10.9) мы уже говорили о том, что этот процесс выполняется в два шага. Так же разрешаются имена и в определениях шаблонов классов и их членов. Каждый шаг относится к разным видам имен: первый– к тем, которые имеют один и тот же смысл во всех экземплярах шаблона, а второй – к тем, которые потенциально могут иметь разный смысл в разных экземплярах. Рассмотрим несколько примеров, где используется функция-член remove()
шаблона класса Queue:
|
// Queue.h: #include <iostream> #include <cstdlib> // определение класса Queue 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; cout << "удалено значение: "; cout << retval << endl; return retval; |
}
В выражении
cout << retval << endl;
переменная retval
имеет тип Type, и ее фактический тип неизвестен до конкретизации функции-члена remove(). То, какой оператор operator<<()
будет выбран, зависит от фактического типа retval, подставленного вместо Type. При разных конкретизациях remove() могут вызываться разные operator<<(). Поэтому мы говорим, что выбранный оператор вывода зависит от параметра шаблона.
Однако для вызова функции exit()
ситуация иная. Ее фактическим аргументом является литерал, значение которого одинаково при всех конкретизациях remove(). Поскольку при обращении к функции не используются аргументы, типы которых зависят от параметра шаблона Type, гарантируется, что всегда будет вызываться exit(), объявленная в заголовочном файле cstdlib. По той же причине в выражении
cout << "удалено значение: ";
всегда вызывается глобальный оператор
ostream& operator<<( ostream &, const char * );
#include "Queue.h" #include "ldouble.h" // содержит: // class LongDouble { ... }; // ostream& operator<<( ostream &, const LongDouble & ); int main() { // конкретизация Queue<LongDouble> Queue<LongDouble> *qld = new Queue<LongDouble>; // конкретизация Queue<LongDouble>::remove() // вызывает оператор вывода для LongDouble qld->remove(); // ... |
Функции-члены также могут быть перегружены, и в этом случае тоже применяется процедура разрешения перегрузки для выбора наилучшей из устоявших. Такое разрешение очень похоже на аналогичную процедуру для обычных функций и состоит из тех же трех шагов:
1. Отбор функций-кандидатов.
2. Отбор устоявших функций.
3. Выбор наилучшей из устоявших функции.
Однако есть небольшие различия в алгоритмах формирования множества кандидатов и отбора устоявших функций-членов. Эти различия мы и рассмотрим в настоящем разделе.
Наследование классов оказывает влияние на все аспекты разрешения перегрузки функций (см. раздел 9.2). Напомним, что эта процедура состоит из трех шагов:
1.
Отбор функций-кандидатов.
2. Отбор устоявших функций.
3. Выбор наилучшей из устоявших функции.
Отбор функций-кандидатов зависит от наследования потому, что на этом шаге принимаются во внимание функции, ассоциированные с базовыми классами, – как их функции-члены, так и функции, объявленные в тех же пространствах имен, где определены базовые классы. Отбор устоявших функций также зависит от наследования, поскольку множество преобразований формальных параметров функции в фактические аргументы расширяется пользовательскими преобразованиями. Кроме того, наследование оказывает влияние на ранжирование последовательностей трансформаций аргументов, а значит, и на выбор наилучшей из устоявших функции. В данном разделе мы рассмотрим влияние наследования на эти три шага разрешения перегрузки более подробно.
В классах могут быть объявлены перегруженные операторы и конвертеры. Предположим, при инициализации встретился оператор сложения:
|
SomeClass sc; |
int iobj = sc + 3;
Как компилятор решает, что следует сделать: вызвать перегруженный оператор для класса SomeClass или конвертировать операнд sc во встроенный тип, а затем уже воспользоваться встроенным оператором?
Ответ зависит от множества перегруженных операторов и конвертеров, определенных в SomeClass. При выборе оператора для выполнения сложения применяется процесс разрешения перегрузки функции. В данном разделе мы расскажем, как этот процесс позволяет выбрать нужный оператор, когда операндами являются объекты типа класса.
При разрешении перегрузки используется все та же процедура из трех шагов, представленная в разделе 9.2:
1. Отбор функций-кандидатов.
2. Отбор устоявших функций.
3. Выбор наилучшей из устоявших функции.
Рассмотрим эти шаги более детально.
Разрешение перегрузки функции не применяется, если все операнды имеют встроенные типы. В таком случае гарантированно употребляется встроенный оператор. (Использование операторов с операндами встроенных типов описано в главе 4.) Например:
|
class SmallInt { public: SmallInt( int ); }; SmallInt operator+ ( const SmallInt &, const SmallInt & ); void func() { int i1, i2; int i3 = i1 + i2; |
}
Поскольку операнды i1 и i2
имеют тип int, а не тип класса, то при сложении используется встроенный оператор +. Перегруженный operator+(const SmallInt
&, const SmallInt &)
игнорируется, хотя операнды можно привести к типу SmallInt с помощью определенного пользователем преобразования в виде конструктора SmallInt(int). Описанный ниже процесс разрешения перегрузки в таких ситуациях не применяется.
Кроме того, разрешение перегрузки для операторов употребляется только в случае использования операторного синтаксиса:
|
void func() { SmallInt si(98); int iobj = 65; int res = si + iobj; // использован операторный синтаксис |
}
Если вместо этого использовать синтаксис вызова функции:
int res = operator+( si, iobj ); // синтаксис вызова функции
то применяется процедура разрешения перегрузки для функций в пространстве имен (см. раздел 15.10). Если же использован синтаксис вызова функции-члена:
|
// синтаксис вызова функции-члена |
int res = si.operator+( iobj );
то работает соответствующая процедура для функций-членов (см. раздел 15.11).