В предыдущем разделе мы видели, что шаблон функции может быть перегружен. Кроме того, допускается использование одного и того же имени для шаблона и обычной функции:
|
// шаблон функции template <class Type> Type sum( Type, int ) { /* ... */ } // обычная функция (не шаблон) |
double sum( double, double );
Когда программа обращается к sum(), вызов разрешается либо в пользу конкретизированного экземпляра шаблона, либо в пользу обычной функции – это зависит от того, какая функция лучше соответствует фактическим аргументам. (Для решения такой проблемы применяется процесс разрешения перегрузки, описанный в главе 9.) Рассмотрим следующий пример:
|
void calc( int ii, double dd ) { // что будет вызвано: конкретизированный экземпляр шаблона // или обычная функция? sum( dd, ii ); |
}
Будет ли при обращении к sum(dd,ii)
вызвана функция, конкретизированная из шаблона, или обычная функция? Чтобы ответить на этот вопрос, выполним по шагам процедуру разрешения перегрузки. Первый шаг заключается в построении множества функций-кандидатов состоящего из одноименных вызванной функций, объявления которых видны в точке вызова.
Если существует шаблон функции и на основе фактических аргументов вызова из него может быть конкретизирована функция, то она будет являться кандидатом. Так ли это на самом деле, зависит от результата процесса вывода аргументов шаблона. (Этот процесс описан в разделе 10.3.) В предыдущем примере для вывода значения аргумента Type
шаблона используется фактический аргумент функции dd. Тип выведенного аргумента оказывается равным double, и к множеству функций-кандидатов добавляется функция sum(double, int). Таким образом, для данного вызова имеются два кандидата: конкретизированная из шаблона функция sum(double, int) и обычная функция sum(double, double).
После того как функции, конкретизированные из шаблона, включены в множество кандидатов, процесс вывода аргументов шаблона продолжается как обычно.
Второй шаг процедуры разрешения перегрузки заключается в выборе устоявших функций из множества кандидатов. Напомним, что устоявшей называется функция, для которой существуют преобразования типов, приводящие каждый фактический аргумент функции к типу соответствующего формального параметра. (В разделе 9.3 описаны преобразования типов, применимые к фактическим аргументам функции.) Нужные трансформации существуют как для конкретизированной функции sum(double, int), так и для обычной функции sum(double, double). Следовательно, обе они являются устоявшими.
// шаблон функции template <class T> |
// определение шаблона функции template <class Type> Type sum( Type, int ) { /* ... */ } // явная специализация для Type == double template<> double sum<double>( double,int ); // обычная функция double sum( double, double ); void manip( int ii, double dd ) { // вызывается явная специализация шаблона sum<double>() sum( dd, ii ); |
// определение шаблона функции template <class Type> Type min( Type, Type ) { /* ... */ } // явная специализация для Type == double template<> double min<double>( double, double ); void manip( int ii, double dd ) { // ошибка: вывод аргументов шаблона неудачен, // нет функций-кандидатов для данного вызова min( dd, ii ); |
// объявление шаблона функции template <class T> T min( T, T ); // обычная функция min(int,int) |
int ai[4] = { 22, 33, 44, 55 }; int main() { // вызывается обычная функция min( int, int ) min( ai[0], 99 ); |
// шаблон функции template <class T> T min( T, T ) { ... } // это обычная функция, не определенная в программе int min( int, int ); int ai[4] = { 22, 33, 44, 55 }; int main() { // ошибка сборки: min( int, int ) не определена min( ai[0], 99 ); |
// определение шаблона функции template <class Type> Type min( Type t1, Type t2 ) { ... } int ai[4] = { 22, 33, 44, 55 }; short ss = 88; void call_instantiation() { // ошибка: для этого вызова нет функции-кандидата min( ai[0], ss ); } // обычная функция int min( int a1, int a2 ) { min<int>( a1, a2 ); } int main() { call_instantiation() { // вызывается обычная функция min( ai[0], ss ); |
template <class Type> Type max( Type, Type ) { ... } // обычная функция |
int main() { int ival; double dval; float fd; // ival, dval и fd присваиваются значения max( 0, ival ); max( 0.25, dval ); max( 0, fd ); |
template <class T> T max( T, T ) { ... } // две обычные функции char max( char, char ); |
int main() { float fd; // в пользу какой функции разрешается вызов? max( 0, fd ); |
template <class Type> Type max( Type, Type ) { ... } |
int main() { int ival; double dval; float fd; max( 0, ival ); max( 0.25, dval ); max( 0, fd ); |
int main() { // ... max( 0, 'j' ); |
int i; unsigned int ui; char str[24]; int ia[24]; template <class T> T calc( T*, int ); template <class T> T calc( T, T ); template<> chat calc( char*. int ); |
(a) cslc( str, 24 ); (d) calc( i, ui ); (b) calc( is, 24 ); (e) calc( ia, ui ); |
При реализации программы в разделе 12.2 нам уже приходилось определять ряд объектов-функций. В этом разделе мы изучим необходимые шаги и возможные вариации при определении класса объекта-функции. (В главе 13 определение класса рассматривается детально; в главе 15 обсуждается перегрузка операторов.)
В самой простой форме определение класса объекта-функции сводится к перегрузке оператора вызова. Вот, например, унарный объект-функция, определяющий, что некоторое значение меньше или равно 10:
|
// простейшая форма класса объекта-функции class less_equal_ten { public: bool operator() ( int val ) { return val <= 10; } |
};
Теперь такой объект-функцию можно использовать точно так же, как предопределенный. Вызов алгоритма count_if() с помощью нашего объекта-функции выглядит следующим образом:
count_if( vec.begin(), vec.end(), less_equal_ten() );
Разумеется, возможности этого класса весьма ограничены. Попробуем применить отрицатель, чтобы подсчитать, сколько в контейнере элементов, больших 10:
|
count_if( vec.begin(), vec.end(), |
not1(less_equal_then ()));
или обобщить реализацию, разрешив пользователю задавать значение, с которым надо сравнивать каждый элемент контейнера. Для этого достаточно ввести в класс член для хранения такого значения и реализовать конструктор, инициализирующий данный член указанной пользователем величиной:
|
class less_equal_value { public: less_equal_value( int val ) : _val( val ) {} bool operator() ( int val ) { return val <= _val; } private: int _val; |
};
Новый объект-функция применяется для задания произвольного целого значения. Например, при следующем вызове подсчитывается число элементов, меньших или равных 25:
count_if( vec.begin(), vec.end(), less_equal_value( 25 ));
Разрешается реализовать класс и без конструктора, если параметризовать его значением, с которым производится сравнение:
|
template < int _val > class less_equal_value { public: bool operator() ( int val ) { return val <= _val; } |
};
Вот как надо было бы вызвать такой класс для подсчета числа элементов, меньших или равных 25:
count_if( vec.begin(), vec.end(), less_equal_value<25>());
(Другие примеры определения собственных объектов-функций можно найти в Приложении.)
Упражнение 12.4
Используя предопределенные объекты-функции и адаптеры, создайте объекты-функции для решения следующих задач:
(a) Найти все значения, большие или равные 1024.
(b) Найти все строки, не равные "pooh".
(c) Умножить все значения на 2.
Упражнение 12.5
Определите объект-функцию для возврата среднего из трех объектов. Определите функцию для выполнения той же операции. Приведите примеры использования каждого объекта непосредственно и путем передачи его функции. Покажите, в чем сходство и различие этих решений.
Автоматические объекты, интенсивно используемые в функции, можно объявить с ключевым словом register, тогда компилятор будет их загружать в машинные регистры. Если же это невозможно, объекты останутся в основной памяти. Индексы массивов и указатели, встречающиеся в циклах, – хорошие кандидаты в регистровые объекты.
|
for ( register int ix =0; ix < sz; ++-ix ) // ... |
for ( register int *p = array ; p < arraySize; ++p ) // ...
Параметры также можно объявлять как регистровые переменные:
|
bool find( register int *pm, int Val ) { while ( *pm ) if ( *pm++ == Val ) return true; return false; |
}
Их активное использование может заметно увеличить скорость выполнения функции.
Указание ключевого слова register – только подсказка компилятору. Некоторые компиляторы игнорируют такой запрос, применяя специальные алгоритмы для определения наиболее подходящих кандидатов на размещение в свободных регистрах.
Поскольку компилятор учитывает архитектуру машины, на которой будет выполняться программа, он зачастую может принять более обоснованное решение об использовании машинных регистров.
Функция, которая прямо или косвенно вызывает сама себя, называется рекурсивной. Например:
| int rgcd( int vl, int v2 )
{ if ( v2 != 0 ) return rgcd( v2, vl%v2 ); return vl; |
}
Такая функция обязательно должна определять условие окончания, в противном случае рекурсия будет продолжаться бесконечно. Подобную ошибку так иногда и называют– бесконечная рекурсия. Для rgcd()
условием окончания является равенство нулю остатка.
Вызов
rgcd( 15, 123 );
возвращает 3 (см. табл. 7.1).
Таблица 7.1. Трассировка вызова rgcd (15,123)
| vl | v2 | return | |||
| 15 | 123 | rgcd(123,15) | |||
| 123 | 15 | rgcd(15,3) | |||
| 15 | 3 | rgcd(3,0) | |||
| 3 | 0 | 3 |
Последний вызов,
rgcd(3,0);
удовлетворяет условию окончания. Функция возвращает наибольший общий делитель, он же возвращается и каждым предшествующим вызовом. Говорят, что значение всплывает (percolates) вверх, пока управление не вернется в функцию, вызвавшую rgcd() в первый раз.
Рекурсивные функции обычно выполняются медленнее, чем их нерекурсивные (итеративные) аналоги. Это связано с затратами времени на вызов функции. Однако, как правило, они компактнее и понятнее.
Приведем пример. Факториалом числа n
является произведение натуральных чисел от 1 до n. Так, факториал 5
равен 120: 1 ´ 2 ´ 3 ´ 4 ´ 5 = 120.
Вычислять факториал удобно с помощью рекурсивной функции:
| unsigned long
factorial( int val ) { if ( val > 1 ) return val * factorial( val-1 ); return 1; |
}
Рекурсия обрывается по достижении val значения 1.
Упражнение 7.12
Перепишите factorial() как итеративную функцию.
Упражнение 7.13
Что произойдет, если условием окончания factorial() будет следующее:
if ( val != 0 )
Программы обычно пишутся для того, чтобы решить какую-то конкретную задачу. Например, книжный магазин ведет запись проданных книг. Регистрируется название книги и издательство, причем запись идет в том порядке, в каком книги продаются. Каждые две недели владелец магазина вручную подсчитывает количество проданных книг с одинаковым названием и количество проданных книг от каждого издателя. Этот список сортируется по издателям и используется для составления последующего заказа книг. Нас попросили написать программу для автоматизации этой деятельности.
Один из методов решения большой задачи состоит в разбиении ее на ряд задач поменьше. В идеале, с маленькими задачами легче справиться, а вместе они помогают одолеть большую. Если подзадачи все еще слишком сложны, мы, в свою очередь, разобьем их на еще меньшие, пока каждая из подзадач не будет решена. Такую стратегию называют пошаговой детализацией или принципом “разделяй и властвуй”. Задача книжного магазина делится на четыре подзадачи:
Прочитать файл с записями о продажах.
Подсчитать количество продаж по названиям и по издателям.
Отсортировать записи по издателям.
Вывести результаты.
Решения для подзадач 1, 2 и 4 известны, их не нужно делить на более мелкие подзадачи. А вот третья подзадача все еще слишком сложна. Будем дробить ее дальше.
3a. Отсортировать записи по издателям.
3b. Для каждого издателя отсортировать записи по названиям.
3c. Сравнить соседние записи в группе каждого издателя. Для каждой одинаковой пары увеличить счетчик для первой записи и удалить вторую.
Эти подзадачи решаются легко. Теперь мы знаем, как решить исходную, большую задачу. Более того, мы видим, что первоначальный список подзадач был не совсем правильным. Правильная последовательность действий такова:
Прочитать файл с записями о продажах.
Отсортировать этот файл: сначала по издателям, внутри каждого издателя– по названиям.
Удалить повторяющиеся названия, наращивая счетчик.
Вывести результат в новый файл.
Результирующая последовательность действий называется алгоритмом. Следующий шаг – перевести наш алгоритм на некоторый язык программирования, в нашем случае – на С++.
void Query:: display() { if ( ! _solution->size() ) { cout << "\n\tИзвините, " << " подходящих строк в тексте не найдено.\n" << endl; } set<short>::const_iterator it = _solution->begin(), end_it = _solution->end(); for ( ; it != end_it; ++it ) { int line = *it; // не будем пользоваться нумерацией строк с 0... cout << "(" << line+1 << " ) " << (*_text_file)[line] << '\n'; } cout << endl; |
class Library { public: bool check_out( LibMember* ); // выдать bool check_in ( LibMember* ); // принять назад bool is_late( const Date& today ); // просрочил double apply_fine(); // наложить штраф ostream& print( ostream&=cout ); Date* due_date() const; // ожидаемая дата возврата Date* date_borrowed() const; // дата выдачи string title() const; // название const LibMember* member() const; // записавшийся |
class base { ... }; (a) class Derived : public Derived { ... }; (b) class Derived : Base { ... }; (c) class Derived : private Base { ... }; (d) class Derived : public Base; |
В стандартной библиотеке С++ auto_ptr
является шаблоном класса, призванным помочь программистам в манипулировании объектами, которые создаются посредством оператора new. (К сожалению, подобного шаблона для манипулирования динамическими массивами нет. Использовать auto_ptr для создания массивов нельзя, это приведет к непредсказуемым результатам.)
Объект auto_ptr
инициализируется адресом динамического объекта, созданного с помощью оператора new. Такой объект автоматически уничтожается, когда заканчивается время жизни auto_ptr. В этом подразделе мы расскажем, как ассоциировать auto_ptr с динамически размещаемыми объектами.
Для использования шаблона класса auto_ptr необходимо включить заголовочный файл:
#include <memory>
Определение объекта auto_ptr имеет три формы:
|
auto_ptr< type_pointed_to > identifier( ptr_allocated_by_new ); auto_ptr< type_pointed_to > identifier( auto_ptr_of_same_type ); |
auto_ptr< type_pointed_to > identifier;
Здесь type_pointed_to
представляет собой тип нужного объекта. Рассмотрим последовательно каждое из этих определений. Как правило, мы хотим непосредственно инициализировать объект auto_ptr
адресом объекта, созданного с помощью оператора new. Это можно сделать следующим образом:
auto_ptr< int > pi ( new int( 1024 ) );
В результате значением
pi
является адрес созданного объекта, инициализированного числом 1024. С объектом, на который указывает auto_ptr, можно работать обычным способом:
|
if ( *pi != 1024 ) // ошибка, что-то не так |
else *pi *= 2;
Объект, на который указывает pi, будет автоматически уничтожен по окончании времени жизни pi. Если указатель pi
является локальным, то объект, который он адресует, будет уничтожен при выходе из блока, где он определен. Если же pi глобальный, то объект, на который он ссылается, уничтожается при выходе из программы.
Что будет, если мы инициализируем auto_ptr
адресом объекта класса, скажем, стандартного класса string? Например:
|
auto_ptr< string > |
string *pstr_type = new string( "Brontosaurus" ); if ( pstr_type->empty() ) |
auto_ptr< string > pstr_auto( new string( "Brontosaurus" ) ); if ( pstr_type->empty() ) |
// кто несет ответственность за уничтожение строки? |
auto_ptr< int > p1( new int( 1024 ) ); |
// пока не ссылается ни на какой объект |
// ошибка: разыменование нулевого указателя if ( *p_auto_int != 1024 ) |
int *pi = 0; |
// проверяем, указывает ли p_auto_int на объект if ( p_auto_int.get() != 0 && *p_auto_int != 1024 ) |
else // хорошо, присвоим ему значение |
void example() { // инициализируется нулем по умолчанию auto_ptr< int > pi; { // не поддерживается pi = new int( 5 ) ; } |
auto_ptr< string > pstr_auto( new string( "Brontosaurus" ) ); // "Brontosaurus" уничтожается перед присваиванием |
// более эффективный способ присвоить новое значение // используем операцию assign() |
auto_ptr< string > pstr_auto( new string( "Brontosaurus" ) ); // ошибка: теперь оба указывают на один объект // и оба являются его владельцами |
// правильно: оба указывают на один объект, // но pstr_auto больше не является его владельцем auto_ptr< string > |
В этом разделе мы завершим реализацию шаблона класса Array, введенного в разделе 2.5 (этот шаблон будет распространен на одиночное наследование в разделе 18.3 и на множественное наследование в разделе 18.6). Так выглядит полный заголовочный файл:
|
#ifndef ARRAY_H #define ARRAY_H #include <iostream> template <class elemType> class Array; template <class elemType> ostream& operator<<( ostream &, Array<elemType> & ); template <class elemType> class Array { public: explicit Array( int sz = DefaultArraySize ) { init( 0, sz ); } Array( const elemType *ar, int sz ) { init( ar, sz ); } Array( const Array &iA ) { init( iA._ia, iA._size ); } ~Array() { delete[] _ia; } Array & operator=( const Array & ); int size() const { return _size; } elemType& operator[]( int ix ) const { return _ia[ix]; } ostream &print( ostream& os = cout ) const; void grow(); void sort( int,int ); int find( elemType ); elemType min(); elemType max(); private: void init( const elemType*, int ); void swap( int, int ); static const int DefaultArraySize = 12; int _size; elemType *_ia; }; |
#endif
Код, общий для реализации всех трех конструкторов, вынесен в отдельную функцию-член init(). Поскольку она не должна напрямую вызываться пользователями шаблона класса Array, мы поместили ее в закрытую секцию:
|
template <class elemType> void Array<elemType>::init( const elemType *array, int sz ) { _size = sz; _ia = new elemType[ _size ]; for ( int ix = 0; ix < _size; ++ix ) if ( ! array ) _ia[ ix ] = 0; else _ia[ ix ] = array[ ix ]; |
}
Реализация копирующего оператора присваивания не вызывает затруднений. Как отмечалось в разделе 14.7, в код включена защита от копирования объекта в самого себя:
template <class elemType> Array<elemType>& Array<elemType>::operator=( const Array<elemType> &iA ) { if ( this != &iA ) { delete[] _ia; init( iA._ia, iA._size ); } return *this; |
template <class elemType> ostream& operator<<( ostream &os, Array<elemType> &ar ) { return ar.print( os ); } template <class elemType> ostream & Array<elemType>::print( ostream &os ) const { const int lineLength = 12; os << "( " << _size << " )< "; for ( int ix = 0; ix < _size; ++ix ) { if ( ix % lineLength == 0 && ix ) os << "\n\t"; os << _ia[ ix ]; // не выводить запятую за последним элементом в строке, // а также за последним элементом массива if ( ix % lineLength != lineLength-1 && ix != _size-1 ) os << ", "; } os << " >\n"; return os; |
template <class elemType> void Array<elemType>::grow() { elemType *oldia = _ia; int oldSize = _size; _size = oldSize + oldSize/2 + 1; _ia = new elemType[_size]; int ix; for ( ix = 0; ix < oldSize; ++ix ) _ia[ix] = oldia[ix]; for ( ; ix < _size; ++ix ) _ia[ix] = elemType(); delete[] oldia; |
template <class elemType> elemType Array<elemType>::min( ) { assert( _ia != 0 ); elemType min_val = _ia[0]; for ( int ix = 1; ix < _size; ++ix ) if ( _ia[ix] < min_val ) min_val = _ia[ix]; return min_val; } template <class elemType> elemType Array<elemType>::max() { assert( _ia != 0 ); elemType max_val = _ia[0]; for ( int ix = 1; ix < _size; ++ix ) if ( max_val < _ia[ix] ) max_val = _ia[ix]; return max_val; } template <class elemType> int Array<elemType>::find( elemType val ) { for ( int ix = 0; ix < _size; ++ix ) if ( val == _ia[ix] ) return ix; return -1; |
template <class elemType> void Array<elemType>::swap( int i, int j ) { elemType tmp = _ia[i]; _ia[i] = _ia[j]; _ia[j] = tmp; } template <class elemType> void Array<elemType>::sort( int low, int high ) { if ( low >= high ) return; int lo = low; int hi = high + 1; elemType elem = _ia[low]; for ( ;; ) { while ( _ia[++lo] < elem ) ; while ( _ia[--hi] > elem ) ; if ( lo < hi ) swap( lo,hi ); else break; } swap( low, hi ); sort( low, hi-1 ); sort( hi+1, high ); |
#include "Array.h" template <class elemType> void try_array( Array<elemType> &iA ) { cout << "try_array: начальные значения массива\n"; cout << iA << endl; elemType find_val = iA [ iA.size()-1 ]; iA[ iA.size()-1 ] = iA.min(); int mid = iA.size()/2; iA[0] = iA.max(); iA[mid] = iA[0]; cout << "try_array: после присваиваний\n"; cout << iA << endl; Array<elemType> iA2 = iA; iA2[mid/2] = iA2[mid]; cout << "try_array: почленная инициализация\n"; cout << iA << endl; iA = iA2; cout << "try_array: после почленного копирования\n"; cout << iA << endl; iA.grow(); cout << "try_array: после вызова grow\n"; cout << iA << endl; int index = iA.find( find_val ); cout << "искомое значение: " << find_val; cout << "\tвозвращенный индекс: " << index << endl; elemType value = iA[index]; cout << "значение элемента с этим индексом: "; cout << value << endl; |
#include "Array.C" #include "try_array.C" #include <string> int main() { static int ia[] = { 12,7,14,9,128,17,6,3,27,5 }; static double da[] = { 12.3,7.9,14.6,9.8,128.0 }; static string sa[] = { "Eeyore", "Pooh", "Tigger", "Piglet", "Owl", "Gopher", "Heffalump" }; Array<int> iA( ia, sizeof(ia)/sizeof(int) ); Array<double> dA( da, sizeof(da)/sizeof(double) ); Array<string> sA( sa, sizeof(sa)/sizeof(string) ); cout << "template Array<int> class\n" << endl; try_array(iA); cout << "template Array<double> class\n" << endl; try_array(dA); cout << "template Array<string> class\n" << endl; try_array(sA); return 0; |
// правильно: автоматически преобразуется в Camera* OrthographicCamera ocam; lookAt( &ocam ); // ... // правильно: автоматически преобразуется в Camera* PerspectiveCamera *pcam = new PerspectiveCamera; |
Шаблон функции или класса может быть членом обычного класса или шаблона класса. Определение шаблона-члена похоже на определение шаблона: ему предшествует ключевое слово template, за которым идет список параметров:
|
template <class T> class Queue { private: // шаблон класса-члена template <class Type> class CL { Type member; T mem; }; // ... public: // шаблон функции-члена template <class Iter> void assign( Iter first, Iter last ) { while ( ! is_empty() ) remove(); // вызывается Queue<T>::remove() for ( ; first != last; ++first ) add( *first ); // вызывается Queue<T>::add( const T & ) } |
}
(Отметим, что шаблоны-члены не поддерживаются компиляторами, написанными до принятия стандарта C++. Эта возможность была добавлена в язык для поддержки реализации абстрактных контейнерных типов, представленных в главе 6.)
Объявление шаблона-члена имеет собственные параметры. Например, у шаблона класса CL
есть параметр Type, а у шаблона функции assign() – параметр Iter. Помимо этого, в определении шаблона-члена могут использоваться параметры объемлющего шаблона класса. Например, у шаблона CL
есть член типа T, представляющего параметр включающего шаблона Queue.
Объявление шаблона-члена в шаблоне класса Queue означает, что конкретизация Queue
потенциально может содержать бесконечное число различных вложенных классов CL
функций-членов assign(). Так, конкретизированный экземпляр Queue<int>
включает вложенные типы:
|
Queue<int>::CL<char> |
Queue<int>::CL<string>
и вложенные функции:
|
void Queue<int>::assign( int *, int * ) void Queue<int>::assign( vector<int>::iterator, |
vector<int>::iterator )
Для шаблона-члена действуют те же правила доступа, что и для других членов класса. Так как шаблон CL
является закрытым членом шаблона Queue, то лишь функции-члены и друзья Queue
int main() { // конкретизация Queue<int> Queue<int> qi; // конкретизация Queue<int>::assign( int *, int * ) int ai[4] = { 0, 3, 6, 9 }; qi.assign( ai, ai + 4 ); // конкретизация Queue<int>::assign( vector<int>::iterator, // vector<int>::iterator ) vector<int> vi( ai, ai + 4 ); qi.assign( vi.begin(), vi.end() ); |
template <class Iter> void assign( Iter first, Iter last ) { // удалить все элементы из очереди for ( ; first != last; ++first ) add( *first ); |
class SmallInt { public: SmallInt( int ival = 0 ) : value( ival ) { } // конвертер: SmallInt ==> int operator int() { return value; } // ... private: int value; }; int main() { // конкретизация Queue<int> Queue<int> qi; vector<SmallInt> vsi; // заполнить вектор // конкретизация // Queue<int>::assign( vector<SmallInt>::iterator, // vector<SmallInt>::iterator ) qi.assign( vsi.begin(), vsi.end() ); list<int*> lpi; // заполнить список // ошибка при конкретизации шаблона-члена assign(): // нет преобразования из int* в int qi.assign( lpi.begin(), lpi.end() ); |
template <class T> class Queue { // ... public: // шаблон-член конструктора template <class Iter> Queue( Iter first, Iter last ) : front( 0 ), back( 0 ) { for ( ; first != last; ++first ) add( * first ); } |
template <class T> class Queue { private: template <class Type> class CL; // ... public: template <class Iter> void assign( Iter first, Iter last ); // ... }; template <class T> template <class Type> class Queue<T>::CL<Type> { Type member; T mem; }; template <class T> template <class Iter> void Queue<T>::assign( Iter first, Iter last ) { while ( ! is_empty() ) remove(); for ( ; first != last; ++first ) add( *first ); |
void Queue<TT>::assign( IterType first, IterType last ) |
В этой главе рассказывается, что такое шаблон функции, как его определять и использовать. Это довольно просто, и многие программисты применяют шаблоны, определенные в стандартной библиотеке, даже не понимая, с чем они работают. Только пользователи, хорошо знающие язык С++, самостоятельно определяют и применяют шаблоны функций так, как здесь описано. Поэтому материал данной главы следует рассматривать как переход к более сложным аспектам C++. Мы начнем с рассказа о том, что такое шаблон функции и как его определять, затем на простом примере проиллюстрируем использование шаблонов. Далее мы перейдем к темам, требующим больших знаний. Сначала посмотрим на усложненные примеры применения шаблонов, затем подробно остановимся на выведении (deduction) их аргументов и покажем, как их можно задавать при конкретизации (instantiation) шаблона функции. После этого мы посмотрим, каким образом компилятор конкретизирует шаблоны и какие требования предъявляются в этой связи к организации наших программ, а также обсудим, как определить специализацию для такой конкретизации. Затем в данной главе будут изложены вопросы, представляющие интерес для проектировщиков шаблонов функций. Мы объясним, как можно перегружать шаблоны и как применительно к ним работает разрешение перегрузки. Мы также расскажем о разрешении имен в определениях шаблонов функций и покажем, как можно определять шаблоны в пространствах имен. Глава завершается развернутым примером.
В этой главе описывается, как определять и использовать шаблоны классов. Шаблон – это предписание для создания класса, в котором один или несколько типов либо значений параметризованы. Начинающий программист может использовать шаблоны, не понимая механизма, стоящего за их определениями и конкретизациями. Фактически на протяжении всей этой книги мы пользовались шаблонами классов, которые определены в стандартной библиотеке C++ (например, vector, list и т.д.), и при этом не нуждались в детальном объяснении механизма их работы. Только профессиональные программисты определяют собственные шаблоны классов и пользуются описанными в данной главе средствами. Поэтому этот материал следует рассматривать как введение в более сложные аспекты C++.
Глава 16 содержит вводные и продвинутые разделы. Во вводных разделах показано, как определяются шаблоны классов, иллюстрируются простые способы применения и обсуждается механизм их конкретизации. Мы расскажем, как можно задавать в шаблонах разные виды членов: функции-члены, статические данные-члены и вложенные типы. В продвинутых разделах представлен материал, необходимый для написания приложений промышленного уровня. Сначала мы рассмотрим, как компилятор конкретизирует шаблоны и какие требования в связи с этим предъявляются к организации
нашей программы. Затем покажем, как определять специализации и частичные специализации для шаблона класса и для его члена. Далее мы остановимся на двух вопросах, представляющих интерес для проектировщиков: как разрешаются имена в определениях шаблона класса и как можно определять шаблоны в пространствах имен. Завершается эта глава примером определения и использования шаблона класса.
Определение шаблона класса– это лишь предписание для построения бесконечного множества типов классов. Сам по себе шаблон не определяет никакого класса. Например, когда компилятор видит:
|
template <class Type> |
class Queue { ... };
он только сохраняет внутреннее представление Queue. Позже, когда встречается реальное использование класса, конкретизированного по шаблону, скажем:
|
int main() { Queue<int> *p_qi = new Queue<int>; |
}
компилятор конкретизирует тип класса Queue<int>, применяя сохраненное внутреннее представление определения шаблона Queue.
Шаблон конкретизируется только тогда, когда он употребляется в контексте, требующем полного определения класса. (Этот вопрос подробно обсуждался в разделе 16.2.) В примере выше класс Queue<int>
конкретизируется, потому что компилятор должен знать размер типа Queue<int>, чтобы выделить нужный объем памяти для объекта, созданного оператором new.
Компилятор может конкретизировать шаблон только тогда, когда он видел не только его объявление, но и фактическое определение, которое должно предшествовать тому месту программы, где этот шаблон используется:
|
// объявление шаблона класса template <class Type> class Queue; Queue<int>* global_pi = 0; // правильно: определение класса не нужно int main() { // ошибка: необходима конкретизация // определение шаблона класса должно быть видимо Queue<int> *p_qi = new Queue<int>; |
}
Шаблон класса можно конкретизировать одним и тем же типом в нескольких файлах. Как и в случае с типами классов, когда определение класса должно присутствовать в каждом файле, где используются его члены, компилятор конкретизирует шаблон некоторым типом во всех файлах, в которых данный экземпляр употребляется в контексте, требующем полного определения класса. Чтобы определение шаблона было доступно везде, где может понадобиться конкретизация, его следует поместить в заголовочный файл.
Функции-члены и статические данные-члены шаблонов классов, а также вложенные в них типы ведут себя почти так же, как сами шаблоны. Определения членов шаблона используются для порождения экземпляров членов в конкретизированном шаблоне. Если компилятор видит:
template <class Type> void Queue<Type>::add( const Type &val ) |
#include "Queue.h" int main() { // конкретизация Queue<int> Queue<int> *p_qi = new Queue<int>; int ival; // ... // конкретизация Queue<int>::add( const int & ) p_qi->add( ival ); // ... |
Библиотека iostream
сильно типизирована. Например, попытка прочитать из объекта класса ostream или записать в объект класса istream помечается компилятором как нарушение типизации. Так, если имеется набор объявлений:
|
#include <iostream> #include <fstream> class Screen; extern istream& operator>>( istream&, const Screen& ); extern void print( ostream& ); |
ifstream inFile;
то следующие две инструкции приводят к нарушению типизации, обнаруживаемому во время компиляции:
|
int main() { Screen myScreen; // ошибка: ожидается ostream& print( cin >> myScreen ); // ошибка: ожидается оператор >> |
inFile << "ошибка: оператор вывода";
Средства ввода/вывода включены в состав стандартной библиотеки C++. В главе 20 библиотека iostream
описана не полностью, в частности вопрос о создании определенных пользователем манипуляторов и буферных классов остался за рамками введения в язык. Мы сосредоточили внимание лишь на той части библиотеки iostream, которая имеет основополагающее значение для программного ввода/вывода.
Приложение
В систему текстового поиска входят текстовый файл, указанный пользователем, и средство для задания запроса, состоящего из слов и, возможно, логических операторов.
Если одно или несколько слов запроса найдены, печатается количество их вхождений. По желанию пользователя печатаются предложения, содержащие найденные слова. Например, если нужно найти все вхождения словосочетаний Civil War и Civil Rights, запрос может выглядеть таким образом[9]:
Civil && ( War || Rights )
Результат запроса:
Civil: 12 вхождений
War: 48 вхождений
Rights: 1 вхождение
Civil && War: 1 вхождение
Civil && Rights: 1 вхождение
(8) Civility, of course, is not to be confused with
Civil Rights, nor should it lead to Civil War
Здесь (8) представляет собой номер предложения в тексте. Наша система должна печатать фразы, содержащие найденные слова, в порядке возрастания их номеров (т.е. предложение номер 7 будет напечатано раньше предложения номер 9), не повторяя одну и ту же несколько раз.
Наша программа должна уметь:
· запросить имя текстового файла, а затем открыть и прочитать этот файл;
· организовать внутреннее представление этого файла так, чтобы впоследствии соотнести найденное слово с предложением, в котором оно встретилось, и определить порядковый номер этого слова ;
· понимать определенный язык запросов. В нашем случае он включает следующие операторы:
&& два слова непосредственно следуют одно за другим в строке
||
одно или оба слова встречаются в строке
!
слово не встречается в строке
()
группировка слов в запросе
Используя этот язык, можно написать:
Lincoln
чтобы найти все предложения, включающие слово Lincoln, или
Вот небольшая программа, иллюстрирующая построение отображения, поиск в нем и обход элементов. Здесь используются два отображения. Первое, необходимое для преобразования слов, содержит два элемента типа string. Ключом является слово, которое нуждается в специальной обработке, а значением– слово, заменяющее ключ. Для простоты мы задали пары ключ/значение непосредственно в тексте программы (вы можете модифицировать программу так, чтобы она читала их из стандартного ввода или из файла). Второе отображение используется для подсчета произведенных замен. Текст программы выглядит следующим образом:
|
#include <map> #include <vector> #include <iostream> #include <string> int main() { map< string, string > trans_map; typedef map< string, string >::value_type valType; // первое упрощение: // жестко заданный словарь trans_map.insert( va1Type( "gratz", "grateful" )); trans_map.insert( va1Type( "'em", "them" )); trans_map.insert( va1Type( "cuz", "because" )); trans_map.insert( va1Type( "nah", "no" )); trans_map.insert( va1Type( "sez", "says" )); trans_map.insert( va1Type( "tanx", "thanks" )); trans_map.insert( va1Type( "wuz", "was" )); trans_map.insert( va1Type( "pos", "suppose" )); // напечатаем словарь map< string,string >::iterator it; cout << "Наш словарь подстановок: \n\n"; for ( it = trans_map.begin(); it != trans_map.end(); ++it ) cout << "ключ: " << (*it).first << "\t" << "значение: " << ("it).second << "\n"; cout << "\n\n"; // второе упрощение: жестко заданный текст string textarray[14]={ "nah", "I", "sez", "tanx", "cuz", "I", "wuz", "pos", "to", "not", "cuz", "I", "wuz", "gratz" }; vector< string > text( textarray, textarray+14 ); vector< string >::iterator iter; // напечатаем текст cout << "Исходный вектор строк:\n\n"; int cnt = 1; for ( iter = text-begin(); iter != text.end(); ++iter,++cnt ) cout << *iter << ( cnt % 8 ? " " : "\n" ); cout << "\n\n\n"; // map для сбора статистики map< string,int > stats; typedef map< string,int >::value_type statsValType; // здесь происходит реальная работа for ( iter=text.begin(); iter != text.end(); ++iter ) if (( it = trans_map.find( *iter )) != trans_map.end() ) { if ( stats.count( *iter )) stats [ *iter ] += 1; else stats.insert( statsVa1Type( *iter, 1 )); *iter = (*it).second; } // напечатаем преобразованный текст cout << "Преобразованный вектор строк:\n\n"; cnt = 1; for ( iter = text.begin(); iter != text.end(); ++iter, ++cnt ) cout << *iter << ( cnt % 8 ? " " : "\n" ); cout << "\n\n\n"; // напечатаем статистику cout << "И напоследок статистика:\n\n"; map<string,int,less<string>,allocator>::iterator siter; for (siter=stats.begin(); siter!=stats.end(); ++siter) cout << (*siter).first << " " << "было заменено " << (*siter).second << (" раз(а)\n" ); |
}
Вот результат работы программы:
Наш словарь подстановок:
key: 'em value: them
key: cuz value: because
key: gratz value: grateful
key: nah value: no
key: pos value: suppose
key: sez value: says
key: tanx value: thanks
key: wuz value: was
Исходный вектор строк:
nah I sez tanx cuz I wuz pos
to not cuz I wuz gratz
Преобразованный вектор строк:
no I says thanks because I was suppose
to not because I was grateful
И напоследок статистика:
cuz было заменено 2 раз(а)
gratz было заменено 1 раз(а)
nah было заменено 1 раз(а)
pos было заменено 1 раз(а)
sez было заменено 1 раз(а)
tanx было заменено 1 раз(а)
wuz было заменено 2 раз(а)
Функция main() для нашего приложения текстового поиска выглядит следующим образом:
|
#include "TextQuery.h" int main() { TextQuery tq; tq.build_up_text(); tq.query_text(); |
}
Функция-член build_text_map() – это не что иное, как переименованная функция doit() из раздела 6.14:
|
inline void TextQuery:: build_text_map() { retrieve_text(); separate_words(); filter_text(); suffix_text(); strip_caps(); build_word_map(); |
}
Функция-член query_text()
заменяет одноименную функцию из раздела 6.14. В первоначальной реализации в ее обязанности входили прием запроса от пользователя и вывод ответа. Мы решили сохранить за query_text() эти задачи, но реализовать ее по-другому[19]:
|
void TextQuery::query_text() { /* локальные объекты: * * text: содержит все слова запроса * query_text: вектор для хранения пользовательского запроса * caps: фильтр для поддержки преобразования * прописных букв в строчные * * user_query: объект UserQuery, в котором инкапсулировано * собственно вычисление ответа на запрос */ string text; string caps( "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ); vector<string, allocator> query_text; UserQuery user_query; // инициализировать статические члены UserQuery NotQuery::all_locs( text_locations->second ); AndQuery::max_col( &line_cnt ); UserQuery::word_map( word_map ); do { // удалить предыдущий запрос, если он был query_text.clear(); cout << "Введите запрос. Пожалуйста, разделяйте все его " << "элементы пробелами.\n" << "Запрос (или весь сеанс) завершается точкой ( . ).\n\n" << "==> ";
/* * прочитать запрос из стандартного ввода, * преобразовать все заглавные буквы, после чего * упаковать его в query_text ... * * примечание: здесь производятся все действия по * обработке запроса, связанные собственно с текстом ... */ while( cin >> text ) { if ( text == "." ) break; string::size_type pos = 0; while (( pos = text.find_first_of( caps, pos )) != string::npos ) text[pos] = tolower( text[pos] ); query_text.push_back( text ); } // теперь у нас есть внутреннее представление запроса // обработаем его ... if ( ! query_text.empty() ) { // передать запрос объекту UserQuery user_query.query( &query_text ); // вычислить ответ на запрос // вернуть иерархию Query* // подробности см. в разделе 17.7 // query - это член класса TextQuery типа Query* query = user_query.eval_query(); // вычислить иерархию Query, // реализация описана в разделе 17.7 query->eval(); // вывести ответ с помощью // функции-члена класса TextQuery display_solution(); // вывести на терминал пользователя дополнительную // пустую строку cout << endl; } } while ( ! query_text.empty() ); cout << "До свидания!\n"; |
void TextQuery:: display_solution() { cout << "\n" << "Requested query: " << *query << "\n\n"; const set<short,less<short>,allocator> *solution = query->solution(); if ( ! solution->size() ) { cout << "\n\t" << "Sorry, no matching lines were found in text.\n" << endl; } set<short>::const_iterator it = solution->begin(), end_it = solution->end(); for ( ; it != end_it; ++it ) { int line = *it; // пронумеруем строки с 1 ... cout << "( " << line+1 << " ) " << (*lines_of_text)[line] << '\n'; } cout << endl; |
В общем случае объект класса эффективнее передавать функции по указателю или по ссылке, нежели по значению. Например, если дана функция с сигнатурой:
bool sufficient_funds( Account acct, double );
то при каждом ее вызове требуется выполнить почленную инициализацию формального параметра acct
значением фактического аргумента-объекта класса Account. Если же функция имеет любую из таких сигнатур:
|
bool sufficient_funds( Account *pacct, double ); |
bool sufficient_funds( Account &acct, double );
то достаточно скопировать адрес объекта Account. В этом случае никакой инициализации класса не происходит (см. обсуждение взаимосвязи между ссылочными и указательными параметрами в разделе 7.3).
Хотя возвращать указатель или ссылку на объект класса также более эффективно, чем сам объект, но корректно запрограммировать это достаточно сложно. Рассмотрим такой оператор сложения:
|
// задача решается, но для больших матриц эффективность может // оказаться неприемлемо низкой Matrix operator+( const Matrix& m1, const Matrix& m2 ) { Matrix result; // выполнить арифметические операции ... return result; |
}
Этот перегруженный оператор позволяет пользователю писать
|
Matrix a, b; // ... // в обоих случаях вызывается operator+() Matrix c = a + b; |
a = b + c;
Однако возврат результата по значению может потребовать слишком больших затрат времени и памяти, если Matrix
представляет собой большой и сложный класс. Если эта операция выполняется часто, то она, вероятно, резко снизит производительность.
Следующая пересмотренная реализация намного увеличивает скорость:
|
// более эффективно, но после возврата адрес оказывается недействительным // это может привести к краху программы Matrix& operator+( const Matrix& m1, const Matrix& m2 ) { Matrix result; // выполнить сложение ... return result; |
}
но при этом происходят частые сбои программы. Дело в том, что значение переменной result не определено после выхода из функции, в которой она объявлена. (Мы возвращаем ссылку на локальный объект, который после возврата не существует.)
// нет возможности гарантировать отсутствие утечки памяти // поскольку матрица может быть большой, утечки будут весьма заметными Matrix& operator+( const Matrix& m1, const Matrix& m2 ) { Matrix *result = new Matrix; // выполнить сложение ... return *result; |
// это обеспечивает нужную эффективность, // но не является интуитивно понятным для пользователя void mat_add( Matrix &result, const Matrix& m1, const Matrix& m3 ) { // вычислить результат |
// более не поддерживается |
// тоже не поддерживается |
Matrix& operator+( const Matrix& m1, const Matrix& m2 ) name result { Matrix result; // ... return result; |
// переписанная компилятором функция // в случае принятия предлагавшегося расширения языка void operator+( Matrix &result, const Matrix& m1, const Matrix& m2 ) name result { // вычислить результат |
Matrix c; |
classType functionName( paramList ) { classType namedResult; // выполнить какие-то действия ... return namedResult; |
void functionName( classType &namedResult, paramList ) { // вычислить результат и разместить его по адресу namedResult |
Matrix c; |
for ( int ix = 0; ix < size-2; ++ix ) { Matrix matSum = mat[ix] + mat[ix+1]; // ... |
Matrix matSum; for ( int ix = 0; ix < size-2; ++ix ) { matSum = mat[ix] + mat[ix+1]; // ... |
// Псевдокод на C++ Point3d p3; |
Point3d p3; |
// Псевдокод на C++ // небезопасно в случае присваивания |
Point3d p3; |
// Псевдокод на C++ Point3d temp; operator+( temp, p1, p2 ); p3.Point3d::operator=( temp ); |
Одна из проблем, вытекающих из возможности объявлять объект или функцию в разных файлах, – вероятность несоответствия объявлений или их расхождения в связи с модификацией программы. В С++ имеются средства, помогающие обнаружить такие различия.
Предположим, что в файле token.C
функция addToken()
определена как имеющая один параметр типа unsigned char. В файле lex.C, где эта функция вызывается, в ее определении указан параметр типа char.
|
// ---- в файле token.C ---- int addToken( unsigned char tok ) { /* ... */ } // ---- в файле lex.C ---- |
extern int addToken( char );
Вызов addToken() в файле lex.C
вызывает ошибку во время связывания программы. Если бы такое связывание прошло успешно, можно представить дальнейшее развитие событий: скомпилированная программа была протестирована на рабочей станции Sun Sparc, а затем перенесена на IBM 390. Первый же запуск потерпел неудачу: даже самые простые тесты не проходили. Что случилось?
Вот часть объявлений набора лексем:
|
const unsigned char INLINE = 128; |
const unsigned char VIRTUAL = 129;
Вызов addToken()
выглядит так:
|
curTok = INLINE; // ... |
addToken( curTok );
Тип char
реализован как знаковый в одном случае и как беззнаковый в другом. Неверное объявление addToken()
приводит к переполнению на той машине, где тип char
является знаковым, всякий раз, когда используется лексема со значением больше 127. Если бы такой программный код компилировался и связывался без ошибки, во время выполнения могли обнаружиться серьезные последствия.
В С++ информация о количестве и типах параметров функций помещается в имя функции – это называется безопасным связыванием (type-safe linkage). Оно помогает обнаружить расхождения в объявлениях функций в разных файлах. Поскольку типы параметров unsigned char и char
различны, в соответствии с принципом безопасного связывания функция addToken(), объявленная в файле lex.C, будет считаться неизвестной. Согласно стандарту определение в файле token.C
задает другую функцию.
// в token. C unsigned char lastTok = 0; unsigned char peekTok() { /* ... */ } // в lex.C extern char lastTok; |
Каждый объект класса из библиотеки iostream
поддерживает состояние формата, которое управляет выполнением операций форматирования, например основание системы счисления для целых значений или точность для значений с плавающей точкой. Для модификации состояния формата объекта в распоряжении программиста имеется предопределенный набор манипуляторов[O.A.6].1
Манипулятор применяется к потоковому объекту так же, как к данным. Однако вместо чтения или записи данных манипулятор модифицирует внутреннее состояние потока. Например, по умолчанию объект типа bool, имеющий значение true (а также литеральная константа true), выводится как целая ‘1’:
|
#include <iostream.h> int main() { bool illustrate = true; cout << "объект illustrate типа bool установлен в true: " << illustrate << '\n'; |
}
Чтобы поток cout
выводил переменную illustrate в виде слова true, мы применяем манипулятор boolalpha:
|
#include <iostream.h> int main() { bool illustrate = true; cout << "объект illustrate типа bool установлен в true: "; // изменяет состояние cout так, что булевские значения // печатаются в виде строк true и false cout << boolalpha; cout << illustrate << '\n'; |
}
Поскольку манипулятор возвращает потоковый объект, к которому он применялся, то допустимо прицеплять его к выводимым данным и другим манипуляторам. Вот как можно перемежать данные и манипуляторы в нашей программе:
|
#include <iostream.h> int main() { bool illustrate = true; cout << "объект illustrate типа bool: " << illustrate << "\nс использованием boolalpha: " << boolalpha << illustrate << '\n'; // ... |
}
Вывод данных и манипуляторов вперемежку может сбить пользователя с толку. Применение манипулятора изменяет не только представление следующего за ним объекта, но и внутреннее состояние потока. В нашем примере все значения типа bool в оставшейся части программы также будут выводиться в виде строк.
cout << boolalpha // устанавливает внутреннее состояние cout << illustrate |
#include <iostream> int main() { int ival = 16; double dval = 16.0; cout << "ival: " << ival << " установлен oct: " << oct << ival << "\n"; cout << "dval: " << dval << " установлен hex: " << hex << dval << "\n"; cout << "ival: " << ival << " установлен dec: " << dec << ival << "\n"; |
#include <iostream> int main() { int ival = 16; double dval = 16.0; cout << showbase; cout << "ival: " << ival << " установлен oct: " << oct << ival << "\n"; cout << "dval: " << dval << " установлен hex: " << hex << dval << "\n"; cout << "ival: " << ival << " установлен dec: " << dec << ival << "\n"; cout << noshowbase; |
#include <iostream> #include <iomanip> #include <math.h> int main() { cout << "Точность: " << cout.precision() << endl << sqrt(2.0) << endl; cout.precision(12); cout << "\nТочность: " << cout.precision() << endl << sqrt(2.0) << endl; cout << "\nТочность: " << setprecision(3) << cout.precision() << endl << sqrt(2.0) << endl; return 0; |
cout << showpoint << 10.0 |
cout << "научная: " << scientific << 10.0 << "с фиксированной точкой: " << fixed |
char ch; while ( cin >> ch ) |
char ch; cin >> noskipws; while ( cin >> ch ) // ... |
// сбрасывает буфер cout << "hi!" << flush; // вставляет нулевой символ, затем сбрасывает буфер char ch[2]; ch[0] = 'a'; ch[1] = 'b'; cout << ch << ends; // вставляет символ новой строки, затем сбрасывает буфер |
istream is; ostream new_os; // ... // tie() возвращает существующую связь ostream *old_tie = is.tie(); is.tie( 0 ); // разорвать существующую связь is.tie( &new_os ); // установить новую связь // ... is.tie( 0 ); // разорвать существующую связь |
|
#include <iostream> #include <iomanip> int main() { int ival = 16; double dval = 3.14159; cout << "ival: " << setw(12) << ival << '\n' << "dval: " << setw(12) << dval << '\n'; |
|
Манипулятор |
Назначение |
|
boolalpha |
Представлять true и false в виде строк |
|
*noboolalpha |
Представлять true и false как 1 и 0 |
|
Showbase |
Печатать префикс, обозначающий систему счисления |
|
*noshowbase |
Не печатать префикс системы счисления |
|
showpoint |
Всегда печатать десятичную точку |
|
*noshowpoint |
Печатать десятичную точку только в том случае, если дробная часть ненулевая |
|
showpos |
Печатать + для неотрицательных чисел |
|
*noshowpos |
Не печатать + для неотрицательных чисел |
|
Манипулятор |
Назначение |
|
*skipws |
Пропускать пробельные символы в операторах ввода |
|
noskipws |
Не пропускать пробельные символы в операторах ввода |
|
uppercase |
Печатать 0X при выводе в шестнадцатеричной системе счисления; E – при выводе в научной нотации |
|
*nouppercase |
Печатать 0x при выводе в шестнадцатеричной системе счисления; e – при выводе в научной нотации |
|
*dec |
Печатать в десятичной системе |
|
hex |
Печатать в шестнадцатеричной системе |
|
oct |
Печатать в восьмеричной системе |
|
left |
Добавлять символ заполнения справа от значения |
|
right |
Добавлять символ заполнения слева от значения |
|
internal |
Добавлять символ заполнения между знаком и значением |
|
*fixed |
Отображать число с плавающей точкой в десятичной нотации |
|
scientific |
Отображать число с плавающей точкой в научной нотации |
|
flush |
Сбросить буфер ostream |
|
ends |
Вставить нулевой символ, затем сбросить буфер ostream |
|
endl |
Вставить символ новой строки, затем сбросить буфер ostream |
|
ws |
Пропускать пробельные символы |
|
// для этих манипуляторов требуется #include <ionamip> |
|
|
setfill( ch) |
Заполнять пустое место символом ch |
|
Setprecision( n ) |
Установить точность вывода числа с плавающей точкой равной n |
|
setw( w ) |
Установить ширину поля ввода или вывода равной w |
|
setbase( b ) |
Выводить целые числа по основанию b |
|
* обозначает состояние потока по умолчанию |
Пользователей библиотеки iostream, разумеется, интересует, находится ли поток в ошибочном состоянии. Например, если мы пишем
|
int ival; |
cin >> ival;
и вводим слово "Borges", то cin
переводится в состояние ошибки после неудачной попытки присвоить строковый литерал целому числу. Если бы мы ввели число 1024, то чтение прошло бы успешно и поток остался бы в нормальном состоянии.
Чтобы выяснить, в каком состоянии находится поток, достаточно проверить его значение на истину:
|
if ( !cin ) |
// операция чтения не прошла или встретился конец файла
Для чтения заранее неизвестного количества элементов мы обычно пишем цикл while:
|
while ( cin >> word ) |
// операция чтения завершилась успешно ...
Условие в цикле while
будет равно false, если достигнут конец файла или произошла ошибка при чтении. В большинстве случаев такой проверки потокового объекта достаточно. Однако при реализации оператора ввода для класса WordCount из раздела 20.5 нам понадобился более точный анализ состояния.
У любого потока есть набор флагов, с помощью которых можно следить за состоянием потока. Имеются четыре предикатные функции-члена:
·
eof()
возвращает true, если достигнут конец файла:
|
if ( inOut.eof() ) |
// отлично: все прочитано ...
· bad()
возвращает true при попытке выполнения некорректной операции, например при установке позиции за концом файла. Обычно это свидетельствует о том, что поток находится в состоянии ошибки;
· fail()
возвращает true, если операция завершилась неудачно, например не удалось открыть файл или передан некорректный формат ввода:
|
ifstream iFile( filename, ios_base::in ); if ( iFile.fail() ) // не удалось открыть |
error_message( ... );
· good()
возвращает true, если все вышеперечисленные условия ложны:
if ( inOut.good() )
Существует два способа явно изменить состояние потока iostream. С помощью функции-члена clear() ему явно присваивается указанное значение. Функция setstate() не сбрасывает состояние, а устанавливает один из флагов, не меняя значения остальных. Например, в коде оператора ввода для класса WordCount при обнаружении неверного формата мы используем setstate() для установки флага fail в состоянии объекта istream:
if ((ch = is.get()) != '<' ) { is.setstate( ios_base::failbit ); return is; |
ios_base::badbit ios_base::eofbit ios_base::failbit |
if ( !cin ) { cerr << "Ошибка ввода WordCount" << endl; return -1; |
ios_base::iostate old_state = cin.rdstate(); cin.clear(); process_input(); // перевести поток cin в прежнее состояние |
Прежде чем приступать к рассмотрению специализаций шаблонов классов и причин, по которым в них может возникнуть надобность, добавим в шаблон Queue
функции-члены min() и max(). Они будут обходить все элементы очереди и искать среди них соответственно минимальное и максимальное значения (правильнее, конечно, использовать для этой цели обобщенные алгоритмы min() и max(), представленные в главе 12, но мы определим эти функции как члены шаблона Queue, чтобы познакомиться со специализациями.)
|
template <class Type> class Queue { // ... public: Type min(); Type max(); // ... }; // найти минимальное значение в очереди Queue template <class Type> Type Queue<Type>::min() { assert( ! is_empty() ); Type min_val = front->item; for ( QueueItem *pq = front->next; pq != 0; pq = pq->next ) if ( pq->item < min_val ) min_val = pq->item; return min_val; } // найти максимальное значение в очереди Queue template <class Type> Type Queue<Type>::max() { assert( ! is_empty() ); Type max_val = front->item; for ( QueueItem *pq = front->next; pq != 0; pq = pq->next ) if ( pq->item > max_val ) max_val = pq->item; return max_val; |
}
Следующая инструкция в функции-члене min()
сравнивает два элемента очереди Queue:
pq->item < min_val
Здесь неявно присутствует требование к типам, которыми может конкретизироваться шаблон класса Queue: такой тип должен либо иметь возможность пользоваться предопределенным оператором “меньше” для встроенных типов, либо быть классом, в котором определен оператор operator<(). Если же этого оператора нет, то попытка применить min() к очереди приведет к ошибке компиляции в том месте, где вызывается несуществующий оператор сравнения. (Аналогичная проблема существует и в max(), только касается оператора operator>()).
Предположим, что шаблон класса Queue
нужно конкретизировать таким типом:
class LongSouble { public: LongDouble( double dbval ) : value( dval ) { } bool compareLess( const LongDouble & ); private: double value; |
// определения явных специализаций template<> LongDouble Queue<LongDouble>::min() { assert( ! is_empty() ); LongDouble min_val = front->item; for ( QueueItem *pq = front->next; pq != 0; pq = pq->next ) if ( pq->item.compareLess( min_val ) ) min_val = pq->item; return min_val; } template<> LongDouble Queue<LongDouble>::max() { assert( ! is_empty() ); LongDouble max_val = front->item; for ( QueueItem *pq = front->next; pq != 0; pq = pq->next ) if ( max_val.compareLess( pq->item ) ) max_val = pq->item; return max_val; |
// объявления явных специализаций функций-членов template <> LongDouble Queue<LongDouble>::min(); |
// QueueLD.h: определяет специализацию класса Queue<LongDouble> #include "Queue.h" template<> Queue<LongDouble> { Queue<LongDouble>(); ~Queue<LongDouble>(); LongDouble& remove(); void add( const LongDouble & ); bool is_empty() const; LongDouble min(); LongDouble max(); private: // Некоторая реализация |
#include "QueueLD.h" // определяет функцию-член min() // из специализированного шаблона класса |
// ---- File1.C ---- #include "Queue.h" void ReadIn( Queue<LongDouble> *pq ) { // использование pq->add() // приводит к конкретизации QueueItem<LongDouble> } |
// ---- File2.C ---- #include "QueueLD.h" void ReadIn( Queue<LongDouble> * ); int main() { // используется определение специализации для Queue<LongDouble> Queue<LongDouble> *qld = new Queue<LongDouble>; ReadIn( qld ); // ... |
Наследование, в котором присутствует один или несколько виртуальных базовых классов, требует специальной семантики инициализации. Взгляните еще раз на реализации Bear и Raccoon в предыдущем разделе. Видите ли вы, какая проблема связана с порождением класса Panda?
|
class Panda : public Bear, public Raccoon, public Endangered { public: Panda( string name, bool onExhibit=true ); virtual ostream& print( ostream& ) const; bool sleeping() const { return _sleeping; } void sleeping( bool newval ) { _sleeping = newval; } // ... protected: bool _sleeping; // ... |
};
Проблема в том, что конструкторы базовых классов Bear и Raccoon вызывают конструктор ZooAnimal с неявным набором аргументов. Хуже того, в нашем примере значения по умолчанию для аргумента fam_name
(название семейства) не только отличаются, они еще и неверны для Panda.
В случае невиртуального наследования производный класс способен явно инициализировать только свои непосредственные базовые классы (см. раздел 17.4). Так, классу Panda, наследующему от ZooAnimal, не разрешается напрямую вызвать конструктор ZooAnimal в своем списке инициализации членов. Однако при виртуальном наследовании только Panda может напрямую вызывать конструктор своего виртуального базового класса ZooAnimal.
Ответственность за инициализацию виртуального базового возлагается на ближайший производный класс. Например, когда объявляется объект класса Bear:
Bear winnie( "pooh" );
то Bear
является ближайшим производным классом для объекта winnie, поэтому выполняется вызов конструктора ZooAnimal, определенный в классе Bear. Когда мы пишем:
cout << winnie.family_name();
будет выведена строка:
The family name for pooh is Bear
(Название семейства для pooh – это Bear)
Аналогично для объявления
Raccoon meeko( "meeko" );
Raccoon – это ближайший производный класс для объекта meeko, поэтому выполняется вызов конструктора ZooAnimal, определенный в классе Raccoon. Когда мы пишем:
Panda::Panda( string name, bool onExhibit=true ) : ZooAnimal( name, onExhibit, "Panda" ), Bear( name, onExhibit ), Raccoon( name, onExhibit ), Endangered( Endangered::environment, Endangered::critical ), sleeping( false ) |
class Bear : public virtual ZooAnimal { public: // если выступает в роли ближайшего производного класса Bear( string name, bool onExhibit=true ) : ZooAnimal( name, onExhibit, "Bear" ), _dance( two_left_feet ) {} // ... остальное без изменения protected: // если выступает в роли промежуточного производного класса Bear() : _dance( two_left_feet ) {} // ... остальное без изменения |
Panda::Panda( string name, bool onExhibit=true ) : ZooAnimal( name, onExhibit, "Panda" ), Endangered( Endangered::environment, Endangered::critical ), sleeping( false ) |
Существует специальная категория функций-членов, отвечающих за такие действия с объектами, как инициализация, присваивание, управление памятью, преобразование типов и уничтожение. Такие функции называются конструкторами. Они вызываются компилятором неявно каждый раз, когда объект класса определяется или создается оператором new. В объявлении конструктора его имя совпадает с именем класса. Вот, например, объявление конструктора класса Screen, в котором заданы значения по умолчанию для параметров hi, wid и bkground:
|
class Screen { public: Screen( int hi = 8, int wid = 40, char bkground = '#'); // объявления других функций-членов не изменяются |
};
Определение конструктора класса Screen
выглядит так:
|
Screen::Screen( int hi, int wid, char bk ) : _height( hi ), // инициализировать _height значением hi _width( wid ), // инициализировать _width значением wid _cursor ( 0 ), // инициализировать _cursor нулем _screen( hi * wid, bk ) // размер экрана равен hi * wid // все позиции инициализируются // символом '#' { // вся работа проделана в списке инициализации членов // этот список обсуждается в разделе 14.5 |
}
Каждый объявленный объект класса Screen
автоматически инициализируется конструктором:
|
Screen s1; // Screen(8,40,'#') Screen *ps = new Screen( 20 ); // Screen(20,40,'#') int main() { Screen s(24,80,'*'); // Screen(24,80,'*') // ... |
}
(В главе 14 конструкторы, деструкторы и операторы присваивания рассматриваются более подробно. В главе 15 обсуждаются конвертеры и функции управления памятью.)
По объявлениям функций-членов pop() и push() класса iStack
невозможно определить, что они возбуждают исключения. Можно, конечно, включить в объявление подходящий комментарий. Тогда описание интерфейса класса в заголовочном файле будет содержать документацию возбуждаемых исключений:
|
class iStack { public: // ... void pop( int &value ); // возбуждает popOnEmpty void push( int value ); // возбуждает pushOnFull private: // ... |
};
Но такое решение несовершенно. Неизвестно, будет ли обновлена документация при выпуске следующих версий iStack. Кроме того, комментарий не дает компилятору достоверной информации о том, что никаких других исключений функция не возбуждает. Спецификация исключений позволяет перечислить в объявлении функции все исключения, которые она может возбуждать. При этом гарантируется, что другие исключения функция возбуждать не будет.
Такая спецификация следует за списком формальных параметров функции. Она состоит из ключевого слова throw, за которым идет список типов исключений, заключенный в скобки. Например, объявления функций-членов класса iStack можно модифицировать, добавив спецификации исключений:
|
class iStack { public: // ... void pop( int &value ) throw(popOnEmpty); void push( int value ) throw(pushOnFull); private: // ... |
};
Гарантируется, что при обращении к pop() не будет возбуждено никаких исключений, кроме popOnEmpty, а при обращении к push()–только pushOnFull.
Объявление исключения – это часть интерфейса функции, оно должно быть задано при ее объявлении в заголовочном файле. Спецификация исключений – это своего рода “контракт” между функцией и остальной частью программы, гарантия того, что функция не будет возбуждать никаких исключений, кроме перечисленных.
Если в объявлении функции присутствует спецификация исключений, то при повторном объявлении этой же функции должны быть перечислены точно те же типы. Спецификации исключений в разных объявлениях одной и той же функции не суммируются:
// два объявления одной и той же функции extern int foo( int = 0 ) throw(string); // ошибка: опущена спецификация исключений |
void recoup( int op1, int op2 ) throw(ExceptionType) { try { // ... throw string("we're in control"); } // обрабатывается возбужденное исключение catch ( string ) { // сделать все необходимое } |
extern void doit( int, int ) throw(string, exceptionType); void action ( int op1, int op2 ) throw(string) { doit( op1, op2 ); // ошибки компиляции не будет // ... |
int convert( int parm ) throw(string) { //... if ( somethingRather ) // ошибка программы: // convert() не допускает исключения типа const char* throw "help!"; |
Спецификацию исключений можно задавать и при объявлении указателя на функцию. Например:
void (*pf)( int ) throw(string);
В этом объявлении говорится, что pf
указывает на функцию, которая способна возбуждать только исключения типа string. Как и для объявлений функций, спецификации исключений в разных объявлениях одного и того же указателя не суммируются, они должны быть одинаковыми:
|
extern void (*pf) ( int ) throw(string); // ошибка: отсутствует спецификация исключения |
void (*pf)( int );
При работе с указателем на функцию со спецификацией исключений есть ограничения на тип указателя, используемого в качестве инициализатора или стоящего в правой части присваивания. Спецификации исключений обоих указателей не обязаны быть идентичными. Однако на указатель-инициализатор она должна накладывать столь же или более строгие ограничения, чем на инициализируемый указатель (или тот, которому присваивается значение). Например:
|
void recoup( int, int ) throw(exceptionType); void no_problem() throw(); void doit( int, int ) throw(string, exceptionType); // правильно: ограничения, накладываемые на спецификации // исключений recoup() и pf1, одинаковы void (*pf1)( int, int ) throw(exceptionType) = &recoup; // правильно: ограничения, накладываемые на спецификацию исключений no_problem(), более строгие, // чем для pf2 void (*pf2)( ) throw(string) = &no_problem; // ошибка: ограничения, накладываемые на спецификацию // исключений doit(), менее строгие, чем для pf3 // |
void (*pf3)( int, int ) throw(string) = &doit;
Третья инициализация не имеет смысла. Объявление указателя гарантирует, что pf3
адресует функцию, которая может возбуждать только исключения типа string. Но doit() возбуждает также исключения типа exceptionType. Поскольку она не подходит под ограничения, накладываемые спецификацией исключений pf3, то не может служить корректным инициализатором для pf3, так что компилятор выдает ошибку.
Упражнение 11.9
В коде, разработанном для упражнения 11.8, измените объявление оператора operator[]() в классе IntArray, добавив спецификацию возбуждаемых им исключений. Модифицируйте программу так, чтобы operator[]()
возбуждал исключение, не указанное в спецификации. Что при этом происходит?
Упражнение 11.10
Какие исключения может возбуждать функция, если ее спецификация исключений имеет вид throw()? А если у нее нет такой спецификации?
Упражнение 11.11
Какое из следующих присваиваний ошибочно? Почему?
|
void example() throw(string); (a) void (*pf1)() = example; |
(b) void (*pf2) throw() = example;
Возьмем следующий пример кода:
|
for ( int index = 0; index < 512; ++index ) |
... ;
С использованием литерала 512 связаны две проблемы. Первая состоит в легкости восприятия текста программы. Почему верхняя граница переменной цикла должна быть равна именно 512? Что скрывается за этой величиной? Она кажется случайной...
Вторая проблема касается простоты модификации и сопровождения кода. Предположим, программа состоит из 10 000 строк, и литерал 512 встречается в 4% из них. Допустим, в 80% случаев число 512 должно быть изменено на 1024. Способны ли вы представить трудоемкость такой работы и количество ошибок, которые можно сделать, исправив не то значение?
Обе эти проблемы решаются одновременно: нужно создать объект со значением 512. Присвоив ему осмысленное имя, например bufSize, мы сделаем программу гораздо более понятной: ясно, с чем именно сравнивается переменная цикла.
index < bufSize
В этом случае изменение размера bufSize не требует просмотра 400 строк кода для модификации 320 из них. Насколько уменьшается вероятность ошибок ценой добавления всего одного объекта! Теперь значение 512 локализовано.
|
int bufSize = 512; // размер буфера ввода // ... |
for ( int index = 0; index < bufSize; ++index )
// ...
Остается одна маленькая проблема: переменная bufSize здесь является l-значением, которое можно случайно изменить в программе, что приведет к трудно отлавливаемой ошибке. Вот одна из распространенных ошибок – использование операции присваивания (=) вместо сравнения (==):
// случайное изменение значения bufSize
if ( bufSize = 1 )
// ...
В результате выполнения этого кода значение bufSize станет равным 1, что может привести к совершенно непредсказуемому поведению программы. Ошибки такого рода обычно очень тяжело обнаружить, поскольку они попросту не видны.
Использование спецификатора const
решает данную проблему. Объявив объект как
const int bufSize = 512; // размер буфера ввода
мы превращаем переменную в константу со значением 512, значение которой не может быть изменено: такие попытки пресекаются компилятором: неверное использование оператора присваивания вместо сравнения, как в приведенном примере, вызовет ошибку компиляции.
// ошибка: попытка присваивания значения константе |
const double minWage = 9.60; // правильно? ошибка? |
const double *pc = 0; const double minWage = 9.60; // правильно: не можем изменять minWage с помощью pc pc = &minWage; double dval = 3.14; // правильно: не можем изменять minWage с помощью pc // хотя dval и не константа pc = &dval; // правильно dval = 3.14159; //правильно |
// В реальных программах указатели на константы чаще всего // употребляются как формальные параметры функций |
do_something(); |
errorHandler(); *curErr = 0; // правильно: обнулим значение errNumb |
const double pi = 3.14159; |
(a) int i; (d) int *const cpi; (b) const int ic; (e) const int *const cpic; |
(a) int i = -1; (b) const int ic = i; (c) const int *pic = ⁣ (d) int *const cpi = ⁣ |
(a) i = ic; (d) pic = cpic; (b) pic = ⁣ (i) cpic = ⁣ |
Объект объявляется как volatile
(неустойчивый, асинхронно изменяемый), если его значение может быть изменено незаметно для компилятора, например переменная, обновляемая значением системных часов. Этот спецификатор сообщает компилятору, что не нужно производить оптимизацию кода для работы с данным объектом.
Спецификатор volatile
используется подобно спецификатору const:
|
volatile int disp1ay_register; volatile Task *curr_task; volatile int ixa[ max_size ]; |
volatile Screen bitmap_buf;
display_register – неустойчивый объект типа int. curr_task – указатель на неустойчивый объект класса Task. ixa – неустойчивый массив целых, причем каждый элемент такого массива считается неустойчивым. bitmap_buf – неустойчивый объект класса Screen, каждый его член данных также считается неустойчивым.
Единственная цель использования спецификатора volatile – сообщить компилятору, что тот не может определить, кто и как может изменить значение данного объекта. Поэтому компилятор не должен выполнять оптимизацию кода, использующего данный объект.
Модифицируем наш класс Account, объявив член _name
типа string:
|
#include <string> class Account { public: // ... private: unsigned int _acct_nmbr; double _balance; string _name; |
};
Придется заодно изменить и конструкторы. Возникает две проблемы: поддержание совместимости с первоначальным интерфейсом и инициализация объекта класса с помощью подходящего набора конструкторов.
Исходный конструктор Account с двумя параметрами
Account( const char*, double = 0.0 );
не может инициализировать член типа string. Например:
|
string new_client( "Steve Hall" ); |
Account new_acct( new_client, 25000 );
не будет компилироваться, так как не существует неявного преобразования из типа string в тип char*. Инструкция
Account new_acct( new_client.c_str(), 25000 );
правильна, но вызовет у пользователей класса недоумение. Одно из решений– добавить новый конструктор вида:
Account( string, double = 0.0 );
Если написать:
Account new_acct( new_client, 25000 );
вызывается именно этот конструктор, тогда как старый код
|
Account *open_new_account( const char *nm ) { Account *pact = new Account( nm ); // ... return pacct; |
}
по-прежнему будет приводить к вызову исходного конструктора с двумя параметрами.
Так как в классе string
определено преобразование из типа char* в тип string
(преобразования классов обсуждаются в этой главе ниже), то можно заменить исходный конструктор на новый, которому в качестве первого параметра передается тип string. В таком случае, когда встречается инструкция:
Account myAcct( "Tinkerbell" );
"Tinkerbell"
преобразуется во временный объект типа string. Затем этот объект передается новому конструктору с двумя параметрами.
При проектировании приходится идти на компромисс между увеличением числа конструкторов класса Account и несколько менее эффективной обработкой аргументов типа char*
из-за необходимости создавать временный объект. Мы предоставили две версии конструктора с двумя параметрами. Тогда модифицированный набор конструкторов Account
#include <string> class Account { public: Account(); Account( const char*, double=0.0 ); Account( const string&, double=0.0 ); Account( const Account& ); // ... private: // ... |
inline Account:: Account( const char* name, double opening_bal ) : _name( name ), _balance( opening_bal ) { _acct_nmbr = het_unique_acct_nmbr(); |
inline Account:: Account( const string& name, double opening_bal ) : _name( name ), _balance( opening_bal ) { _acct_nmbr = het_unique_acct_nmbr(); |
inline Account:: Account( const char* name, double opening_bal ) : _name( name ), _balance( opening_bal ) { _acct_nmbr = het_unique_acct_nmbr(); |
Account( const char* name, double opening_bal ) { _name = name; _balance = opening_bal; _acct_nmbr = het_unique_acct_nmbr(); |
inline Account:: Account() { _name = ""; _balance = 0.0; _acct_nmbr = 0; |
inline Account:: Account() : _name( string() ) { _balance = 0.0; _acct_nmbr = 0; |
inline Account:: Account() { _balance = 0.0; _acct_nmbr = 0; |
// предпочтительный стиль инициализации inline Account:: Account() : _balance( 0.0 ), _acct_nmbr( 0 ) |
class ConstRef { public: ConstRef(int ii ); private: int i; const int ci; int &ri; }; ConstRef:: ConstRef( int ii ) { // присваивание i = ii; // правильно ci = ii; // ошибка: нельзя присваивать константному члену ri = i; // ошибка: ri не инициализирована |
// правильно: инициализируются константные члены и ссылки ConstRef:: ConstRef( int ii ) : ci( ii ), ri ( i ) |
class Account { public: // ... private: unsigned int _acct_nmbr; double _balance; string _name; |
inline Account:: Account() : _name( string() ), _balance( 0.0 ), _acct_nmbr( 0 ) |
inline Account:: Account( const char* name, double bal ) : _name( name ), _balance( bal ) { _acct_nmbr = get_unique_acct_nmbr(); |
class X { int i; int j; public: // видите проблему? X( int val ) : j( val ), i( j ) {} // ... |
// предпочтительная идиома |
(a) Word::Word( char *ps, int count = 1 ) : _ps( new char[strlen(ps)+1] ), _count( count ) { if ( ps ) strcpy( _ps, ps ); else { _ps = 0; _count = 0; } |
(b) class CL1 { public: CL1() { c.real(0.0); c.imag(0.0); s = "not set"; } // ... private: complex<double> c; string s; |
(c) class CL2 { public: CL2( map<string,location> *pmap, string key ) : _text( key ), _loc( (*pmap)[key] ) {} // ... private: location _loc; string _text; |
Список параметров не может быть опущен. Функция, которая не требует параметров, должна иметь пустой список либо список, состоящий из одного ключевого слова void. Например, следующие объявления эквивалентны:
|
int fork(); |
int fork( void );
Такой список состоит из названий типов, разделенных запятыми. После имени типа может находиться имя параметра, хотя это и необязательно. В списке параметров не разрешается использовать сокращенную запись, соотнося одно имя типа с несколькими параметрами:
|
int manip( int vl, v2 ); // ошибка |
int manip( int vl, int v2 ); // правильно
Имена параметров не могут повторяться. Имена, фигурирующие в определении функции, можно и даже нужно использовать в ее теле. В объявлении же функции они не обязательны и служат средством документирования ее интерфейса. Например:
void print( int *array, int size );
Имена параметров в объявлении и в определении одной и той же функции не обязаны совпадать. Однако употребление разных имен может запутать пользователя.
С++ допускает сосуществование двух или более функций, имеющих одно и то же имя, но разные списки параметров. Такие функции называются перегруженными. О списке параметров в этом случае говорят как о сигнатуре функции, поскольку именно он используется различения разных версий одноименных функций. Имя и сигнатура однозначно идентифицируют версию. (Перегруженные функции подробно обсуждаются в главе 9.)
Сравнительные объекты-функции поддерживают операции равенства, неравенства, больше, больше или равно, меньше, меньше или равно.
·
Равенство: equal_to<Type>
|
equal_to<string> stringEqual; sres = stringEqual( sval1, sval2 ); ires = count_if( svec.begin(), svec.end(), |
equal_to<string>(), sval1 );
· Неравенство: not_equal_to<Type>
|
not_equal_to<complex> complexNotEqual; cres = complexNotEqual( cval1, cval2 ); ires = count_if( svec.begin(), svec.end(), |
not_equal_to<string>(), sval1 );
· Больше: greater<Type>
|
greater<int> intGreater; ires = intGreater( ival1, ival2 ); ires = count_if( svec.begin(), svec.end(), |
greater<string>(), sval1 );
· Больше или равно: greater_equal<Type>
|
greater_equal<double> doubleGreaterEqual; dres = doubleGreaterEqual( dval1, dval2 ); ires = count_if( svec.begin(), svec.end(), |
greater_equal <string>(), sval1 );
· Меньше: less<Type>
|
less<Int> IntLess; Ires = IntLess( Ival1, Ival2 ); ires = count_if( svec.begin(), svec.end(), |
less<string>(), sval1 );
· Меньше или равно: less_equal<Type>
|
less_equal<int> intLessEqual; ires = intLessEqual( ival1, ival2 ); ires = count_if( svec.begin(), svec.end(), |
less_equal<string>(), sval1 );
Фактический аргумент или формальный параметр функции могут быть ссылками. Как это влияет на правила преобразования типов?
Рассмотрим, что происходит, когда ссылкой является фактический аргумент. Его тип никогда не бывает ссылочным. Аргумент-ссылка трактуется как l-значение, тип которого совпадает с типом соответствующего объекта:
|
int i; int& ri = i; void print( int ); int main() { print( i ); // аргумент - это lvalue типа int print( ri ); // то же самое return 0; |
}
Фактический аргумент в обоих вызовах имеет тип int. Использование ссылки для его передачи во втором вызове не влияет на сам тип аргумента.
Стандартные преобразования и расширения типов, рассматриваемые компилятором, одинаковы для случаев, когда фактический аргумент является ссылкой на тип T и когда он сам имеет такой тип. Например:
|
int i; int& ri = i; void calc( double ); int main() { calc( i ); // стандартное преобразование между целым типом // и типом с плавающей точкой calc( ri ); // то же самое return 0; |
}
А как влияет на преобразования, применяемые к фактическому аргументу, формальный параметр-ссылка? Сопоставление дает следующие результаты:
· фактический аргумент подходит в качестве инициализатора параметра-ссылки. В таком случае мы говорим, что между ними есть точное соответствие:
|
void swap( int &, int & ); void manip( int i1, int i2 ) { // ... swap( i1, i2 ); // правильно: вызывается swap( int &, int & ) // ... return 0; |
}
· фактический аргумент не может инициализировать параметр-ссылку. В такой ситуации точного соответствия нет, и аргумент нельзя использовать для вызова функции. Например:
|
int obj; void frd( double & ); int main() { frd( obj ); // ошибка: параметр должен иметь иметь тип const double & return 0; |
class B; void takeB( B& ); B giveB(); int main() { takeB( giveB() ); // ошибка: параметр должен быть типа const B & return 0; |
void print( int ); void print( int& ); int iobj; int &ri = iobj; int main() { print( iobj ); // ошибка: неоднозначность print( ri ); // ошибка: неоднозначность print( 86 ); // правильно: вызывается print( int ) return 0; |
(a) void print( int *, int ); int arr[6]; print( arr, 6 ); // вызов функции (b) void manip( int, int ); manip( 'a', 'z' ); // вызов функции (c) int calc( int, int ); double dobj; double = calc( 55.4, dobj ) // вызов функции (d) void set( const int * ); int *pi; |
(a) enum Stat { Fail, Pass }; void test( Stat ); text( 0 ); // вызов функции (b) void reset( void *); reset( 0 ); // вызов функции (c) void set( void * ); int *pi; set( pi ); // вызов функции (d) #include <list> list<int> oper(); void print( oper() ); // вызов функции (e) void print( const int ); int iobj; |
Ссылочный тип, иногда называемый псевдонимом, служит для задания объекту дополнительного имени. Ссылка позволяет косвенно манипулировать объектом, точно так же, как это делается с помощью указателя. Однако эта косвенная манипуляция не требует специального синтаксиса, необходимого для указателей. Обычно ссылки употребляются как формальные параметры функций. В этом разделе мы рассмотрим самостоятельное использование объектов ссылочного типа.
Ссылочный тип обозначается указанием оператора взятия адреса (&) перед именем переменной. Ссылка должна быть инициализирована. Например:
|
int ival = 1024; // правильно: refVal - ссылка на ival int &refVal = ival; // ошибка: ссылка должна быть инициализирована |
int &refVal2;
Хотя, как мы говорили, ссылка очень похожа на указатель, она должна быть инициализирована не адресом объекта, а его значением. Таким объектом может быть и указатель:
|
int ival = 1024; // ошибка: refVal имеет тип int, а не int* int &refVal = &ival; int *pi = &ival; // правильно: ptrVal - ссылка на указатель |
int *&ptrVal2 = pi;
Определив ссылку, вы уже не сможете изменить ее так, чтобы работать с другим объектом (именно поэтому ссылка должна быть инициализирована в месте своего определения). В следующем примере оператор присваивания не меняет значения refVal, новое значение присваивается переменной ival – ту, которую адресует refVal.
|
int min_val = 0; // ival получает значение min_val, // а не refVal меняет значение на min_val |
refVal = min_val;
Все операции со ссылками реально воздействуют на адресуемые ими объекты. В том числе и операция взятия адреса. Например:
refVal += 2;
прибавляет 2 к ival – переменной, на которую ссылается refVal. Аналогично
int ii = refVal;
присваивает ii
текущее значение ival,
int *pi = &refVal;
инициализирует pi
адресом ival.
Если мы определяем ссылки в одной инструкции через запятую, перед каждым объектом типа ссылки должен стоять амперсанд (&) – оператор взятия адреса (точно так же, как и для указателей). Например:
// определено два объекта типа int int ival = 1024, ival2 = 2048; // определена одна ссылка и один объект int &rval = ival, rval2 = ival2; // определен один объект, один указатель и одна ссылка int inal3 = 1024, *pi = ival3, &ri = ival3; // определены две ссылки |
double dval = 3.14159; // верно только для константных ссылок const int &ir = 1024; const int &ir2 = dval; |
double dval = 1024; |
int temp = dval; |
const int ival = 1024; // ошибка: нужна константная ссылка |
const int ival = 1024; // все равно ошибка |
const int ival = 1024; // правильно |
int temp = 0; |
int ival = 1024, ival2 = 2048; int *pi = &ival, *pi2 = &ival2; |
int &ri = ival, &ri2 = ival2; |
// пример использования ссылок // Значение возвращается в параметре next_value bool get_next_value( int &next_value ); // перегруженный оператор |
int ival; |
(a) int ival = 1.01; (b) int &rval1 = 1.01; (c) int &rval2 = ival; (d) int &rval3 = &ival; (e) int *pi = &ival; (f) int &rval4 = pi; (g) int &rval5 = pi*; (h) int &*prval1 = pi; |
(a) rval1 = 3.14159; (b) prval1 = prval2; (c) prval2 = rval1; |
(a) int ival = 0; const int *pi = 0; const int &ri = 0; (b) pi = &ival; ri = &ival; |
Все компоненты стандартной библиотеки С++ находятся в пространстве имен std. Каждая функция, объект и шаблон класса, объявленные в стандартном заголовочном файле, таком, как <vector> или <iostream>, принадлежат к этому пространству.
Если все компоненты библиотеки объявлены в std, то какая ошибка допущена в данном примере:
|
#include <vector> #include <string> #include <iterator> int main() { // привязка istream_iterator к стандартному вводу istream_iterator<string> infile( cin ); // istream_iterator, отмечающий end-of-stream istream_iterator<string> eos; // инициализация svec элементами, считываемыми из cin vector<string> svec( infile, eos ); // ... |
}
Правильно, этот фрагмент кода не компилируется, потому что члены пространства имен std
должны использоваться с указанием их специфицированных имен. Для того чтобы исправить положение, мы можем выбрать один из следующих способов:
· заменить имена членов пространства std в этом примере соответствующими специфицированными именами;
· применить using-объявления, чтобы сделать видимыми используемые члены пространства std;
· употребить using-директиву, сделав видимыми все члены пространства std.
Членами пространства имен std в этом примере являются: шаблон класса istream_iterator, стандартный входной поток cin, класс string и шаблон класса vector.
Простейшее решение – добавить using-директиву после директивы препроцессора #include:
using namespace std;
В данном примере using-директива делает все члены пространства std
видимыми. Однако не все они нам нужны. Предпочтительнее пользоваться using-объявлениями, чтобы уменьшить вероятность коллизии имен при последующем добавлении в программу глобальных объявлений.
using std::istream_iterator; using std::string; using std::cin; |
namespace Exercise { int ivar = 0; double dvar = 0; const int limit = 1000; } int ivar = 0; //1 void manip() { //2 double dvar = 3.1416; int iobj = limit + 1; ++ivar; ++::ivar; |
Хотя встроенный массив формально и обеспечивает механизм контейнера, он, как мы видели выше, не поддерживает семантику абстракции контейнера. До принятия стандарта C++ для программирования на таком уровне мы должны были либо приобрести нужный класс, либо реализовать его самостоятельно. Теперь же класс массива является частью стандартной библиотеки C++. Только называется он не массив, а вектор.
Разумеется, вектор реализован в виде шаблона класса. Так, мы можем написать
vector<int> ivec(10);
vector<string> svec(10);
Есть два существенных отличия нашей реализации шаблона класса Array от реализации шаблона класса vector. Первое отличие состоит в том, что вектор поддерживает как присваивание значений существующим элементам, так и вставку дополнительных элементов, то есть динамически растет во время выполнения, если программист решил воспользоваться этой его возможностью. Второе отличие более радикально и отражает существенное изменение парадигмы проектирования. Вместо того чтобы поддержать большой набор операций-членов, применимых к вектору, таких, как sort(), min(), max(), find()и так далее, класс vector
предоставляет минимальный набор: операции сравнения на равенство и на меньше, size() и empty(). Более общие операции, перечисленные выше, определены как независимые обобщенные алгоритмы.
Для использования класса vector мы должны включить соответствующий заголовочный файл.
#include <vector>
|
// разные способы создания объектов типа vector vector<int> vec0; // пустой вектор const int size = 8; const int value = 1024; // вектор размером 8 // каждый элемент инициализируется 0 vector<int> vec1(size); // вектор размером 8 // каждый элемент инициализируется числом 1024 vector<int> vec2(size,value); // вектор размером 4 // инициализируется числами из массива ia int ia[4] = { 0, 1, 1, 2 }; vector<int> vec3(ia,ia+4); // vec4 - копия vec2 |
vector<int> vec4(vec2);
#include <vector> |
#include <vector> |
#include <vector> #include <algorithm> #include <iostream> int ia[ 10 ] = { 51, 23, 7, 88, 41, 98, 12, 103, 37, 6 }; int main() { vector< int > vec( ia, ia+10 ); vector<int>::iterator it = vec.begin(), end_it = vec.end(); cout << "Начальный массив: "; for ( ; it != end_it; ++ it ) cout << *it << ' '; cout << "\n"; // сортировка массива sort( vec.begin(), vec.end() ); cout << "упорядоченный массив: "; it = vec.begin(); end_it = vec.end(); for ( ; it != end_it; ++ it ) cout << *it << ' '; cout << "\n\n"; int search_value; cout << "Введите значение для поиска: "; cin >> search_value; // поиск элемента vector<int>::iterator found; found = find( vec.begin(), vec.end(), search_value ); if ( found != vec.end() ) cout << "значение найдено!\n\n"; else cout << "значение найдено!\n\n"; // инвертирование массива reverse( vec.begin(), vec.end() ); cout << "инвертированный массив: "; it = vec.begin(); end_it = vec.end(); for ( ; it != end_it; ++ it ) cout << *it << ' '; cout << endl; |
#include <map> |
string pals[] = { "pooh", "tiger", "piglet", "eeyore", "kanga" }; (a) vector<string> svec1(pals,pals+5); (b) vector<int> ivec1(10); (c) vector<int> ivec2(10,10); (d) vector<string> svec2(svec1); (e) vector<double> dvec; |
template <class elemType> |
Иногда нужно, чтобы все объекты некоторого класса имели доступ к единственному глобальному объекту. Допустим, необходимо подсчитать, сколько их было создано; глобальным может быть указатель на процедуру обработки ошибок для класса или, скажем, указатель на свободную память для его объектов. В подобных случаях более эффективно иметь один глобальный объект, используемый всеми объектами класса, чем отдельные члены в каждом объекте. Хотя такой объект является глобальным, он существует лишь для поддержки реализации абстракции класса.
В этой ситуации приемлемым решением является статический член класса, который ведет себя как глобальный объект, принадлежащий своему классу. В отличие от других членов, которые присутствуют в каждом объекте как отдельные элементы данных, статический член существует в единственном экземпляре и связан с самим типом, а не с конкретным его объектом. Это разделяемая сущность, доступная всем объектам одного класса.
По сравнению с глобальным объектом у статического члена есть следующие преимущества:
·
статический член не находится в глобальном пространстве имен программы, следовательно, уменьшается вероятность случайного конфликта имен с другими глобальными объектами;
· остается возможность сокрытия информации, так как статический член может быть закрытым, а глобальный объект – никогда.
Чтобы сделать член статическим, надо поместить в начале его объявления в теле класса ключевое слово static. К ним применимы все правила доступа к открытым, закрытым и защищенным членам. Например, для определенного ниже класса Account член _interestRate
объявлен как закрытый и статический типа double:
|
class Account { // расчетный счет Account( double amount, const string &owner ); string owner() { return _owner; } private: static double _interestRate; // процентная ставка double _amount; // сумма на счету string _owner; // владелец |
// явная инициализация статического члена класса #include "account.h" |
#include <string> class Account { // ... private: static const string name; }; |
// заголовочный файл class Account { //... private: static const int nameSize = 16; static const string name[nameSize]; }; // исходный файл const string Account::nameSize; // необходимо определение члена |
class Account { //... private: static const int nameSize = 16; // правильно: целый тип static const string name[nameSize] = "Savings Account"; // ошибка |
inline double Account::dailyReturn() { return( _interestRate / 365 * _amount ); |
class Account { // ... private: friend int compareRevenue( Account&, Account* ); // остальное без изменения }; // мы используем ссылочный и указательный параметры, // чтобы проиллюстрировать оба оператора доступа int compareRevenue( Account &ac1, Account *ac2 ); { double ret1, ret2; ret1 = ac1._interestRate * ac1._amount; ret2 = ac2->_interestRate * ac2->_amount; // ... |
// доступ к статическому члену с указанием квалифицированного имени |
int compareRevenue( Account &ac1, Account *ac2 ); { double ret1, ret2; ret1 = Account::_interestRate * ac1._amount; ret2 = Account::_interestRate * ac2->_amount; // ... |
class Bar { public: // ... private: static Bar mem1; // правильно Bar *mem2; // правильно Bar mem3; // ошибка |
extern int var; class Foo { private: int var; static int stcvar; public: // ошибка: трактуется как Foo::var, // но ассоциированного объекта класса не существует int mem1( int = var ); // правильно: трактуется как static Foo::stcvar, // ассоциированный объект и не нужен int mem2( int = stcvar ); // правильно: трактуется как глобальная переменная var int mem3( int = :: var ); |
В шаблоне класса могут быть объявлены статические данные-члены. Каждый конкретизированный экземпляр имеет собственный набор таких членов. Рассмотрим операторы new() и delete() для шаблона QueueItem. В класс QueueItem
нужно добавить два статических члена:
|
static QueueItem<Type> *free_list; |
static const unsigned QueueItem_chunk;
Модифицированное определение шаблона QueueItem
выглядит так:
|
#include <cstddef> template <class Type> class QueueItem { // ... private: void *operator new( size_t ); void operator delete( void *, size_t ); // ... static QueueItem *free_list; static const unsigned QueueItem_chunk; // ... |
};
Операторы new() и delete()
объявлены закрытыми, чтобы предотвратить создание объектов типа QueueItem
вызывающей программой: это разрешается только членам и друзьям QueueItem (к примеру, шаблону Queue).
Оператор new()
можно реализовать таким образом:
|
template <class Type> void* QueueItem<Type>::operator new( size_t size ) { QueueItem<Type> *p; if ( ! free_list ) { size_t chunk = QueueItem_chunk * size; free_list = p = reinterpret_cast< QueueItem<Type>* > ( new char[chunk] ); for ( ; p != &free_list[ QueueItem_chunk - 1 ]; ++p ) p->next = p + 1; p->next = 0; } p = free_list; free_list = free_list->next; return p; |
}
А реализация оператора delete()
выглядит так:
|
template <class Type> void QueueItem<Type>:: operator delete( void *p, size_t ) { static_cast< QueueItem<Type>* >( p )->next = free_list; free_list = static_cast< QueueItem<Type>* > ( p ); |
}
Теперь остается инициализировать статические члены free_list и QueueItem_chunk. Вот шаблон для определения статических данных-членов:
|
/* для каждой конкретизации QueueItem сгенерировать * соответствующий free_list и инициализировать его нулем */ template <class T> QueueItem<T> *QueueItem<T>::free_list = 0; /* для каждой конкретизации QueueItem сгенерировать * соответствующий QueueItem_chunk и инициализировать его значением 24 */ template <class T> const unsigned int |
// ошибка: QueueItem - это не реальный конкретизированный экземпляр int ival0 = QueueItem::QueueItem_chunk; int ival1 = QueueItem<string>::QueueItem_chunk; // правильно |
Функции-члены raiseInterest() и interest()
обращаются к глобальному статическому члену _interestRate:
|
class Account { public: void raiseInterest( double incr ); double interest() { return _interestRate; } private: static double _interestRate; }; inline void Account::raiseInterest( double incr ) { _interestRate += incr; |
}
Проблема в том, что любая функция-член должна вызываться с помощью оператора доступа к конкретному объекту класса. Поскольку приведенные выше функции обращаются только к статическому _interestRate, то совершенно безразлично, для какого объекта они вызываются. Нестатические члены при вызове этих функций не читаются и не модифицируются.
Поэтому лучше объявить такие функции-члены как статические. Это можно сделать следующим образом:
|
class Account { public: static void raiseInterest( double incr ); static double interest() { return _interestRate; } private: static double _interestRate; }; inline void Account::raiseInterest( double incr ) { _interestRate += incr; |
}
Объявление статической функции-члена почти такое же, как и нестатической: в теле класса ему предшествует ключевое слово static, а спецификаторы const или volatile
запрещены. В ее определении, находящемся вне тела класса, слова static
быть не должно.
Такой функции-члену указатель this не передается, поэтому явное или неявное обращение к нему внутри ее тела вызывает ошибку компиляции. В частности, попытка обращения к нестатическому члену класса неявно требует наличия указателя this и, следовательно, запрещена. Например, представленную ранее функцию-член dailyReturn() нельзя объявить статической, поскольку она обращается к нестатическому члену _amount.
Статическую функцию-член можно вызвать для объекта класса, пользуясь одним из операторов доступа. Ее также можно вызвать непосредственно, квалифицировав ее имя, даже если никаких объектов класса не объявлено. Вот небольшая программа, иллюстрирующая их применение:
|
#include <iostream> #include "account.h" bool limitTest( double limit ) { // пока еще ни одного объекта класса Account не объявлено // правильно: вызов статической функции-члена return limit <= Account::interest() ; } int main() { double limit = 0.05; if ( limitTest( limit ) ) { // указатель на статическую функцию-член // объявлен как обычный указатель void (*psf)(double) = &Account::raiseInterest; psf( 0.0025 ); } Account ac1( 5000, "Asterix" ); Account ac2( 10000, "Obelix" ); if ( compareRevenue( ac1, &ac2 ) > 0 ) cout << ac1.owner() << " is richer than " << ac2.owner() << "\n"; else cout << ac1.owner() << " is poorer than " << ac2.owner() << "\n"; return 0; |
class X { public: X( int i ) { _val = i; } int val() { return _val; } private: int _val; }; class Y { public: Y( int i ); static X xval(); static int callsXval(); private: static X _xval; static int _callsXval; |
// example.h class Example { public: static double rate = 6.5; static const int vecSize = 20; static vector<double> vec(vecSize); }; // example.c #include "example.h" double Example::rate; |
Внутри функции или составной инструкции можно объявить объект с локальной областью видимости, который, однако, будет существовать в течение всего времени выполнения программы. Если значение локального объекта должно сохраняться между вызовами функции, то обычный автоматический объект не подойдет: ведь его значение теряется каждый раз после выхода.
В таком случае локальный объект необходимо объявить как static (со статической продолжительностью хранения). Хотя значение такого объекта сохраняется между вызовами функции, в которой он определен, видимость его имени ограничена локальной областью. Статический локальный объект инициализируется во время первого выполнения инструкции, где он объявлен. Вот, например, версия функции gcd(),устанавливающая глубину рекурсии с его помощью:
|
#include <iostream> int traceGcd( int vl, int v2 ) { static int depth = 1; cout << "глубина #" << depth++ << endl; if ( v2 == 0 ) { depth = 1; return vl; } return traceGcd( v2, vl%v2 ); |
}
Значение, ассоциированное со статическим локальным объектом depth, сохраняется между вызовами traceGcd(). Его инициализация выполняется только один раз – когда к этой функции обращаются впервые. В следующей программе используется traceGcd():
|
#include <iostream> extern int traceGcd(int, int); int main() { int rslt = traceCcd( 15, 123 ); cout << "НОД (15,123): " << rslt << endl; return 0; |
}
Результат работы программы:
глубина #1
глубина #2
глубина #3
глубина #4
НОД (15,123): 3
Неинициализированные статические локальные объекты получают значение 0. А автоматические объекты в подобной ситуации получают случайные значения. Следующая программа иллюстрирует разницу инициализации по умолчанию для автоматических и статических объектов и опасность, подстерегающую программиста в случае ее отсутствия для автоматических объектов.
|
#include <iostream> const int iterations = 2; void func() { int value1, value2; // не инициализированы static int depth; // неявно инициализирован нулем if ( depth < iterations ) { ++depth; func(); } else depth = 0; cout << "\nvaluel:\t" << value1; cout << "\tvalue2:\t" << value2; cout << "\tsum:\t" << value1 + value2; } int main() { for ( int ix = 0; ix < iterations; ++ix ) func(); return 0; |
}
Вот результат работы программы:
valuel: 0 value2: 74924 sum: 74924
valuel: 0 value2: 68748 sum: 68748
valuel: 0 value2: 68756 sum: 68756
valuel: 148620 value2: 2350 sum: 150970
valuel: 2147479844 value2: 671088640 sum: -1476398812
valuel: 0 value2: 68756 sum: 68756
value1 и
value2 – неинициализированные автоматические объекты. Их начальные значения, как можно видеть из приведенной распечатки, оказываются случайными, и потому результаты сложения непредсказуемы. Объект depth, несмотря на отсутствие явной инициализации, гарантированно получает значение 0, и функция func()
рекурсивно вызывает сама себя только дважды.
Вызывая виртуальную функцию с помощью оператора разрешения области видимости класса, мы отменяем механизм виртуализации и разрешаем вызов статически, на этапе компиляции. Предположим, что мы определили виртуальную функцию isA() в базовом и каждом из производных классов иерархии Query:
|
Query *pquery = new NameQuery( "dumbo" ); // isA() вызывается динамически с помощью механизма виртуализации // реально будет вызвана NameQuery::isA() pquery->isA(); // isA вызывается статически во время компиляции // реально будет вызвана Query::isA |
pquery->Query::isA();
Тогда явный вызов Query::isA()
разрешается на этапе компиляции в пользу реализации isA() в базовом классе Query, хотя pquery
адресует объект NameQuery.
Зачем нужно отменять механизм виртуализации? Как правило, ради эффективности. В теле виртуальной функции производного класса часто необходимо вызвать реализацию из базового, чтобы завершить операцию, расщепленную между базовым и производным классами. К примеру, вполне вероятно, что виртуальная функция display() из Camera
выводит некоторую информацию, общую для всех камер, а реализация display() в классе PerspectiveCamera
сообщает информацию, специфичную только для перспективных камер. Вместо того чтобы дублировать в ней действия, общие для всех камер, можно вызвать реализацию из класса Camera. Мы точно знаем, какая именно реализация нам нужна, поэтому нет нужды прибегать к механизму виртуализации. Более того, реализация в Camera
объявлена встроенной, так что разрешение во время компиляции приводит к подстановке по месту вызова.
Приведем еще один пример, когда отмена механизма виртуализации может оказаться полезной, а заодно познакомимся с неким аспектом чисто виртуальных функций, который начинающим программистам кажется противоречащим интуиции.
Реализации функции print() в классах AndQuery и OrQuery
совпадают во всем, кроме литеральной строки, представляющей название оператора. Реализуем только одну функцию, которую можно вызывать из данных классов. Для этого мы снова определим абстрактный базовый BinaryQuery (его наследники – AndQuery и OrQuery). В нем определены два операнда и еще один член типа string для хранения значения оператора. Поскольку это абстрактный класс, объявим print()
class BinaryQuery : public Query { public: BinaryQuery( Query *lop, Query *rop, string oper ) : _lop(lop), _rop(rop), _oper(oper) {} ~BinaryQuery() { delete _lop; delete _rop; } ostream &print( ostream&=cout, ) const = 0; protected: Query *_lop; Query *_rop; string _oper; |
inline ostream& BinaryQuery:: print( ostream &os ) const { if ( _lparen ) print_lparen( _lparen, os ); _lop->print( os ); os << ' ' << _oper << ' '; _rop->print( os ); if ( _rparen ) print_rparen( _rparen, os ); return os; |
inline ostream& AndQuery:: print( ostream &os ) const { // правильно: подавить механизм виртуализации // вызвать BinaryQuery::print статически BinaryQuery::print( os ); |
В разделе 4.5 операции инкремента и декремента были проиллюстрированы на примере реализации абстракции стека. В общем случае стек является очень полезным механизмом для сохранения текущего состояния, если в разные моменты выполнения программы одновременно существует несколько состояний, вложенных друг в друга.
Поскольку стек– это важная абстракция данных, в стандартной библиотеке С++ предусмотрен класс stack, для использования которого нужно включить заголовочный файл:
#include <stack>
В стандартной библиотеке стек реализован несколько иначе, чем у нас. Разница состоит в том, что доступ к элементу с вершины стека и удаление его осуществляются двумя функциями – top() и pop(). Полный набор операций со стеком приведен в таблице 6.5.
Таблица 6.5. Операции со стеком
| Операция | Действие | ||
| empty() | Возвращает true, если стек пуст, и false в противном случае | ||
| size() | Возвращает количество элементов в стеке | ||
| pop() | Удаляет элемент с вершины стека, но не возвращает его значения | ||
| top() | Возвращает значение элемента с вершины стека, но не удаляет его | ||
| push(item) | Помещает новый элемент в стек |
В нашей программе приводятся примеры использования этих операций:
| #include <stack>
#include <iostream> int main() { const int ia_size = 10; int ia[ia_size ]={0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // заполним стек int ix = 0; stack< int > intStack; for ( ; ix < ia_size; ++ix ) intStack.push( ia[ ix ] ); int error_cnt = 0; if ( intStack.size() != ia_size ) { cerr << "Ошибка! неверный размер IntStack: " << intStack.size() << "\t ожидается: " << ia_size << endl, ++error_cnt; } int value; while ( intStack.empty() == false ) { // считаем элемент с вершины value = intStack.top(); if ( value != --ix ) { cerr << "Ошибка! ожидается " << ix << " получено " << value << endl; ++error_cnt; } // удалим элемент intStack.pop(); } cout << "В результате запуска программы получено " << error_cnt << " ошибок" << endl; |
#include <stack> class NurbSurface { /* mumble */ }; |
В этом разделе мы построим отображение (map), позволяющее для каждого уникального слова текста сохранить номера строк и колонок, в которых оно встречается. (В следующем разделе мы изучим ассоциативный контейнер set.) В общем случае контейнер set полезен, если мы хотим знать, содержится ли определенный элемент в некотором множестве, а map
позволяет связать с каждым из них какую-либо величину.
В map хранятся пары ключ/значение. Ключ играет роль индекса для доступа к ассоциированному с ним значению. В нашей программе каждое уникальное слово текста будет служить ключом, а значением станет вектор, содержащий пары (номер строки, номер колонки). Для доступа применяется оператор взятия индекса. Например:
|
string query( "pickle" ); vector< location > *locat; // возвращается location<vector>*, ассоциированный с "pickle" |
locat = text_map[ query ];
Ключом здесь является строка, а значение имеет тип location<vector>*.
Для использования отображения необходимо включить соответствующий заголовочный файл:
#include <map>
Какие основные действия производятся над ассоциативными контейнерами? Их заполняют элементами или проверяют на наличие определенного элемента. В следующем подразделе мы покажем, как определить пару ключ/значение и как поместить такие пары в контейнер. Далее мы расскажем, как сформулировать запрос на поиск элемента и извлечь значение, если элемент существует.
Библиотека iostream
поддерживает операции над строковыми объектами в памяти. Класс ostringstream
вставляет символы в строку, istringstream читает символы из строкового объекта, а stringstream
может использоваться как для чтения, так и для записи. Чтобы работать со строковым потоком, в программу необходимо включить заголовочный файл
#include <sstream>
Например, следующая функция читает весь файл alice_emma в объект buf
класса ostringstream. Размер buf
увеличивается по мере необходимости, чтобы вместить все символы:
|
#include <string> #include <fstream> #include <sstream> string read_file_into_string() { ifstream ifile( "alice_emma" ); ostringstream buf; char ch; while ( buf && ifile.get( ch )) buf.put( ch ); return buf.str(); |
}
Функция-член str()
возвращает строку – объект класса string, ассоциированный со строковым потоком ostringstream. Этой строкой можно манипулировать так же, как и “обычным” объектом класса string. Например, в следующей программе text почленно инициализируется строкой, ассоциированной с buf:
|
int main() { string text = read_file_into_string(); // запомнить позиции каждого символа новой строки vector< string::size_type > lines_of_text; string::size_type pos = 0; while ( pos != string::npos ) { pos = text.find( '\n' pos ); lines_of_text.push_back( pos ); } // ... |
}
Объект класса ostringstream
можно использовать для автоматического форматирования составной строки, т.е. строки, составленной из данных разных типов. Так, следующий оператор вывода автоматически преобразует любой арифметический тип в соответствующее строковое представление, поэтому заботиться о выделении нужного количества памяти нет необходимости:
|
#include <iostream> #include <sstream> int main() { int ival = 1024; int *pival = &ival; double dval = 3.14159; double *pdval = &dval; ostringstream format_message; // преобразование значений в строковое представление format_message << "ival: " << ival << " адрес ival: " << pival << 'n' << "dval: " << dval << " адрес dval: " << pdval << endl; string msg = format_message.str(); cout << " размер строки сообщения: " << msg.size() << " сообщение: " << msg << endl; |
string format( string msg, int expected, int received ) { ostringstream message; message << msg << " ожидалось: " << expected << " принято: " << received << "\n"; return message.str(); } string format( string msg, vector<int> *values ); |
#include <iostream> #include <sstream> #include <string> int main() { int ival = 1024; int *pival = &ival; double dval = 3.14159; double *pdval = &dval; // создает строку, в которой значения разделены пробелами ostringstream format_string; format_string << ival << " " << pival << " " << dval << " " << pdval << endl; // извлекает сохраненные значения в коде ASCII // и помещает их в четыре разных объекта istringstream input_istring( format_string.str() ); input_istring >> ival >> pival >> dval >> pdval; |
int ival = 1024; double dval = 3.14159; char cval = 'a'; char *sval = "the end"; printf( "ival: %d\tdval% %g\tcval: %c\tsval: %s", |
%d целое число %g число с плавающей точкой %c char |
В С++ поддерживаются два типа строк– встроенный тип, доставшийся от С, и класс string из стандартной библиотеки С++. Класс string предоставляет гораздо больше возможностей и поэтому удобней в применении, однако на практике нередки ситуации, когда необходимо пользоваться встроенным типом либо хорошо понимать, как он устроен. (Одним из примеров может являться разбор параметров командной строки, передаваемых в функцию main(). Мы рассмотрим это в главе 7.)
Считывание можно производить как в C-строки, так и в объекты класса string. Мы рекомендуем пользоваться последними. Их главное преимущество – автоматическое управление памятью для хранения символов. Чтобы прочитать данные в C-строку, т.е. массив символов, необходимо сначала задать его размер, достаточный для хранения строки. Обычно мы читаем символы в буфер, затем выделяем из хипа ровно столько памяти, сколько нужно для хранения прочитанной строки, и копируем данные из буфера в эту память:
|
#include <iostream> #include <string.h> char inBuf[ 1024 ]; try { while ( cin >> inBuf ) { char *str = new char[ strlen( inBuf ) + 1 ]; strcpy( str, inBuf ); // ... сделать что-то с массивом символов str delete [] str; } } |
catch( ... ) { delete [] str; throw; }
Работать с типом string
значительно проще:
|
#include <iostream> #include <string.h> string str; while ( cin >> str ) |
// ... сделать что-то со строкой
Рассмотрим операторы ввода в C-строки и в объекты класса string. В качестве входного текста по-прежнему будет использоваться рассказ об Алисе Эмме:
Alice Emma has long flowing red hair. Her Daddy says
when the wind blows through her hair, it looks almost
alive, like a fiery bird in flight. A beautiful fiery
bird, he tells her, magical but untamed. "Daddy, shush,
there is no such creature," she tells him, at the same time
wanting him to tell her more. Shyly, she asks, "I mean,
Daddy, is there?"
Поместим этот текст в файл alice_emma, а затем перенаправим на него стандартный вход программы. Позже, когда мы познакомимся с файловым вводом, мы откроем и прочтем этот файл непосредственно. Следующая программа помещает прочитанные со стандартного ввода слова в C-строку и находит самое длинное слово:
|
#include <iostream.h> #include <string.h> int main() { const int bufSize = 24; char buf[ bufSize ], largest[ bufSize ]; // для хранения статистики int curLen, max = -1, cnt = 0; while ( cin >> buf ) { curLen = strlen( buf ); ++cnt; // новое самое длинное слово? сохраним его if ( curLen > max ) { max = curLen; strcpy( largest, buf ); } } cout << "Число прочитанных слов " << cnt << endl; cout << "Длина самого длинного слова " << max << endl; cout << "Самое длинное слово " << largest << endl; |
#include <iostream> #include <iomanip> int main() { const int bufSize = 24; char buf[ bufSize ]; char *pbuf = buf; // если строка длиннее, чем sizeof(char*), // она разбивается на несколько строк while ( cin >> setw( sizeof( pbuf )) >> pbuf ) cout << pbuf << endl; |
#include <iostream.h> #include <string> int main() { string buf, largest; // для хранения статистики int curLen, // длина текущего слова max = -1, // максимальная длина слова cnt = 0; // счетчик прочитанных слов while ( cin >> buf ) { curLen = buf.size(); ++cnt; // новое самое длинное слово? сохраним его if ( curLen > max ) { max = curLen; largest = buf; } } cout << "Число прочитанных слов " << cnt << endl; cout << "Длина самого длинного слова " << max << endl; cout << "Самое длинное слово " << largest << endl; |
#include <string> void filter_string( string &str ) { // элементы, подлежащие фильтрации string filt_elems( "\",?." ); string::size_type pos = 0; while (( pos = str.find_first_of( filt_elems, pos )) != string::npos ) str.erase( pos, 1 ); |
#include <string> void filter_string( string &str, string filt_elems = string("\",.")) { string::size_type pos = 0; while (( pos = str.find_first_of( filt_elems, pos )) != string::npos ) str.erase( pos, 1 ); |
template <class InputIterator> void filter_string( InputIterator first, InputIterator last, string filt_elems = string("\",.")) { for ( ; first != last; first++ ) { string::size_type pos = 0; while (( pos = (*first).find_first_of( filt_elems, pos )) != string::npos ) (*first).erase( pos, 1 ); } |
#include <string> #include <algorithm> #include <iterator> #include <vector> #include <iostream> bool length_less( string s1, string s2 ) { return s1.size() < s2.size(); } int main() { istream_iterator< string > input( cin ), eos; vector< string > text; // copy - это обобщенный алгоритм copy( input, eos, back_inserter( text )); string filt_elems( "\",.;:"); filter_string( text.begin(), text.end(), filt_elems ); int cnt = text.size(); // max_element - это обобщенный алгоритм string *max = max_element( text.begin(), text.end(), length_less ); int len = max->size(); cout << "Число прочитанных слов " << cnt << endl; cout << "Длина самого длинного слова " << len << endl; cout << "Самое длинное слово " << *max << endl; |
Объект типа bool
может принимать одно из двух значений: true и false. Например:
|
// инициализация строки string search_word = get_word(); // инициализация переменной found bool found = false; string next_word; while ( cin >> next_word ) if ( next_word == search_word ) found = true; // ... // сокращенная запись: if ( found == true ) if ( found ) cout << "ok, мы нашли слово\n"; |
else cout << "нет, наше слово не встретилось.\n";
Хотя bool
относится к одному из целых типов, он не может быть объявлен как signed, unsigned, short или long, поэтому приведенное определение ошибочно:
|
// ошибка |
short bool found = false;
Объекты типа bool
неявно преобразуются в тип int. Значение true
превращается в 1, а false – в 0. Например:
|
bool found = false; int occurrence_count = 0; while ( /* mumble */ ) { found = look_for( /* something */ ); // значение found преобразуется в 0 или 1 occurrence_count += found; |
}
Таким же образом значения целых типов и указателей могут быть преобразованы в значения типа bool. При этом 0 интерпретируется как false, а все остальное как true:
|
// возвращает количество вхождений extern int find( const string& ); bool found = false; if ( found = find( "rosebud" )) // правильно: found == true // возвращает указатель на элемент extern int* find( int value ); if ( found = find( 1024 )) |
// правильно: found == true
Указателю на функцию нельзя присвоить адрес функции-члена, даже если типы возвращаемых значений и списки параметров полностью совпадают. Например, переменная pfi – это указатель на функцию без параметров, которая возвращает значение типа int:
int (*pfi)();
Если имеются глобальные функции HeightIs() и WidthIs()
вида:
|
int HeightIs(); |
int WidthIs();
то допустимо присваивание pfi
адреса любой из этих переменных:
|
pfi = HeightIs; |
pfi = WidthIs;
В классе Screen
также определены две функции доступа, height() и width(), не имеющие параметров и возвращающие значение типа int:
|
inline int Screen::height() { return _height; } |
inline int Screen::width() { return _width; }
Однако попытка присвоить их переменной pfi является нарушением типизации и влечет ошибку компиляции:
|
// неверное присваивание: нарушение типизации pfi = &Screen::height; |
В чем нарушение? У функций-членов есть дополнительный атрибут типа, отсутствующий у функций, не являющихся членами, – класс. Указатель на функцию-член должен соответствовать типу присваиваемой ему функции не в двух, а в трех отношениях: по типу и количеству формальных параметров; типу возвращаемого значения; типу класса, членом которого является функция.
Несоответствие типов между двумя указателями – на функцию-член и на обычную функцию – обусловлено их разницей в представлении. В указателе на обычную функцию хранится ее адрес, который можно использовать для непосредственного вызова. (Указатели на функции рассматривались в разделе 7.9.) Указатель же на функцию-член должен быть сначала привязан к объекту или указателю на объект, чтобы получить this, и только после этого он применяется для вызова функции-члена. (В следующем подразделе мы покажем, как осуществить такую привязку.) Хотя для указателя на обычную функцию и для указателя на функцию-член используется один и тот же термин, их природа различна.
Синтаксис объявления указателя на функцию-член должен принимать во внимание тип класса. То же верно и в отношении указателей на данные-члены. Рассмотрим член _height
// всем указателям на функции-члены класса можно присвоить значение 0 int (Screen::*pmf1)() = 0; int (Screen::*pmf2)() = &Screen::height; pmf1 = pmf2; |
typedef Screen& (Screen::*Action)(); Action default = &Screen::home; |
Screen meScreen; typedef Screen& (Screen::*Action)(); Action default = &Screen::home; extern Screen& action( Screen&, Sction = &Screen::display ); void ff() { action( myScreen ); action( myScreen, default ); action( myScreen, &Screen::end ); |
Мы уже касались массивов в разделе 2.1. Массив – это набор элементов одного типа, доступ к которым производится по индексу – порядковому номеру элемента в массиве. Например:
int ival;
определяет ival как переменную типа int, а инструкция
int ia[ 10 ];
задает массив из десяти объектов типа int. К каждому из этих объектов, или элементов массива, можно обратиться с помощью операции взятия индекса:
ival = ia[ 2 ];
присваивает переменной ival
значение элемента массива ia с индексом 2. Аналогично
ia[ 7 ] = ival;
присваивает элементу с индексом 7 значение ival.
Определение массива состоит из спецификатора типа, имени массива и размера. Размер задает количество элементов массива (не менее 1) и заключается в квадратные скобки. Размер массива нужно знать уже на этапе компиляции, а следовательно, он должен быть константным выражением, хотя не обязательно задается литералом. Вот примеры правильных и неправильных определений массивов:
|
extern int get_size(); // buf_size и max_files константы const int buf_size = 512, max_files = 20; int staff_size = 27; // правильно: константа char input_buffer[ buf_size ]; // правильно: константное выражение: 20 - 3 char *fileTable[ max_files-3 ]; // ошибка: не константа double salaries[ staff_size ]; // ошибка: не константное выражение |
int test_scores[ get_size() ];
Объекты buf_size и max_files
являются константами, поэтому определения массивов input_buffer и fileTable
правильны. А вот staff_size – переменная (хотя и инициализированная константой 27), значит, salaries[staff_size] недопустимо. (Компилятор не в состоянии найти значение переменной staff_size в момент определения массива salaries.)
Выражение max_files-3
может быть вычислено на этапе компиляции, следовательно, определение массива fileTable[max_files-3]
синтаксически правильно.
Нумерация элементов начинается с 0, поэтому для массива из 10 элементов правильным диапазоном индексов является не 1 – 10, а 0 – 9. Вот пример перебора всех элементов массива:
int main() { const int array_size = 10; int ia[ array_size ]; for ( int ix = 0; ix < array_size; ++ ix ) ia[ ix ] = ix; |
const int array_size = 3; |
// массив размера 3 |
// ia ==> { 0, 1, 2, 0, 0 } const int array_size = 5; |
const char cal[] = {'C', '+', '+' }; |
// ошибка: строка "Daniel" состоит из 7 элементов |
const int array_size = 3; int ix, jx, kx; // правильно: массив указателей типа int* int *iar [] = { &ix, &jx, &kx }; // error: массивы ссылок недопустимы int &iar[] = { ix, jx, kx }; int main() { int ia3{ array_size ]; // правильно // ошибка: встроенные массивы нельзя копировать ia3 = ia; return 0; |
const int array_size = 7; int ia1[] = { 0, 1, 2, 3, 4, 5, 6 }; int main() { int ia3[ array_size ]; for ( int ix = 0; ix < array_size; ++ix ) ia2[ ix ] = ia1[ ix ]; return 0; |
int someVal, get_index(); |
(a) int ia[ buf_size ]; (d) int ia[ 2 * 7 - 14 ] (b) int ia[ get_size() ]; (e) char st[ 11 ] = "fundamental"; |
int main() { const int array_size = 10; int ia[ array_size ]; for ( int ix = 1; ix <= array_size; ++ix ) ia[ ia ] = ix; // ... |
Как объявить указатель на функцию? Как выглядит формальный параметр, когда фактическим аргументом является такой указатель? Вот определение функции lexicoCompare(), которая сравнивает две строки лексикографически:
|
#include <string> int lexicoCompare( const string &sl, const string &s2 ) { return sl.compare(s2); |
}
Если все символы строк s1 и s2
равны, lexicoCompare()
вернет 0, в противном случае– отрицательное число, если s1
меньше чем s2, и положительное, если s1
больше s2.
Имя функции не входит в ее сигнатуру – она определяется только типом возвращаемого значения и списком параметров. Указатель на lexicoCompare()
должен адресовать функцию с той же сигнатурой. Попробуем написать так:
|
int *pf( const string &, const string & ) ; |
// нет, не совсем так
Эта инструкция почти правильна. Проблема в том, что компилятор интерпретирует ее как объявление функции с именем pf, которая возвращает указатель типа int*. Список параметров правилен, но тип возвращаемого значения не тот. Оператор разыменования (*) ассоциируется с данным типом (int в нашем случае), а не с pf. Чтобы исправить положение, нужно использовать скобки:
|
int (*pf)( const string &, const string & ) ; |
// правильно
pf объявлен как указатель на функцию с двумя параметрами, возвращающую значение типа int, т.е. такую, как lexicoCompare().
pf способен адресовать и приведенную ниже функцию, поскольку ее сигнатура совпадает с типом lexicoCompare():
int sizeCompare( const string &sl, const string &s2 );
Функции calc() и gcd()другого типа, поэтому pf не может указывать на них:
|
int calc( int , int ); |
int gcd( int , int );
Указатель, который адресует эти две функции, определяется так:
int (*pfi)( int, int );
Многоточие является частью сигнатуры функции. Если у двух функций списки параметров отличаются только тем, что в конце одного из них стоит многоточие, то считается, что функции различны. Таковы же и типы указателей.
|
int printf( const char*, ... ); int strlen( const char* ); int (*pfce)( const char*, ... ); // может указывать на printf() |
int (*pfc)( const char* ); // может указывать на strlen()
Типов функций столько, сколько комбинаций типов возвращаемых значений и списков параметров.
Тип возвращаемого функцией значения бывает встроенным, как int или double, составным, как int& или double*, или определенным пользователем– перечислением или классом. Можно также использовать специальное ключевое слово void, которое говорит о том, что функция не возвращает никакого значения:
|
#include <string> #include <vector> class Date { /* определение */ }; bool look_up( int *, int ); double calc( double ); int count( const string &, char ); Date& calendar( const char ); |
void sum( vector<int>&, int );
Однако функция или встроенный массив не могут быть типом возвращаемого значения. Следующий пример ошибочен:
|
// массив не может быть типом возвращаемого значения |
int[10] foo_bar();
Но можно вернуть указатель на первый элемент массива:
|
// правильно: указатель на первый элемент массива |
int *foo_bar();
(Размер массива должен быть известен вызывающей программе.)
Функция может возвращать типы классов, в частности контейнеры. Например:
|
// правильно: возвращается список символов |
list<char> foo_bar();
(Этот подход не очень эффективен. Обсуждение типа возвращаемого значения см. в разделе 7.4.)
Тип возвращаемого функцией значения должен быть явно указан. Приведенный ниже код вызывает ошибку компиляции:
|
// ошибка: пропущен тип возвращаемого значения |
const is_equa1( vector<int> vl, vector<int> v2 );
В предыдущих версиях С++ в подобных случаях считалось, что функция возвращает значение типа int. Стандарт С++ отменил это соглашение. Правильное объявление is_equal()
выглядит так:
|
// правильно: тип возвращаемого значения указан |
const bool is_equa1( vector<int> vl, vector<int> v2 );
В этой главе приводится обзор встроенных, или элементарных, типов данных языка С++. Она начинается с определения литералов, таких, как 3.14159 или pi, а затем вводится понятие переменной, или объекта, который должен принадлежать к одному из типов данных. Оставшаяся часть главы посвящена подробному описанию каждого встроенного типа. Кроме того, приводятся производные типы данных для строк и массивов, предоставляемые стандартной библиотекой С++. Хотя эти типы не являются элементарными, они очень важны для написания настоящих программ на С++, и нам хочется познакомить с ними читателя как можно раньше. Мы будем называть такие типы данных расширением базовых типов С++.