Инициализация класса
Рассмотрим следующее определение класса:
class Data {
public:
int ival;
char *ptr;
};
Чтобы безопасно пользоваться объектом класса, необходимо правильно инициализировать его члены. Однако смысл этого действия для разных классов различен. Например, может ли ival содержать отрицательное значение или нуль? Каковы правильные начальные значения обоих членов класса? Мы не ответим на эти вопросы, не понимая абстракции, представляемой классом. Если с его помощью описываются служащие компании, то ptr, вероятно, указывает на фамилию служащего, а ival – его уникальный номер. Тогда отрицательное или нулевое значения ошибочны. Если же класс представляет текущую температуру в городе, то допустимы любые значения ival. Возможно также, что класс Data представляет строку со счетчиком ссылок: в таком случае ival содержит текущее число ссылок на строку по адресу ptr. При такой абстракции ival инициализируется значением 1; как только значение становится равным 0, объект класса уничтожается.
Мнемонические имена класса и обоих его членов сделали бы, конечно, его назначение более понятным для читателя программы, но не дали бы никакой дополнительной информации компилятору. Чтобы компилятор понимал наши намерения, мы должны предоставить одну или несколько перегруженных функций инициализации – конструкторов. Подходящий конструктор выбирается в зависимости от множества начальных значений, указанных при определении объекта. Например, любая из приведенных ниже инструкций представляет корректную инициализацию объекта класса Data:
Data dat01( "Venus and the Graces", 107925 );
Data dat02( "about" );
Data dat03( 107925 );
Data dat04;
Бывают ситуации (как в случае с dat04), когда нам нужен объект класса, но его начальные значения мы еще не знаем. Возможно, они станут известны позже. Однако начальное значение задать необходимо, хотя бы такое, которое показывает, что разумное начальное значение еще не присвоено. Другими словами, инициализация объекта иногда сводится к тому, чтобы показать, что он еще не
инициализирован. Большинство классов предоставляют специальный конструктор по умолчанию, для которого не требуется задавать начальных значений. Как правило, он инициализирует объект таким образом, чтобы позже можно было понять, что реальной инициализации еще не проводилось.
Обязан ли наш класс Data иметь конструктор? Нет, поскольку все его члены открыты. Унаследованный из языка C механизм поддерживает явную инициализацию, аналогичную используемой при инициализации массивов:
int main()
{
// local1.ival = 0; local1.ptr = 0
Data local1 = { 0, 0 };
// local2.ival = 1024;
// local3.ptr = "Anna Livia Plurabelle"
Data.local2 - { 1024, "Anna Livia Plurabelle" };
// ...
}
Значения присваиваются позиционно, на основе порядка, в котором объявляются данные-члены. Следующий пример приводит к ошибке компиляции, так как ival объявлен перед ptr:
// ошибка: ival = "Anna Livia Plurabelle";
// ptr = 1024
Data.local2 - { "Anna Livia Plurabelle", 1024 };
Явная инициализация имеет два основных недостатка. Во-первых, она может быть применена лишь для объектов классов, все члены которых открыты (т.е. эта инициализация не поддерживает инкапсуляции данных и абстрактных типов – их не было в языке C, откуда она заимствована). А во-вторых, такая форма требует вмешательства программиста, что увеличивает вероятность появления ошибок (забыл включить список инициализации или перепутал порядок следования инициализаторов в нем).
Так нужно ли применять явную инициализацию вместо конструкторов? Да. Для некоторых приложений более эффективно использовать список для инициализации больших структур постоянными значениями. К примеру, мы можем таким образом построить палитру цветов или включить в текст программы фиксированные координаты вершин и значения в узлах сложной геометрической модели. В подобных случаях инициализация выполняется во время загрузки, что сокращает затраты времени на запуск конструктора, даже если он определен как встроенный. Это особенно удобно при работе с глобальными объектами1.
Однако в общем случае предпочтительным методом инициализации является конструктор, который гарантированно будет вызван компилятором для каждого объекта до его первого использования. В следующем разделе мы познакомимся с конструкторами детально.
Инициализация массива, распределенного из хипа *
По умолчанию инициализация массива объектов, распределенного из хипа, проходит в два этапа: выделение памяти для массива, к каждому элементу которого применяется конструктор по умолчанию, если он определен, и последующее присваивание значения каждому элементу.
Чтобы свести инициализацию к одному шагу, программист должен вмешаться и поддержать следующую семантику: задать начальные значения для всех или некоторых элементов массива и гарантировать применение конструктора по умолчанию для тех элементов, начальные значения которых не заданы. Ниже приведено одно из возможных программных решений, где используется оператор размещения new:
#include <utility>
#include <vector >
#include <new>
#include <cstddef>
#include "Accounts.h"
typedef pair<char*, double> value_pair;
/* init_heap_array()
* объявлена как статическая функция-член
* обеспечивает выделение памяти из хипа и инициализацию
* массива объектов
* init_values: пары начальных значений элементов массива
* elem_count: число элементов в массиве
* если 0, то размером массива считается размер вектора
* init_values
*/
Account*
Account::
init_heap_array(
vector<value_pair> &init_values,
vector<value_pair>::size_type elem_count = 0 )
{
vector<value_pair>::size_type
vec_size = init_value.size();
if ( vec_size == 0 && elem_count == 0 )
return 0;
// размер массива равен либо elem_count,
// либо, если elem_count == 0, размеру вектора ...
size_t elems = elem_count
? elem_count : vec_size();
// получить блок памяти для размещения массива
char *p = new char[sizeof(Account)*elems];
// по отдельности инициализировать каждый элемент массива
int offset = sizeof( Account );
for ( int ix = 0; ix < elems; ++ix )
{
// смещение ix-ого элемента
// если пара начальных значений задана,
// передать ее конструктору;
// в противном случае вызвать конструктор по умолчанию
if ( ix < vec_size )
new( p+offset*ix ) Account( init_values[ix].first,
init_values[ix].second );
else new( p+offset*ix ) Account;
}
// отлично: элементы распределены и инициализированы;
// вернуть указатель на первый элемент
return (Account*)p;
}
Необходимо заранее выделить блок памяти, достаточный для хранения запрошенного массива, как массив байт, чтобы избежать применения к каждому элементу конструктора по умолчанию. Это делается в такой инструкции:
char *p = new char[sizeof(Account)*elems];
Далее программа в цикле обходит этот блок, присваивая на каждой итерации переменной p адрес следующего элемента и вызывая либо конструктор с двумя параметрами, если задана пара начальных значений, либо конструктор по умолчанию:
for ( int ix = 0; ix < elems; ++ix )
{
if ( ix < vec_size )
new( p+offset*ix ) Account( init_values[ix].first,
init_values[ix].second );
else new( p+offset*ix ) Account;
}
В разделе 14.3 говорилось, что оператор размещения new позволяет применить конструктор класса к уже выделенной области памяти. В данном случае мы используем new для поочередного применения конструктора класса Account к каждому из выделенных элементов массива. Поскольку при создании инициализированного массива мы подменили стандартный механизм выделения памяти, то должны сами позаботиться о ее освобождении. Оператор delete работать не будет:
delete [] ps;
Почему? Потому что ps (мы предполагаем, что эта переменная была инициализирована вызовом init_heap_array()) указывает на блок памяти, полученный не с помощью стандартного оператора new, поэтому число элементов в массиве компилятору неизвестно. Так что всю работу придется сделать самим:
void
Account::
dealloc_heap_array( Account *ps, size_t elems )
{
for ( int ix = 0; ix < elems; ++ix )
ps[ix].Account::~Account();
delete [] reinterpret_cast<char*>(ps);
}
Если в функции инициализации мы пользовались арифметическими операциями над указателями для доступа к элементам:
new( p+offset*ix ) Account;
то здесь мы обращаемся к ним, задавая индекс в массиве ps:
ps[ix].Account::~Account();
Хотя и ps, и p адресуют одну и ту же область памяти, ps объявлен как указатель на объект класса Account, а p – как указатель на char. Индексирование p дало бы ix-й байт, а не ix-й объект класса Account. Поскольку с p ассоциирован не тот тип, что нужно, арифметические операции над указателями приходится программировать самостоятельно.
Мы объявляем обе функции статическими членами класса:
typedef pair<char*, double> value_pair;
class Account {
public:
// ...
static Account* init_heap_array(
vector<value_pair> &init_values,
vector<value_pair>::size_type elem_count = 0 );
static void dealloc_heap_array( Account*, size_t );
// ...
};
Инициализация, присваивание и уничтожение класса
В этой главе мы детально изучим автоматическую инициализацию, присваивание и уничтожение объектов классов в программе. Для поддержки инициализации служит конструктор– определенная проектировщиком функция (возможно, перегруженная), которая автоматически применяется к каждому объекту класса перед его первым использованием. Парная по отношению к конструктору функция, деструктор, автоматически применяется к каждому объекту класса по окончании его использования и предназначена для освобождения ресурсов, захваченных либо в конструкторе класса, либо на протяжении его жизни.
По умолчанию как инициализация, так и присваивание одного объекта класса другому выполняются почленно, т.е. путем последовательного копирования всех членов. Хотя этого обычно достаточно, при некоторых обстоятельствах такая семантика оказывается неадекватной. Тогда проектировщик класса должен предоставить специальный копирующий конструктор и копирующий оператор присваивания. Самое сложное в поддержке этих функций-членов – понять, что они должны быть написаны.
Инструкции
Мельчайшей независимой частью С++ программы является инструкция. Она соответствует предложению естественного языка, но завершается точкой с запятой (;), а не точкой. Выражение С++ (например, ival + 5) становится простой инструкцией, если после него поставить точку с запятой. Составная инструкция – это последовательность простых, заключенная в фигурные скобки. По умолчанию инструкции выполняются в порядке записи. Как правило, последовательного выполнения недостаточно для решения реальных задач. Специальные управляющие конструкции позволяют менять порядок действий в зависимости от некоторых условий и повторять составную инструкцию определенное количество раз. Инструкции if, if-else и switch обеспечивают условное выполнение. Повторение обеспечивается инструкциями цикла while, do-while и for.
Инструкции объявления
В С++ определение объекта, например
int ival;
рассматривается как инструкция объявления (хотя в данном случае более правильно было бы сказать определения). Ее можно использовать в любом месте программы, где разрешено употреблять инструкции. В следующем примере объявления помечены комментарием //#n, где n – порядковый номер.
#include <fstream>
#include <string>
#include <vector>
int main()
{
string fileName; // #1
cout << "Введите имя файла: ";
cin >> fileName;
if ( fileName.empty() ) {
// странный случай
cerr << "Пустое имя файла. Завершение работы.\n";
return -1;
}
ifstream inFile( fileName.c_str() ); // #2
if ( ! inFile ) {
cerr << "Невозможно открыть файл.\n";
return -2;
}
string inBuf; // #3
vector< string > text; // #4
while ( inFile >> inBuf ) {
for ( int ix = 0; ix < inBuf .size(); ++ix ) // #5
// можно обойтись без ch,
// но мы использовали его для иллюстрации
if (( char ch = inBuf[ix] )=='.'){ // #6
ch = '_';
inBuf[ix] = ch;
}
text.push_back( inBuf );
}
if ( text.empty() )
return 0;
// одна инструкция объявления,
// определяющая сразу два объекта
vector<string>::iterator iter = text.begin(), // #7
iend = text.end();
while ( iter != -iend ) {
cout << *iter << '\n';
++iter;
}
return 0;
}
Программа содержит семь инструкций объявления и восемь определений объектов. Объявления действуют локально; переменная объявляется непосредственно перед первым использованием объекта.
В 70-е годы философия программирования уделяла особое внимание тому, чтобы определения всех объектов находились в начале программы или блока, перед исполняемыми инструкциями. (В С, например, определение переменной не является инструкцией и обязано располагаться в начале блока.) В некотором смысле это была реакция на идиому использования переменных без предварительного объявления, чреватую ошибками. Такую идиому поддерживал, например, FORTRAN.
// передать ее конструктору;
// в противном случае вызвать конструктор по умолчанию
if ( ix < vec_size )
new( p+offset*ix ) Account( init_values[ix].first,
init_values[ix].second );
else new( p+offset*ix ) Account;
}
// отлично: элементы распределены и инициализированы;
// вернуть указатель на первый элемент
return (Account*)p;
}
Необходимо заранее выделить блок памяти, достаточный для хранения запрошенного массива, как массив байт, чтобы избежать применения к каждому элементу конструктора по умолчанию. Это делается в такой инструкции:
char *p = new char[sizeof(Account)*elems];
Далее программа в цикле обходит этот блок, присваивая на каждой итерации переменной p адрес следующего элемента и вызывая либо конструктор с двумя параметрами, если задана пара начальных значений, либо конструктор по умолчанию:
for ( int ix = 0; ix < elems; ++ix )
{
if ( ix < vec_size )
new( p+offset*ix ) Account( init_values[ix].first,
init_values[ix].second );
else new( p+offset*ix ) Account;
}
В разделе 14.3 говорилось, что оператор размещения new позволяет применить конструктор класса к уже выделенной области памяти. В данном случае мы используем new для поочередного применения конструктора класса Account к каждому из выделенных элементов массива. Поскольку при создании инициализированного массива мы подменили стандартный механизм выделения памяти, то должны сами позаботиться о ее освобождении. Оператор delete работать не будет:
delete [] ps;
Почему? Потому что ps (мы предполагаем, что эта переменная была инициализирована вызовом init_heap_array()) указывает на блок памяти, полученный не с помощью стандартного оператора new, поэтому число элементов в массиве компилятору неизвестно. Так что всю работу придется сделать самим:
void
Account::
dealloc_heap_array( Account *ps, size_t elems )
{
for ( int ix = 0; ix < elems; ++ix )
ps[ix].Account::~Account();
delete [] reinterpret_cast<char*>(ps);
}
Если в функции инициализации мы пользовались арифметическими операциями над указателями для доступа к элементам:
new( p+offset*ix ) Account;
то здесь мы обращаемся к ним, задавая индекс в массиве ps:
ps[ix].Account::~Account();
Хотя и ps, и p адресуют одну и ту же область памяти, ps объявлен как указатель на объект класса Account, а p – как указатель на char. Индексирование p дало бы ix-й байт, а не ix-й объект класса Account. Поскольку с p ассоциирован не тот тип, что нужно, арифметические операции над указателями приходится программировать самостоятельно.
Мы объявляем обе функции статическими членами класса:
typedef pair<char*, double> value_pair;
class Account {
public:
// ...
static Account* init_heap_array(
vector<value_pair> &init_values,
vector<value_pair>::size_type elem_count = 0 );
static void dealloc_heap_array( Account*, size_t );
// ...
};
Инструкция break
Инструкция break останавливает циклы for, while, do while и блока switch. Выполнение программы продолжается с инструкции, следующей за закрывающей фигурной скобкой цикла или блока. Например, данная функция ищет в массиве целых чисел определенное значение. Если это значение найдено, функция сообщает его индекс, в противном случае она возвращает -1. Вот как выглядит реализация функции:
// возвращается индекс элемента или -1
int search( int *ia, int size, int value )
{
// проверка что ia != 0 и size > 0 ...
int loc = -1;
for ( int ix = 0; ix < size; ++ix ) {
if ( value == ia[ ix ] ) {
// нашли!
// запомним индекс и выйдем из цикла
1oc = ix;
break;
}
} // конец цикла
// сюда попадаем по break ...
return 1oc;
}
В этом примере break прекращает выполнение цикла for и передает управление инструкции, следующей за этим циклом, – в нашем случае return. Заметим, что break выводит из блока, относящегося к инструкции for, а не if, хотя является частью составной инструкции, соответствующей if. Использование break внутри блока if, не входящего в цикл или в switch, является синтаксической ошибкой:
// ошибка: неверное использование break
if ( ptr ) {
if ( *ptr == "quit" )
break;
// ...
}
Если эта инструкция используется внутри вложенных циклов или инструкций switch, она завершает выполнение того внутреннего блока, в котором находится. Цикл или switch, включающий тот цикл или switch, из которого мы вышли с помощью break, продолжает выполняться. Например:
white ( cin >> inBuf )
{
switch( inBuf[ 0 ] ) {
case '-':
for ( int ix = 1; ix < inBuf.size(); ++ix ) {
if ( inBuf[ ix ] == ' ' )
break; // #1
// ...
// ...
}
break; // #2
case '+':
// ...
}
}
Инструкция break, помеченная // #1, завершает выполнение цикла for внутри ветви case '-' блока switch, но не сам switch. Аналогично break // #2 завершает выполнение блока switch, но не цикла while, в который тот входит.
Инструкция цикла for
Как мы видели, выполнение программы часто состоит в повторении последовательности инструкций – до тех пор, пока некоторое условие остается истинным. Например, мы читаем и обрабатываем записи файла, пока не дойдем до его конца, перебираем элементы массива, пока индекс не станет равным размерности массива минус 1, и т.д. В С++ предусмотрено три инструкции для организации циклов, в частности for и while, которые начинаются проверкой условия. Такая проверка означает, что цикл может закончиться без выполнения связанной с ним простой или составной инструкции. Третий тип цикла, do while, гарантирует, что тело будет выполнено как минимум один раз: условие цикла проверяется по его завершении. (В этом разделе мы детально рассмотрим цикл for; в разделе 5.6 разберем while, а в разделе 5.7– do while.)
Цикл for обычно используется для обработки структур данных, имеющих фиксированную длину, таких, как массив или вектор:
#include <vector>
int main() {
int ia[ 10 ];
for ( int ix = 0; ix < 10; ++-ix )
ia[ ix ] = ix;
vector<int> ivec( ia, ia+10 );
vector<int>::iterator iter = ivec.begin() ;
for ( ; iter != ivec.end(); ++iter )
*iter *= 2;
return 0;
}
Синтаксис цикла for следующий:
for (инструкция-инициализации; условие; выражение )
инструкция
инструкция-инициализации может быть либо выражением, либо инструкцией объявления. Обычно она используется для инициализации переменной значением, которое увеличивается в ходе выполнения цикла. Если такая инициализация не нужна или выполняется где-то в другом месте, эту инструкцию можно заменить пустой (см. второй из приведенных ниже примеров). Вот примеры правильного использования инструкции-инициализации:
// index и iter определены в другом месте
for ( index =0; ...
for ( ; /* пустая инструкция */ ...
for ( iter = ivec.begin(); ...
for ( int 1o = 0,hi = max; ...
for ( char *ptr = getStr(); ...
условие служит для управления циклом. Пока условие при вычислении дает true, инструкция продолжает выполняться. Выполняемая в цикле инструкция может быть как простой, так и составной. Если же самое первое вычисление условия дает false, инструкция не выполняется ни разу. Правильные условия можно записать так:
(... index < arraySize; ... )
(... iter != ivec.end(); ... )
(... *stl++ = *st2++; ... )
(... char ch = getNextChar(); ... )
Выражение вычисляется после выполнения инструкции на каждой итерации цикла. Обычно его используют для модификации переменной, инициализированной в инструкции-инициализации. Если самое первое вычисление условия дает false, выражение не выполняется ни разу. Правильные выражения выглядят таким образом:
( ... ...; ++-index )
( ... ...; ptr = ptr->next )
( ... ...; ++i, --j, ++cnt )
( ... ...; ) // пустое выражение
Для приведенного ниже цикла for
const int sz = 24;
int ia[ sz ];
vector<int> ivec( sz );
for ( int ix = 0; ix < sz; ++ix ) {
ivec[ ix ] = ix;
ia[ ix ]= ix;
}
порядок вычислений будет следующим:
1. инструкция-инициализации выполняется один раз перед началом цикла. В данном примере объявляется переменная ix, которая инициализируется значением 0.
2. Вычисляется условие. Если оно равно true, выполняется составная инструкция тела цикла. В нашем примере, пока ix меньше sz, значение ix присваивается элементам ivec[ix] и ia[ix]. Когда значением условия станет false, выполнение цикла прекратится. Если самое первое вычисление условия даст false, составная инструкция выполняться не будет.
3. Вычисляется выражение. Как правило, его используют для модификации переменной, фигурирующей в инструкции-инициализации и проверяемой в условии. В нашем примере ix увеличивается на 1.
Эти три шага представляют собой полную итерацию цикла for. Теперь шаги 2 и 3 будут повторяться до тех пор, пока условие не станет равным false, т.е. ix окажется равным или большим sz.
В инструкции-инициализации можно определить несколько объектов, однако все они должны быть одного типа, так как инструкция объявления допускается только одна:
for ( int ival = 0, *pi = &ia, &ri = val;
ival < size;
++iva1, ++pi, ++ri )
// ...
Объявление объекта в условии гораздо труднее правильно использовать: такое объявление должно хотя бы раз дать значение false, иначе выполнение цикла никогда не прекратится. Вот пример, хотя и несколько надуманный:
#include <iostream>
int main()
{
for ( int ix = 0;
bool done = ix == 10;
++ix )
cout << "ix: " << ix << endl;
}
Видимость всех объектов, определенных внутри круглых скобок инструкции for, ограничена телом цикла. Например, проверка iter после цикла вызовет ошибку компиляции[8]:
int main()
{
string word;
vector< string > text;
// ...
for ( vector< string >::iterator
iter = text.begin(),
iter_end = text.end();
iter != text.end(); ++iter )
{
if ( *iter == word )
break;
// ...
}
// ошибка: iter и iter_end невидимы
if ( iter != iter_end )
// ...
Упражнение 5.8
Допущены ли ошибки в нижеследующих циклах for? Если да, то какие?
(a)
for ( int *ptr = &ia, ix = 0;
ix < size && ptr != ia+size;
++ix, ++ptr )
// ...
(b)
for ( ; ; ) {
if ( some_condition )
break;
// ...
}
(c)
for ( int ix = 0; ix < sz; ++ix )
// ...
if ( ix != sz )
// ...
(d)
int ix;
for ( ix < sz; ++ix )
// ...
(e)
for ( int ix = 0; ix < sz; ++ix, ++ sz )
// ...
Упражнение 5.9
Представьте, что вам поручено придумать общий стиль использования цикла for в вашем проекте. Объясните и проиллюстрируйте примерами правила использования каждой из трех частей цикла.
Упражнение 5.10
Дано объявление функции:
bool is_equa1( const vector<int> &vl,
const vector<int> &v2 );
Напишите тело функции, определяющей равенство двух векторов. Для векторов разной длины сравнивайте только то количество элементов, которое соответствует меньшему из двух. Например, векторы (0,1,1,2) и (0,1,1,2,3,5,8) считаются равными. Длину векторов можно узнать с помощью функций v1.size() и v2.size().
Инструкция continue
Инструкция continue завершает текущую итерацию цикла и передает управление на вычисление условия, после чего цикл может продолжиться. В отличие от инструкции break, завершающей выполнение всего цикла, инструкция continue завершает выполнение только текущей итерации. Например, следующий фрагмент программы читает из входного потока по одному слову. Если слово начинается с символа подчеркивания, оно обрабатывается, в противном случае программа переходит к новому слову.
while ( cin >> inBuf ) {
if ( inBuf[0] '= '_' )
continue; // завершение итерации
// обработка слова ...
}
Инструкция continue может быть использована только внутри цикла.
Инструкция do while
Представим, что нам надо написать программу, переводящую мили в километры. Структура программы выглядит так:
int val;
bool more = true; // фиктивное значение, нужное для
// начала цикла
while ( more ) {
val = getValue();
val = convertValue(val);
printValue(val);
more = doMore();
}
Проблема заключается в том, что условие вычисляется в теле цикла. for и while требуют, чтобы значение условия равнялось true до первого вхождения в цикл, иначе тело не выполнится ни разу. Это означает, что мы должны обеспечить такое условие до начала работы цикла. Альтернативой может служить использование do while, гарантирующего выполнение тела цикла хотя бы один раз. Синтаксис цикла do while таков:
do
инструкция
while ( условие );
инструкция выполняется до первой проверки условия. Если вычисление условия дает false, цикл останавливается. Вот как выглядит предыдущий пример с использованием цикла do while:
do {
val = getValue();
val = convertValue(val);
printValue(val);
} while doMore();
В отличие от остальных инструкций циклов, do while не разрешает объявлять объекты в своей части условия. Мы не можем написать:
// ошибка: объявление переменной
// в условии не разрешается
do {
// ...
mumble( foo );
} while ( int foo = get_foo() ) // ошибка
потому что до условной части инструкции do while мы дойдем только после первого выполнения тела цикла.
Упражнение 5.14
Какие ошибки допущены в следующих циклах do while:
(a)
do
string rsp;
int vail, va12;
cout << "Введите два числа: ";
c-in >> vail >> va12;
cout << "Сумма " << vail
<< " и " << va12
<< " = " << vail + va12 << "\n\n"
<< "Продолжить? [да][нет] ";
cin >> rsp;
while ( rsp[0] != 'n' );
(b)
do {
// ...
} while ( int iva1 = get_response() );
(c)
do {
int ival = get_response();
if ( iva1 == some_value() )
break;
} while ( iva1 );
if ( !iva1 )
// ...
Упражнение 5.15
Напишите небольшую программу, которая запрашивает у пользователя две строки и печатает результат лексикографического сравнения этих строк (строка считается меньшей, если идет раньше при сортировке по алфавиту). Пусть она повторяет эти действия, пока пользователь не даст команду закончить. Используйте тип string, сравнение строк и цикл do while.
Инструкция goto
Инструкция goto обеспечивает безусловный переход к другой инструкции внутри той же функции, поэтому современная практика программирования выступает против ее применения.
Синтаксис goto следующий:
goto метка;
где метка– определенный пользователем идентификатор. Метка ставится перед инструкцией, на которую можно перейти с помощью goto, и должна заканчиваться двоеточием. Нельзя ставить метку непосредственно перед закрывающей фигурной скобкой. Если же это необходимо, их следует разделить пустой инструкцией:
end: ; // пустая инструкция
}
Переход через инструкцию объявления в том же блоке с помощью goto невозможен. Например, данная функция вызывает ошибку компиляции:
int oops_in_error() {
// mumble ...
goto end;
// ошибка: переход через объявление
int ix = 10;
// ... код, использующий ix
end: ;
}
Правильная реализация функции помещает объявление ix и использующие его инструкции во вложенный блок:
int oops_in_error() {
// mumble ...
goto end;
{
// правильно: объявление во вложенном блоке
int ix = 10;
// ... код, использующий ix
}
end: ;
}
Причина такого ограничения та же, что и для объявлений внутри блока switch: компилятор должен гарантировать, что для объявленного объекта конструктор и деструктор либо выполняются вместе, либо ни один из них не выполняется. Это и достигается заключением объявления во вложенный блок.
Переход назад через объявление, однако, не считается ошибкой. Почему? Перепрыгнуть через инициализацию объекта нельзя, но проинициализировать один и тот же объект несколько раз вполне допустимо, хотя это может привести к снижению эффективности. Например:
// переход назад через объявление не считается ошибкой.
void
mumble ( int max_size )
{
begin:
int sz = get_size();
if ( sz <= 0 ) {
// выдать предупреждение ...
goto end;
}
else
if ( sz > max_size )
// получить новое значение sz
goto begin;
{ // правильно: переход через целый блок
int ia = new int[ sz ];
doit( ia, sz ) ;
delete [] ia;
}
end:
;
}
Использование инструкции goto резко критикуется во всех современных языках программирования. Ее применение приводит к тому, что ход выполнения программы становится трудно понять и, следовательно, такую программу трудно модифицировать. В большинстве случаев goto можно заменить на инструкции if или циклы. Если вы все-таки решили использовать goto, не перескакивайте через большой фрагмент кода, чтобы можно было легко найти начало и конец вашего перехода.
Инструкция if
Инструкция if обеспечивает выполнение или пропуск инструкции или блока в зависимости от условия. Ее синтаксис таков:
if ( условие )
инструкция
условие заключается в круглые скобки. Оно может быть выражением, как в этом примере:
if(a+b>c) { ... }
или инструкцией объявления с инициализацией:
if ( int ival = compute_value() ){...}
Область видимости объекта, объявленного в условной части, ограничивается ассоциированной с if инструкцией или блоком. Например, такой код вызывает ошибку компиляции:
if ( int ival = compute_value() ) {
// область видимости ival
// ограничена этим блоком
}
// ошибка: ival невидим
if ( ! ival ) ...
Попробуем для иллюстрации применения инструкции if реализовать функцию min(), возвращающую наименьший элемент вектора. Заодно наша функция будет подсчитывать число элементов, равных минимуму. Для каждого элемента вектора мы должны проделать следующее:
1. Сравнить элемент с текущим значением минимума.
2. Если элемент меньше, присвоить текущему минимуму значение элемента и сбросить счетчик в 1.
3. Если элемент равен текущему минимуму, увеличить счетчик на 1.
4. В противном случае ничего не делать.
5. После проверки последнего элемента вернуть значение минимума и счетчика.
Необходимо использовать две инструкции if:
if ( minVal > ivec[ i ] )...// новое значение minVal
if ( minVal == ivec[ i ] )...// одинаковые значения
Довольно часто программист забывает использовать фигурные скобки, если нужно выполнить несколько инструкций в зависимости от условия:
if ( minVal > ivec[ i ] )
minVal = ivec[ i ];
occurs = 1; // не относится к if!
Такую ошибку трудно увидеть, поскольку отступы в записи подразумевают, что и minVal=ivec[i], и occurs=1 входят в одну инструкцию if. На самом же деле инструкция
occurs = 1;
не является частью if и выполняется безусловно, всегда сбрасывая occurs в 1. Вот как должна быть составлена правильная if-инструкция (точное положение открывающей фигурной скобки является поводом для бесконечных споров):
if ( minVal > ivec[ i ] )
{
minVal = ivec[ i ];
occurs = 1;
}
Вторая инструкция if выглядит так:
if ( minVal == ivec [ i ] )
++occurs;
Заметим, что порядок следования инструкций в этом примере крайне важен. Если мы будем сравнивать minVal именно в такой последовательности, наша функция всегда будет ошибаться на 1:
if ( minVal > ivec[ i ] ) {
minVal = ivec[ i ];
occurs = 1;
}
// если minVal только что получила новое значение,
// то occurs будет на единицу больше, чем нужно
if ( minVal == ivec[ i ] )
++occurs;
Выполнение второго сравнения не обязательно: один и тот же элемент не может одновременно быть и меньше и равен minVal. Поэтому появляется необходимость выбора одного из двух блоков в зависимости от условия, что реализуется инструкцией if-else, второй формой if-инструкции. Ее синтаксис выглядит таким образом:
if ( условие )
инструкция1
else
инструкция2
инструкция1 выполняется, если условие истинно, иначе переходим к инструкция2. Например:
if ( minVal == ivec[ i ] )
++occurs;
else
if ( minVal > ivec[ i ] ) {
minVal = ivec[ i ];
occurs = 1;
}
Здесь инструкция2 сама является if-инструкцией. Если minVal меньше ivec[i], никаких действий не производится.
В следующем примере выполняется одна из трех инструкций:
if ( minVal < ivec[ i ] )
{} // пустая инструкция
else
if ( minVal > ivec[ i ] ) {
minVal = ivec[ i ];
occurs = 1;
}
else // minVal == ivec[ i ]
++occurs;
Составные инструкции if-else могут служить источником неоднозначного толкования, если частей else больше, чем частей if. К какому из if отнести данную часть else? (Эту проблему иногда называют проблемой висячего else). Например:
if ( minVal <= ivec[ i ] )
if ( minVal == ivec[ i ] )
++occurs;
else {
minVal = ivec[ i ];
occurs = 1;
}
Судя по отступам, программист предполагает, что else относится к самому первому, внешнему if. Однако в С++ неоднозначность висячих else разрешается соотнесением их с последним встретившимся if. Таким образом, в действительности предыдущий фрагмент означает следующее:
if ( minVal <= ivec[ i ] ) {
if ( minVal == ivec[ i ] )
++occurs;
else {
minVal = ivec[ i ];
occurs = 1;
}
}
Одним из способов разрешения данной проблемы является заключение внутреннего if в фигурные скобки:
if ( minVal <= ivec[ i ] ) {
if ( minVal == ivec[ i ] )
++occurs;
}
else {
minVal = ivec[ i ];
occurs = 1;
}
В некоторых стилях программирования рекомендуется всегда употреблять фигурные скобки при использовании инструкций if-else, чтобы не допустить возможности неправильной интерпретации кода.
Вот первый вариант функции min(). Второй аргумент функции будет возвращать количество вхождений минимального значения в вектор. Для перебора элементов массива используется цикл for. Но мы допустили ошибку в логике программы. Сможете ли вы заметить ее?
#include <vector>
int min( const vector<int> &ivec, int &occurs )
{
int minVal = 0;
occurs = 0;
int size = ivec.size();
for ( int ix = 0; ix < size; ++ix ) {
if ( minVal == ivec[ ix ] )
++occurs;
else
if ( minVal > ivec[ ix ] ) {
minVal = ivec[ ix ];
occurs = 1;
}
}
return minVal;
}
Обычно функция возвращает только одно значение. Однако согласно нашей спецификации в точке вызова должно быть известно не только само минимальное значение, но и количество его вхождений в вектор. Для возврата второго значения мы использовали параметр типа ссылка. (Параметры-ссылки рассматриваются в разделе 7.3.) Любое присваивание значения ссылке occurs изменяет значение переменной, на которую она ссылается:
int main()
{
int occur_cnt = 0;
vector< int > ivec;
// occur_cnt получает значение occurs
// из функции min()
int minval = min( ivec, occur_cnt );
// ...
}
Альтернативой использованию параметра-ссылки является применение объекта класса pair, представленного в разделе 3.14. Функция min() могла бы возвращать два значения в одной паре:
// альтернативная реализация
// с помощью пары
#include <uti1ity>
#include <vector>
typedef pair<int,int> min_va1_pair;
min_va1_pair
min( const vector<int> &ivec )
{
int minVal = 0;
int occurs = 0;
// то же самое ...
return make_pair( minVal, occurs );
}
К сожалению, и эта реализация содержит ошибку. Где же она? Правильно: мы инициализировали minVal нулем, поэтому, если минимальный элемент вектора больше нуля, наша реализация вернет нулевое значение минимума и нулевое значение количества вхождений.
Программу можно изменить, инициализировав minVal первым элементом вектора:
int minVal = ivec[0];
Теперь функция работает правильно. Однако в ней выполняются некоторые лишние действия, снижающие ее эффективность.
// исправленная версия min()
// оставляющая возможность для оптимизации ...
int minVal = ivec[0];
occurs = 0;
int size = ivec.size();
for ( int ix = 0; ix < size; ++ix )
{
if ( minVal == ivec[ ix ] )
++occurs;
// ...
Поскольку ix инициализируется нулем, на первой итерации цикла значение первого элемента сравнивается с самим собой. Можно инициализировать ix единицей и избежать ненужного выполнения первой итерации. Однако при оптимизации кода мы допустили другую ошибку (наверное, стоило все оставить как было!). Сможете ли вы ее обнаружить?
// оптимизированная версия min(),
// к сожалению, содержащая ошибку...
int minVal = ivec[0];
occurs = 0;
int size = ivec.size();
for ( int ix = 1; ix < size; ++ix )
{
if ( minVal == ivec[ ix ] )
++occurs;
// ...
Если ivec[0] окажется минимальным элементом, переменная occurs не получит значения 1. Конечно, исправить это очень просто, но сначала надо найти ошибку:
int minVal = ivec[0];
occurs = 1;
К сожалению, подобного рода недосмотры встречаются не так уж редко: программисты тоже люди и могут ошибаться. Важно понимать, что это неизбежно, и быть готовым тщательно тестировать и анализировать свои программы.
Вот окончательная версия функции min() и программа main(), проверяющая ее работу:
#include <iostream>
#include <vector>
int min( const vector< int > &ivec, int &occurs )
{
int minVal = ivec[ 0 ];
occurs = 1;
int size = ivec.size();
for ( int ix = 1; ix < size; ++ix )
{
if ( minVal == ivec[ ix ] )
++occurs;
else
if ( minVal > ivec[ ix ] ){
minVal = ivec[ ix ];
occurs = 1;
}
}
return minVal;
}
int main()
{
int ia[] = { 9,1,7,1,4,8,1,3,7,2,6,1,5,1 };
vector<int> ivec( ia, ia+14 );
int occurs = 0;
int minVal = min( ivec, occurs );
cout << "Минимальное значение: " << minVal
<< " встречается: " << occurs << " раз.\n";
return 0;
}
Результат работы программы:
Минимальное значение: 1 встречается: 5 раз.
В некоторых случаях вместо инструкции if-else можно использовать более краткое и выразительное условное выражение. Например, следующую реализацию функции min():
template <class valueType>
inline const valueType&
min( valueType &vall, valueType &va12 )
{
if ( vall < va12 )
return vall;
return va12;
}
можно переписать так:
template <class valueType>
inline const valueType&
min( valueType &vall, valueType &va12 )
{
return ( vall < va12 ) ? vall : va12;
}
Длинные цепочки инструкций if-else, подобные приведенной ниже, трудны для восприятия и, таким образом, являются потенциальным источником ошибок.
if ( ch == 'a' ||
ch == 'A' )
++aCnt;
else
if ( ch == 'e' ||
ch == 'E' )
++eCnt;
else
if ( ch == 'i' ||
ch == 'I' )
++iCnt;
else
if ( ch == 'o' ||
ch == '0' )
++oCnt;
else
if ( ch == 'u' ||
ch == 'U' )
++uCnt;
В качестве альтернативы таким цепочкам С++ предоставляет инструкцию switch. Это тема следующего раздела.
Упражнение 5.3
Исправьте ошибки в примерах:
(a) if ( ivall != iva12 )
ivall = iva12
else
ivall = iva12 = 0;
(b) if ( ivat < minval )
minvat = ival;
occurs = 1;
(c) if ( int ival = get_value())
cout << "ival = "
<< ival << endl;
if ( ! ival )
cout << "ival = 0\n";
(d) if ( ival = 0 )
ival = get_value();
(e) if ( iva1 == 0 )
else ival = 0;
Упражнение 5.4
Преобразуйте тип параметра occurs функции min(), сделав его не ссылкой, а простым объектом. Запустите программу. Как изменилось ее поведение?
Инструкция switch
Длинные цепочки инструкций if-else, наподобие приведенной в конце предыдущего раздела, трудны для восприятия и потому являются потенциальным источником ошибок. Модифицируя такой код, легко сопоставить, например, разные else и if. Альтернативный метод выбора одного их взаимоисключающих условий предлагает инструкция switch.
Для иллюстрации инструкции switch рассмотрим следующую задачу. Нам надо подсчитать, сколько раз встречается каждая из гласных букв в указанном отрывке текста. (Общеизвестно, что буква e – наиболее часто встречающаяся гласная в английском языке.) Вот алгоритм программы:
1. Считывать по одному символу из входного потока, пока они не кончатся.
2. Сравнить каждый символ с набором гласных.
3. Если символ равен одной из гласных, прибавить 1 к ее счетчику.
4. Напечатать результат.
Написанная программа была запущена, в качестве контрольного текста использовался раздел из оригинала данной книги. Результаты подтвердили, что буква e действительно самая частая:
aCnt: 394
eCnt: 721
iCnt: 461
oCnt: 349
uCnt: 186
Инструкция switch состоит из следующих частей:
ключевого слова switch, за которым в круглых скобках идет выражение, являющееся условием:
char ch;
while ( cm >> ch )
switch( ch )
набора меток case, состоящих из ключевого слова case и константного выражения, с которым сравнивается условие. В данном случае каждая метка представляет одну из гласных латинского алфавита:
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
последовательности инструкций, соотносимых с метками case. В нашем примере с каждой меткой будет сопоставлена инструкция, увеличивающая значение соответствующего счетчика;
необязательной метки default, которая является аналогом части else инструкции if-else. Инструкции, соответствующие этой метке, выполняются, если условие не отвечает ни одной из меток case. Например, мы можем подсчитать суммарное количество встретившихся символов, не являющихся гласными буквами:
default: // любой символ, не являющийся гласной
++non_vowe1_cnt;
Константное выражение в метке case должно принадлежать к целому типу, поэтому следующие строки ошибочны:
// неверные значения меток
case 3.14: // не целое
case ival: // не константа
Кроме того, две разные метки не могут иметь одинаковое значение.
Выражение условия в инструкции switch может быть сколь угодно сложным, в том числе включать вызовы функций. Результат вычисления условия сравнивается с метками case, пока не будет найдено равное значение или не выяснится, что такого значения нет. Если метка обнаружена, выполнение будет продолжено с первой инструкции после нее, если же нет, то с первой инструкции после метки default (при ее наличии) или после всей составной инструкции switch.
В отличие от if-else инструкции, следующие за найденной меткой, выполняются друг за другом, проходя все нижестоящие метки case и метку default. Об этом часто забывают. Например, данная реализация нашей программы выполняется совершенно не так, как хотелось бы:
#include <iostream>
int main()
{
char ch;
int aCnt=0, eCnt=0, iCnt=0, oCnt=0, uCnt=0;
while ( cin >> ch )
// Внимание! неверная реализация!
switch ( ch ) {
case 'a':
++aCnt;
case 'e':
++eCnt;
case 'i':
++iCnt;
case 'o':
++oCnt;
case 'u':
++uCnt;
}
cout << "Встретилась a: \t" << aCnt << '\n'
<< "Встретилась e: \t" << eCnt << '\n'
<< "Встретилась i: \t" << iCnt << '\n'
<< "Встретилась o: \t" << oCnt << '\n'
<< "Встретилась u: \t" << uCnt << '\n';
}
Если значение ch равно i, выполнение начинается с инструкции после case 'i' и iCnt возрастет на 1. Однако следующие ниже инструкции, ++oCnt и ++uCnt, также выполняются, увеличивая значения и этих переменных. Если же переменная ch равна a, изменятся все пять счетчиков.
Программист должен явно дать указание компьютеру прервать последовательное выполнение инструкций в определенном месте switch, вставив break. В абсолютном большинстве случаев за каждой метке case должен следовать соответствующий break.
break прерывает выполнение switch и передает управление инструкции, следующей за закрывающей фигурной скобкой, – в данном случае производится вывод. Вот как это должно выглядеть:
switch ( ch ) {
case 'a':
++aCnt;
break;
case 'e':
++eCnt;
break;
case 'i':
++iCnt;
break;
case 'o':
++oCnt;
break;
case 'u':
++uCnt;
break;
}
Если почему-либо нужно, чтобы одна из секций не заканчивалась инструкцией break, то желательно написать в этом месте разумный комментарий. Программа создается не только для машин, но и для людей, и необходимо сделать ее как можно более понятной для читателя. Программист, изучающий чужой текст, не должен сомневаться, было ли нестандартное использование языка намеренным или ошибочным.
При каком условии программист может отказаться от инструкции break и позволить программе провалиться
сквозь несколько меток case? Одним из таких случаев является необходимость выполнить одни и те же действия для двух или более меток. Это может понадобиться потому, что с case всегда связано только одно значение. Предположим, мы не хотим подсчитывать, сколько раз встретилась каждая гласная в отдельности, нас интересует только суммарное количество всех встретившихся гласных. Это можно сделать так:
int vowelCnt = 0;
// ...
switch ( ch )
{
// любой из символов a,e,1,o,u
// увеличит значение vowelCnt
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
++vowe1Cnt;
break;
}
Некоторые программисты подчеркивают осознанность своих действий тем, что предпочитают в таком случае писать метки на одной строке:
switch ( ch )
{
// допустимый синтаксис
case 'a': case 'e':
case 'i': case 'o': case 'u':
++vowe1Cnt;
break;
}
В данной реализации все еще осталась одна проблема: как будут восприняты слова типа
UNIX
Наша программа не понимает заглавных букв, поэтому заглавные U и I не будут отнесены к гласным. Исправить ситуацию можно следующим образом:
switch ( ch ) {
case 'a': case 'A':
++aCnt;
break;
case 'e': case 'E':
++eCnt;
break;
case 'i': case 'I':
++iCnt;
break;
case 'o': case 'O':
++oCnt;
break;
case 'u': case 'U':
++uCnt;
break;
}
Метка default является аналогом части else инструкции if-else. Инструкции, соответствующие default, выполняются, если условие не отвечает ни одной из меток case. Например, добавим к нашей программе подсчет суммарного количества согласных:
#include <iostream>
#include <ctype.h>
int main()
{
char ch;
int aCnt=0, eCnt=0, iCnt=0, oCnt=0, uCnt=0,
consonantCount=0;
while ( cin >> ch )
switch ( ch ) {
case 'a': case 'A':
++aCnt;
break;
case 'e': case 'E':
++eCnt;
break;
case 'i': case 'I':
++iCnt;
break;
case 'o': case 'O':
++oCnt;
break;
case 'u': case 'U':
++uCnt;
break;
default:
if ( isa1pha( ch ) )
++consonantCnt;
break;
}
cout << "Встретилась a: \t" << aCnt << '\n'
<< "Встретилась e: \t" << eCnt << '\n'
<< "Встретилась i: \t" << iCnt << '\n'
<< "Встретилась o: \t" << oCnt << '\n'
<< "Встретилась u: \t" << uCnt << '\n'
<< "Встретилось согласных: \t" << consonantCnt
<< '\n';
}
isalpha() – функция стандартной библиотеки С; она возвращает true, если ее аргумент является буквой. isalpha() объявлена в заголовочном файле ctype.h. (Функции из ctype.h мы будем рассматривать в главе 6.)
Хотя оператор break функционально не нужен после последней метки в инструкции switch, лучше его все-таки ставить. Причина проста: если мы впоследствии захотим добавить еще одну метку после case, то с большой вероятностью забудем вписать недостающий break.
Условная часть инструкции switch может содержать объявление, как в следующем примере:
switch( int ival = get_response() )
ival инициализируется значением, получаемым от get_response(), и это значение сравнивается со значениями меток case. Переменная ival видна внутри блока switch, но не вне его.
Помещать же инструкцию объявления внутри тела блока switch не разрешается. Данный фрагмент кода не будет пропущен компилятором:
case illegal_definition:
// ошибка: объявление не может
// употребляться в этом месте
string file_name = get_file_name();
// ...
break;
Если бы разрешалось объявлять переменную таким образом, то ее было бы видно во всем блоке switch, однако инициализируется она только в том случае, если выполнение прошло через данную метку case.
Мы можем употребить в этом месте составную инструкцию, тогда объявление переменной file_name будет синтаксически правильным. Использование блока гарантирует, что объявленная переменная видна только внутри него, а в этом контексте она заведомо инициализирована. Вот как выглядит правильный текст:
case ok:
{
// ок
string file_name = get_file_name();
// ...
break;
}
Упражнение 5.5
Модифицируйте программу из данного раздела так, чтобы она подсчитывала не только буквы, но и встретившиеся пробелы, символы табуляции и новой строки.
Упражнение 5.6
Модифицируйте программу из данного раздела так, чтобы она подсчитывала также количество встретившихся двухсимвольных последовательностей ff, fl и fi.
Упражнение 5.7
Найдите и исправьте ошибки в следующих примерах:
(a)
switch ( ival ) {
case 'a': aCnt++;
case 'e': eCnt++;
default: iouCnt++;
}
(b)
switch ( ival ) {
case 1:
int ix = get_value();
ivec[ ix ] = ival;
break;
default:
ix = ivec.sizeQ-1;
ivec[ ix ] = ival;
}
(c)
switch ( ival ) {
case 1, 3, 5, 7, 9:
oddcnt++;
break;
case 2, 4, 6, 8, 10:
evencnt++;
break;
}
(d)
int iva1=512 jva1=1024, kva1=4096;
int bufsize;
// ...
switch( swt ) {
case ival:
bufsize = ival * sizeof( int );
break;
case jval:
bufsize = jval * sizeof( int );
break;
case kval:
bufsize = kval * sizeof( int );
break;
}
(e)
enum { illustrator = 1, photoshop, photostyler = 2 };
switch ( ival ) {
case illustrator:
--i11us_1icense;
break;
case photoshop:
--pshop_1icense;
break;
case photostyler:
--psty1er_license;
break;
} |
Инструкция while
Синтаксис инструкции while следующий:
while ( условие )
инструкция
Пока значением условия является true, инструкция выполняется в такой последовательности:
1. Вычислить условие.
2. Выполнить инструкцию, если условие истинно.
3. Если самое первое вычисление условия дает false, инструкция не выполняется.
Условием может быть любое выражение:
bool quit = false;
// ...
while ( ! quit ) {
// ...
quit = do_something();
}
string word;
while ( cin >> word ){ ... }
или объявление с инициализацией:
while ( symbol *ptr = search( name )) {
// что-то сделать
}
В последнем случае ptr видим только в блоке, соответствующем инструкции while, как это было и для инструкций for и switch.
Вот пример цикла while, обходящего множество элементов, адресуемых двумя указателями:
int sumit( int *parray_begin, int *parray_end )
{
int sum = 0;
if ( ! parray_begin || ! parray_end )
return sum;
while ( parray_begin != parray_end )
// прибавить к sum
// и увеличить указатель
sum += *parray_begin++;
return sum;
}
int ia[6] = { 0, 1, 2, 3, 4, 5 };
int main()
{
int sum = sumit( &ia[0], &ia[ 6 ] );
// ...
}
Для того чтобы функция sumit() выполнялась правильно, оба указателя должны адресовать элементы одного и того же массива (parray_end может указывать на элемент, следующий за последним). В противном случае sumit() будет возвращать бессмысленную величину. Увы, С++ не гарантирует, что два указателя адресуют один и тот же массив. Как мы увидим в главе 12, стандартные универсальные алгоритмы реализованы подобным же образом, они принимают параметрами указатели на первый и последний элементы массива.
Упражнение 5.11
Какие ошибки допущены в следующих циклах while:
(a)
string bufString, word;
while ( cin >> bufString >> word )
// ...
(b)
while ( vector<int>::iterator iter != ivec.end() )
// ...
(c)
while ( ptr = 0 )
ptr = find_a_value();
(d)
while ( bool status = find( word )) {
word = get_next_word();
if ( word.empty() )
break;
// ...
}
if ( ! status )
cout << "Слов не найдено\n";
Упражнение 5.12
while обычно применяется для циклов, выполняющихся, пока некоторое условие истинно, например, читать следующее значение, пока не будет достигнут конец файла. for обычно рассматривается как пошаговый цикл: индекс пробегает по определенному диапазону значений. Напишите по одному типичному примеру for и while, а затем измените их, используя цикл другого типа. Если бы вам нужно было выбрать для постоянной работы только один из этих типов, какой бы вы выбрали? Почему?
Упражнение 5.13
Напишите функцию, читающую последовательность строк из стандартного ввода до тех пор, пока одно и то же слово не встретится два раза подряд либо все слова не будут обработаны. Для чтения слов используйте while; при обнаружении повтора слова завершите цикл с помощью инструкции break. Если повторяющееся слово найдено, напечатайте его. В противном случае напечатайте сообщение о том, что слова не повторялись.
Исключения и наследование
Обработка исключений – это стандартное языковое средство для реакции на аномальное поведение программы во время выполнения. C++ поддерживает единообразный синтаксис и стиль обработки исключений, а также способы тонкой настройки этого механизма в специальных ситуациях. Основы его поддержки в языке C++ описаны в главе 11, где показано, как программа может возбудить исключение, передать управление его обработчику (если таковой существует) и как обработчики исключений ассоциируются с try-блоками.
Возможности механизма обработки исключений становятся больше, если в качестве исключений использовать иерархии классов. В этом разделе мы расскажем, как писать программы, которые умеют возбуждать и обрабатывать исключения, принадлежащие таким иерархиям.
Исключения и вопросы проектирования
С обработкой исключений в программах C++ связано несколько вопросов. Хотя поддержка такой обработки встроена в язык, не стоит использовать ее везде. Обычно она применяется для обмена информацией об ошибках между независимо разработанными частями программы. Например, автор некоторой библиотеки может с помощью исключений сообщать пользователям об ошибках. Если библиотечная функция обнаруживает аномальную ситуацию, которую не способна обработать самостоятельно, она может возбудить исключение для уведомления вызывающей программы.
В нашем примере в библиотеке определен класс iStack и его функции-члены. Разумно предположить, что программист, кодировавший main(), где используется эта библиотека, не разрабатывал ее. Функции-члены класса iStack могут обнаружить, что операция pop() вызвана, когда стек пуст, или что операция push() вызвана, когда стек полон; однако разработчик библиотеки ничего не знал о программе, пользующейся его функциями, так что не мог разрешить проблему локально. Не сумев обработать ошибку внутри функций-членов, мы решили возбуждать исключения, чтобы известить вызывающую программу.
Хотя C++ поддерживает исключения, следует применять и другие методы обработки ошибок (например, возврат кода ошибки) – там, где это более уместно. Однозначного ответа на вопрос: “Когда ошибку следует трактовать как исключение?” не существует. Ответственность за решение о том, что считать исключительной ситуацией, возлагается на разработчика. Исключения – это часть интерфейса библиотеки, и решение о том, какие исключения она возбуждает, – важный аспект ее дизайна. Если библиотека предназначена для использования в программах, которые не должны аварийно завершаться ни при каких обстоятельствах, то она обязана разбираться с аномалиями сама либо извещать о них вызывающую программу, передавая ей управление. Решение о том, какие ошибки следует обрабатывать как исключения, – трудная часть работы по проектированию библиотеки.
В нашем примере с классом iStack вопрос, должна ли функция push() возбуждать исключение, если стек полон, является спорным. Альтернативная и, по мнению многих, лучшая реализация push() – локальное решение проблемы: увеличение размера стека при его заполнении. В конце концов, единственное ограничение – это объем доступной программе памяти. Наше решение о возбуждении исключения при попытке поместить значение в полный стек, по-видимому, непродуманно. Можно переделать функцию-член push(), чтобы она в такой ситуации наращивала стек:
void iStack::push( int value )
{
// если стек полон, увеличить размер вектора
if ( full() )
_stack.resize( 2 * _stack.size() );
_stack[ _top++ ] = value;
}
Аналогично следует ли функции pop() возбуждать исключение при попытке извлечь значение из пустого стека? Интересно отметить, что класс stack из стандартной библиотеки C++ (он рассматривался в главе 6) не возбуждает исключения в такой ситуации. Вместо этого постулируется, что поведение программы при попытке выполнения подобной операции не определено. Разрешить программе продолжать работу при обнаружении некорректного состояния признали возможным. Мы уже упоминали, что в разных библиотеках определены разные исключения. Не существует пригодного для всех случаев ответа на вопрос, что такое исключение.
Не все программы должны беспокоиться по поводу исключений, возбуждаемых библиотечными функциями. Хотя есть системы, для которых простой недопустим и которые, следовательно, должны обрабатывать все исключительные ситуации, не к каждой программе предъявляются такие требования. Обработка исключений предназначена в первую очередь для реализации отказоустойчивых систем. В этом случае решение о том, должна ли программа обрабатывать все исключения, возбуждаемые библиотеками, или может закончить выполнение аварийно, – это трудная часть процесса проектирования.
Еще один аспект проектирования программ заключается в том, что обработка исключений обычно структурирована. Как правило, программа строится из компонентов, и каждый компонент решает сам, какие исключения обрабатывать локально, а какие передавать на верхние уровни. Что мы понимаем под компонентом? Например, система анализа текстовых запросов, рассмотренная в главе 6, может быть разбита на три компонента, или слоя. Первый слой – это стандартная библиотека C++, которая обеспечивает базовые операции над строками, отображениями и т.д. Второй слой – это сама система анализа текстовых запросов, где определены такие функции, как string_caps() и suffix_text(), манипулирующие текстами и использующие стандартную библиотеку как основу. Третий слой – это программа, которая применяет нашу систему. Каждый компонент строится независимо и должен принимать решения о том, какие исключительные ситуации обрабатывать локально, а какие передавать на более высокий уровень.
Не все функции должны уметь обрабатывать исключения. Обычно try-блоки и ассоциированные с ними catch-обработчики применяются в функциях, являющихся точками входа в компонент. Catch-обработчики проектируются так, чтобы перехватывать те исключения, которые не должны попасть на верхние уровни программы. Для этого также используются спецификации исключений (см. раздел 11.4).
Мы расскажем о других аспектах проектирования программ, использующих исключения, в главе 19, после знакомства с классами и иерархиями классов.
Исключения, определенные как иерархии классов
В главе 11 мы использовали два типа класса для описания исключений, возбуждаемых функциями-членами нашего класса iStack:
class popOnEmpty { ... };
class pushOnFull { ... };
В реальных программах на C++ типы классов, представляющих исключения, чаще всего организуются в группы, или иерархии. Как могла бы выглядеть вся иерархия для этих классов?
Мы можем определить базовый класс Excp, которому наследуют оба наши класса исключений. Он инкапсулирует данные и функции-члены, общие для обоих производных:
class Excp { ... };
class popOnEmpty : public Excp { ... };
class pushOnFull : public Excp { ... };
Одной из операцией, которые предоставляет базовый класс, является вывод сообщения об ошибке. Эта возможность используется обоими классами, стоящими ниже в иерархии:
class Excp {
public:
// напечатать сообщение об ошибке
static void print( string msg ) {
cerr << msg << endl;
}
};
Иерархию классов исключений разрешается развивать и дальше. От Excp можно произвести другие классы для более точного описания исключений, обнаруживаемых программой:
class Excp { ... };
class stackExcp : public Excp { ... };
class popOnEmpty : public stackExcp { ... };
class pushOnFull : public stackExcp { ... };
class mathExcp : public Excp ( ... };
class zeroOp : public mathExcp { ... };
class divideByZero : public mathExcp { ... };
Последующие уточнения позволяют более детально идентифицировать аномальные ситуации в работе программы. Дополнительные классы исключений организуются как слои. По мере углубления иерархии каждый новый слой описывает все более специфичные исключения. Например, первый, самый общий слой в приведенной выше иерархии представлен классом Excp. Второй специализирует Excp, выделяя из него два подкласса: stackExcp (для исключений при работе с нашим iStack) и mathExcp (для исключений, возбуждаемых функциями из математической библиотеки). Третий, самый специализированный слой данной иерархии уточняет классы исключений: popOnEmpty и pushOnFull определяют два вида исключений работы со стеком, а ZeroOp и divideByZero – два вида исключений математических операций.
В последующих разделах мы рассмотрим, как возбуждаются и обрабатываются исключения, представленные классами в нашей иерархии.
Использование членов пространства имен *
Использование квалифицированных имен при каждом обращении к членам пространств может стать обременительным, особенно если имена пространств достаточно длинны. Если бы удалось сделать их короче, то такие имена проще было бы читать и набивать. Однако употребление коротких имен увеличивает риск их совпадения с другими, поэтому желательно, чтобы в библиотеках применялись пространства с длинными именами.
К счастью, существуют механизмы, облегчающие использование членов пространств имен в программах. Псевдонимы пространства имен, using-объявления и using-директивы помогают преодолеть неудобства работы с очень длинными именами.
Использование исключений
Исключениями называют аномальные ситуации, возникающие во время исполнения программы: невозможность открыть нужный файл или получить необходимое количество памяти, использование выходящего за границы индекса для какого-либо массива. Обработка такого рода исключений, как правило, плохо интегрируется в основной алгоритм программы, и программисты вынуждены изобретать разные способы корректной обработки исключения, стараясь в то же время не слишком усложнить программу добавлением всевозможных проверок и дополнительных ветвей алгоритма.
С++ предоставляет стандартный способ реакции на исключения. Благодаря вынесению в отдельную часть программы кода, ответственного за проверку и обработку ошибок, значительно облегчается восприятие текста программы и сокращается ее размер. Единый синтаксис и стиль обработки исключений можно, тем не менее, приспособить к самым разнообразным нуждам и запросам.
Механизм исключений делится на две основные части:
точка программы, в которой произошло исключение. Определение того факта, что при выполнении возникла какая-либо ошибка, влечет за собой возбуждение
исключения. Для этого в С++ предусмотрен специальный оператор throw. Возбуждение исключения в случае невозможности открыть некоторый файл выглядит следующим образом:
if ( !infile ) {
string errMsg("Невозможно открыть файл: ");
errMsg += fileName;
throw errMsg;
}
Место программы, в котором исключение обрабатывается. При возбуждении исключения нормальное выполнение программы приостанавливается и управление передается обработчику исключения. Поиск нужного обработчика часто включает в себя раскрутку так называемого стека вызовов программы. После обработки исключения выполнение программы возобновляется, но не с того места, где произошло исключение, а с точки, следующей за обработчиком. Для определения обработчика исключения в С++ используется ключевое слово catch. Вот как может выглядеть обработчик для примера из предыдущего абзаца:
catch (string exceptionMsg) {
log_message (exceptionMsg);
return false;
}
Каждый catch-обработчик ассоциирован с исключениями, возникающими в блоке операторов, который непосредственно предшествует обработчику и помечен ключевым словом try. Одному try-блоку могут соответствовать несколько catch-предложений, каждое из которых относится к определенному виду исключений. Приведем пример:
int* stats (const int *ia, int size)
{
int *pstats = new int [4];
try {
pstats[0] = sum_it (ia,size);
pstats[1] = min_val (ia,size);
pstats[2] = max_val (ia,size);
}
catch (string exceptionMsg) {
// код обработчика
}
catch (const statsException &statsExcp) {
// код обработчика
}
pstats [3] = pstats[0] / size;
do_something (pstats);
return pstats;
}
В данном примере в теле функции stats() три оператора заключены в try-блок, а четыре – нет. Из этих четырех операторов два способны возбудить исключения.
1) int *pstats = new int [4];
Выполнение оператора new может окончиться неудачей. Стандартная библиотека С++ предусматривает возбуждение исключения bad_alloc в случае невозможности выделить нужное количество памяти. Поскольку в примере не предусмотрен обработчик исключения bad_alloc, при его возбуждении выполнение программы закончится аварийно.
2) do_something (pstats);
Мы не знаем реализации функции do_something(). Любая инструкция этой функции, или функции, вызванной из этой функции, или функции, вызванной из функции, вызванной этой функцией, и так далее, потенциально может возбудить исключение. Если в реализации функции do_something и вызываемых из нее предусмотрен обработчик такого исключения, то выполнение stats() продолжится обычным образом. Если же такого обработчика нет, выполнение программы аварийно завершится.
Необходимо заметить, что, хотя оператор
pstats [3] = pstats[0] / size;
может привести к делению на ноль, в стандартной библиотеке не предусмотрен такой тип исключения.
Обратимся теперь к инструкциям, объединенным в try-блок. Если в одной из вызываемых в этом блоке функций – sum_it(), min_val() или max_val() –произойдет исключение, управление будет передано на обработчик, следующий за try-блоком и перехватывающий именно это исключение. Ни инструкция, возбудившая исключение, ни следующие за ней инструкции в try-блоке выполнены не будут. Представим себе, что при вызове функции sum_it() возбуждено исключение:
throw string ("Ошибка: adump27832");
Выполнение функции sum_it() прервется, операторы, следующие в try- блоке за вызовом этой функции, также не будут выполнены, и pstats[0] не будет инициализирована. Вместо этого возбуждается исключительное состояние и исследуются два catch-обработчика. В нашем случае выполняется catch с параметром типа string:
catch (string exceptionMsg) {
// код обработчика
}
После выполнения управление будет передано инструкции, следующей за последним catch-обработчиком, относящимся к данному try-блоку. В нашем случае это
pstats [3] = pstats[0] / size;
(Конечно, обработчик сам может возбуждать исключения, в том числе – того же типа. В такой ситуации будет продолжено выполнение catch-предложений, определенных в программе, вызвавшей функцию stats().)
Вот пример:
catch (string exceptionMsg) {
// код обработчика
cerr << "stats(): исключение: "
<< exceptionMsg
<< endl;
delete [] pstats;
return 0;
}
В таком случае выполнение вернется в функцию, вызвавшую stats(). Будем считать, что разработчик программы предусмотрел проверку возвращаемого функцией stats() значения и корректную реакцию на нулевое значение.
Функция stats() умеет реагировать на два типа исключений: string и statsException. Исключение любого другого типа игнорируется, и управление передается в вызвавшую функцию, а если и в ней не найдется обработчика, – то в функцию более высокого уровня, и так до функции main().При отсутствии обработчика и там, программа аварийно завершится.
Возможно задание специального обработчика, который реагирует на любой тип исключения. Синтаксис его таков:
catch (...) {
// обрабатывает любое исключение,
// однако ему недоступен объект, переданный
// в обработчик в инструкции throw
}
(Детально обработка исключительных ситуаций рассматривается в главах 11 и 19.)
Упражнение 2.18
Какие ошибочные ситуации могут возникнуть во время выполнения следующей функции:
int *alloc_and_init (string file_name)
{
ifstream infile (file_name)
int elem_cnt;
infile >> elem_cnt;
int *pi = allocate_array(elem_cnt);
int elem;
int index=0;
while (cin >> elem)
pi[index++] = elem;
sort_array(pi,elem_cnt);
register_data(pi);
return pi;
}
Упражнение 2.19
В предыдущем примере вызываемые функции allocate_array(), sort_array() и register_data() могут возбуждать исключения типов noMem, int и string соответственно. Перепишите функцию alloc_and_init(), вставив соответствующие блоки try и catch для обработки этих исключений. Пусть обработчики просто выводят в cerr сообщение об ошибке.
Упражнение 2.20
Усовершенствуйте функцию alloc_and_init() так, чтобы она сама возбуждала исключение в случае возникновения всех возможных ошибок (это могут быть исключения, относящиеся к вызываемым функциям allocate_array(), sort_array() и register_data() и какими-то еще операторами внутри функции alloc_and_init()). Пусть это исключение имеет тип string и строка, передаваемая обработчику, содержит описание ошибки.
Использование обобщенных алгоритмов
Допустим, мы задумали написать книжку для детей и хотим понять, какой словарный состав наиболее подходит для такой цели. Чтобы ответить на этот вопрос, нужно прочитать несколько детских книг, сохранить текст в отдельных векторах строк (см. раздел 6.7) и подвергнуть его следующей обработке:
Создать копию каждого вектора.
Слить все векторы в один.
Отсортировать его в алфавитном порядке.
Удалить все дубликаты.
Снова отсортировать, но уже по длине слов.
Подсчитать число слов, длина которых больше шести знаков (предполагается, что длина– это некоторая мера сложности, по крайней мере, в терминах словаря).
Удалить семантически нейтральные слова (например, союзы and (и), if (если), or (или), but (но) и т.д.).
Напечатать получившийся вектор.
На первый взгляд, задача на целую главу. Но с помощью обобщенных алгоритмов мы решим ее в рамках одного подраздела.
Аргументом нашей функции является вектор из векторов строк. Мы принимаем указатель на него, проверяя, не является ли он нулевым:
#include <vector>
#include <string>
typedef vector<string, allocator> textwords;
void process_vocab( vector<textwords, allocator> *pvec )
{
if ( ! pvec ) {
// выдать предупредительное сообщение
return;
}
// ...
}
Нужно создать один вектор, включающий все элементы исходных векторов. Это делается с помощью обобщенного алгоритма copy() (для его использования необходимо включить заголовочные файлы algorithm и iterator):
#include <algorithm>
#include <iterator>
void process_vocab( vector<textwords, allocator> *pvec )
{
// ...
vector< string > texts;
vector<textwords, allocator>::iterator iter = pvec->begin();
for ( ; iter != pvec->end(); ++iter )
copy( (*iter).begin(), (*iter).end(), back_inserter( texts ));
// ...
}
Первыми двумя аргументами алгоритма copy() являются итераторы, ограничивающие диапазон подлежащих копированию элементов. Третий аргумент – это итератор, указывающий на место, куда надо копировать элементы. back_inserter называется адаптером итератора; он позволяет вставлять элементы в конец вектора, переданного ему в качестве аргумента. (Подробнее мы рассмотрим адаптеры итераторов в разделе 12.4.).
Алгоритм unique() удаляет из контейнера дубликаты, расположенные рядом. Если дана последовательность 01123211, то результатом будет 012321, а не 0123. Чтобы получить вторую последовательность, необходимо сначала отсортировать вектор с помощью алгоритма sort(); тогда из последовательности 01111223 получится 0123. (Хотя на самом деле получится 01231223.)
unique() не изменяет размер контейнера. Вместо этого каждый уникальный элемент помещается в очередную свободную позицию, начиная с первой. В нашем примере физический результат – это последовательность 01231223; остаток 1223 – это, так сказать, “отходы” алгоритма. unique() возвращает итератор, указывающий на начало этого остатка. Как правило, этот итератор затем передается алгоритму erase() для удаления ненужных элементов. (Поскольку встроенный массив не поддерживает операции erase(), то семейство алгоритмов unique() в меньшей степени подходит для работы с ним.) Вот соответствующий фрагмент функции:
void process_vocab( vector<textwords, allocator> *pvec )
{
// ...
// отсортировать вектор texts
sort( texts.begin(), texts.end() );
// удалить дубликаты
vector<string, allocator>::iterator it;
it = unique( texts.begin(), texts.end() );
texts.erase( it, texts.end() );
// ...
}
Ниже приведен результат печати вектора texts, объединяющего два небольших текстовых файла, после применения sort(), но до применения unique():
a a a a alice alive almost
alternately ancient and and and and and and
and as asks at at beautiful becomes bird
bird blows blue bounded but by calling coat
daddy daddy daddy dark darkened darkening distant each
either emma eternity falls fear fiery fiery flight
flowing for grow hair hair has he heaven,
held her her her her him him home
houses i immeasurable immensity in in in in
inexpressibly is is is it it it its
journeying lands leave leave life like long looks
magical mean more night, no not not not
now now of of on one one one
passion puts quite red rises row same says
she she shush shyly sight sky so so
star star still stone such tell tells tells
that that the the the the the the
the there there thing through time to to
to to trees unravel untamed wanting watch what
when wind with with you you you you
your your
После применения unique() и последующего вызова erase() вектор texts выглядит следующим образом:
a alice alive almost alternately ancient
and as asks at beautiful becomes bird blows
blue bounded but by calling coat daddy dark
darkened darkening distant each either emma eternity falls
fear fiery flight flowing for grow hair has
he heaven, held her him home houses i
immeasurable immensity in inexpressibly is it its journeying
lands leave life like long looks magical mean
more night, no not now of on one
passion puts quite red rises row same says
she shush shyly sight sky so star still
stone such tell tells that the there thing
through time to trees unravel untamed wanting watch
what when wind with you your
Следующая наша задача – отсортировать строки по длине. Для этого мы воспользуемся не алгоритмом sort(), а алгоритмом stable_sort(), который сохраняет относительные положения равных элементов. В результате для элементов равной длины сохраняется алфавитный порядок. Для сортировки по длине мы применим собственную операцию сравнения “меньше”. Один из возможных способов таков:
bool less_than( const string & s1, const string & s2 )
{
return s1.size() < s1.size();
}
void process_vocab( vector<textwords, allocator> *pvec )
{
// ...
// отсортировать элементы вектора texts по длине,
// сохранив также прежний порядок
stable_sort( texts.begin(), texts.end(), less_than );
// ...
}
Нужный результат при этом достигается, но эффективность существенно ниже, чем хотелось бы. less_than() реализована в виде одной инструкции. Обычно она вызывается как встроенная (inline) функция. Но, передавая указатель на нее, мы не даем компилятору сделать ее встроенной. Способ, позволяющий добиться этого, –применение объекта-функции:
// объект-функция - операция реализована с помощью перегрузки
// оператора operator()
class LessThan {
public:
bool operator()( const string & s1, const string & s2 )
{ return s1.size() < s2.size(); }
};
Объект-функция – это класс, в котором перегружен оператор вызова operator(). В теле этого оператора и реализуется логика функции, в данном случае сравнение “меньше”. Определение оператора вызова выглядит странно из-за двух пар скобок. Запись
operator()
говорит компилятору, что мы перегружаем оператор вызова. Вторая пара скобок
( const string & s1, const string & s2 )
задает передаваемые ему формальные параметры. Если сравнить это определение с предыдущим определением функции less_than(), мы увидим, что, за исключением замены less_than на operator(), они совпадают.
Объект-функция определяется так же, как обычный объект класса (правда, в данном случае нам не понадобился конструктор: нет членов, подлежащих инициализации):
LessThan lt;
Для вызова экземпляра перегруженного оператора мы применяем оператор вызова к нашему объекту класса, передавая необходимые аргументы. Например:
string st1( "shakespeare" );
string st2( "marlowe" );
// вызывается lt.operator()( st1, st2 );
bool is_shakespeare_less = lt( st1, st2 );
Ниже показана исправленная функция process_vocab(), в которой алгоритму stable_sort() передается безымянный объект-функция LessThan():
void process_vocab( vector<textwords, allocator> *pvec )
{
// ...
stable_sort( texts.begin(), texts.end(), LessThan() );
// ...
}
Внутри stable_sort() перегруженный оператор вызова подставляется в текст программы как встроенная функция. (В качестве третьего аргумента stable_sort() может принимать как указатель на функцию less_than(), так и объект класса LessThan, поскольку аргументом является параметр-тип шаблона. Подробнее об объектах-функциях мы расскажем в разделе 12.3.)
Вот результат применения stable_sort() к вектору texts:
a i
as at by he in is it no
of on so to and but for has
her him its not now one red row
she sky the you asks bird blue coat
dark each emma fear grow hair held home
life like long mean more puts same says
star such tell that time what when wind
with your alice alive blows daddy falls fiery
lands leave looks quite rises shush shyly sight
still stone tells there thing trees watch almost
either flight houses night, ancient becomes bounded calling
distant flowing heaven, magical passion through unravel untamed
wanting darkened eternity beautiful darkening immensity journeying alternately
immeasurable inexpressibly
Подсчитать число слов, длина которых больше шести символов, можно с помощью обобщенного алгоритма count_if() и еще одного объекта-функции – GreaterThan. Этот объект чуть сложнее, так как позволяет пользователю задать размер, с которым производится сравнение. Мы сохраняем размер в члене класса и инициализируем его с помощью конструктора (по умолчанию – значением 6):
#include <iostream>
class GreaterThan {
public:
GreaterThan( int size = 6 ) : _size( size ){}
int size() { return _size; }
bool operator()( const string & s1 )
{ return s1.size() > 6; }
private:
int _size;
};
Использовать его можно так:
void process_vocab( vector<textwords, allocator> *pvec )
{
// ...
// подсчитать число строк, длина которых больше 6
int cnt = count_if( texts.begin(), texts.end(),
GreaterThan() );
cout << "Number of words greater than length six are "
<< cnt << endl;
// ...
}
Этот фрагмент программы выводит такую строку:
Number of words greater than length six are 22
Алгоритм remove() ведет себя аналогично unique(): он тоже не изменяет размер контейнера, а просто разделяет элементы на те, что следует оставить (копируя их по очереди в начало контейнера), и те, что следует удалить (перемещая их в конец контейнера). Вот как можно воспользоваться им для исключения из коллекции слов, которые мы не хотим сохранять:
void process_vocab( vector<textwords, allocator> *pvec )
{
// ...
static string rw[] = { "and", "if", "or", "but", "the" };
vector< string > remove_words( rw, rw+5 );
vector< string >::iterator it2 = remove_words.begin();
for ( ; it2 != remove_words.end(); ++it2 ) {
// просто для демонстрации другой формы count()
int cnt = count( texts.begin(), texts.end(), *it2 );
cout << cnt << " instances removed: "
<< (*it2) << endl;
texts.erase(
remove(texts.begin(),texts.end(),*it2 ),
texts.end()
);
}
// ...
}
Результат применения remove():
1 instances removed: and
0 instances removed: if
0 instances removed: or
1 instances removed: but
1 instances removed: the
Теперь нам нужно распечатать содержимое вектора. Можно обойти все элементы и вывести каждый по очереди, но, поскольку при этом обобщенные алгоритмы не используются, мы считаем такое решение неподходящим. Вместо этого проиллюстрируем работу алгоритма for_each() для вывода всех элементов вектора. for_each() применяет указатель на функцию или объект-функцию к каждому элементу контейнера из диапазона, ограниченного парой итераторов. В нашем случае объект-функция PrintElem копирует один элемент в стандартный вывод:
class PrintElem {
public:
PrintElem( int lineLen = 8 )
: _line_length( lineLen ), _cnt( 0 )
{}
void operator()( const string &elem )
{
++_cnt;
if ( _cnt % _line_length == 0 )
{ cout << '\n'; }
cout << elem << " ";
}
private:
int _line_length;
int _cnt;
};
void process_vocab( vector<textwords, allocator> *pvec )
{
// ...
for_each( texts.begin(), texts.end(), PrintElem() );
}
Вот и все. Мы получили законченную программу, для чего пришлось лишь последовательно записать обращения к нескольким обобщенным алгоритмам. Для удобства мы приводим ниже полный листинг вместе с функцией main() для ее тестирования (здесь используются специальные типы итераторов, которые будут обсуждаться только в разделе 12.4). Мы привели текст реально исполнявшегося кода, который не полностью удовлетворяет стандарту C++. В частности, в нашем распоряжении были лишь устаревшие реализации алгоритмов count() и count_if(), которые не возвращают результат, а требуют передачи дополнительного аргумента для вычисленного значения. Кроме того, библиотека iostream отражает предшествующую принятию стандарта реализацию, в которой требуется заголовочный файл iostream.h.
#include <vector>
#include <string>
#include <algorithm>
#include <iterator>
// предшествующий принятию стандарта синтаксис <iostream>
#include <iostream.h>
class GreaterThan {
public:
GreaterThan( int size = 6 ) : _size( sz ){}
int size() { return _size; }
bool operator()(const string &s1)
{ return s1.size() > _size; }
private:
int _size;
};
class PrintElem {
public:
PrintElem( int lineLen = 8 )
: _line_length( lineLen ), _cnt( 0 )
{}
void operator()( const string &elem )
{
++_cnt;
if ( _cnt % _line_length == 0 )
{ cout << '\n'; }
cout << elem << " ";
}
private:
int _line_length;
int _cnt;
};
class LessThan {
public:
bool operator()( const string & s1,
const string & s2 )
{ return s1.size() < s2.size(); }
};
typedef vector<string, allocator> textwords;
void process_vocab( vector<textwords, allocator> *pvec )
{
if ( ! pvec ) {
// вывести предупредительное сообщение
return;
}
vector< string, allocator > texts;
vector<textwords, allocator>::iterator iter;
for ( iter = pvec->begin() ; iter != pvec->end(); ++iter )
copy( (*iter).begin(), (*iter).end(),
back_inserter( texts ));
// отсортировать вектор texts
sort( texts.begin(), texts.end() );
// теперь посмотрим, что получилось
for_each( texts.begin(), texts.end(), PrintElem() );
cout << "\n\n"; // разделить части выведенного текста
// удалить дубликаты
vector<string, allocator>::iterator it;
it = unique( texts.begin(), texts.end() );
texts.erase( it, texts.end() );
// посмотрим, что осталось
for_each( texts.begin(), texts.end(), PrintElem() );
cout << "\n\n";
// отсортировать элементы
// stable_sort сохраняет относительный порядок равных элементов
stable_sort( texts.begin(), texts.end(), LessThan() );
for_each( texts.begin(), texts.end(), PrintElem() );
cout << "\n\n";
// подсчитать число строк, длина которых больше 6
int cnt = 0;
// устаревшая форма count - в стандарте используется другая
count_if( texts.begin(), texts.end(), GreaterThan(), cnt );
cout << "Number of words greater than length six are "
<< cnt << endl;
static string rw[] = { "and", "if", "or", "but", "the" };
vector<string,allocator> remove_words( rw, rw+5 );
vector<string, allocator>::iterator it2 = remove_words.begin();
for ( ; it2 != remove_words.end(); ++it2 )
{
int cnt = 0;
// устаревшая форма count - в стандарте используется другая
count( texts.begin(), texts.end(), *it2, cnt );
cout << cnt << " instances removed: "
<< (*it2) << endl;
texts.erase(
remove(texts.begin(),texts.end(),*it2),
texts.end()
);
}
cout << "\n\n";
for_each( texts.begin(), texts.end(), PrintElem() );
}
// difference_type - это тип, с помощью которого можно хранить результат
// вычитания двух итераторов одного и того же контейнера
// - в данном случае вектора строк ...
// обычно это предполагается по умолчанию
typedef vector<string,allocator>::difference_type diff_type;
// предшествующий принятию стандарта синтаксис для <fstream>
#include <fstream.h>
main()
{
vector<textwords, allocator> sample;
vector<string,allocator> t1, t2;
string t1fn, t2fn;
// запросить у пользователя имена входных файлов ...
// в реальной программе надо бы выполнить проверку
cout << "text file #1: "; cin >> t1fn;
cout << "text file #2: "; cin >> t2fn;
// открыть файлы
ifstream infile1( t1fn.c_str());
ifstream infile2( t2fn.c_str());
// специальная форма итератора
// обычно diff_type подразумевается по умолчанию ...
istream_iterator< string, diff_type > input_set1( infile1 ), eos;
istream_iterator< string, diff_type > input_set2( infile2 );
// специальная форма итератора
copy( input_set1, eos, back_inserter( t1 ));
copy( input_set2, eos, back_inserter( t2 ));
sample.push_back( t1 ); sample.push_back( t2 );
process_vocab( &sample );
}
Упражнение 12.2
Длина слова – не единственная и, вероятно, не лучшая мера трудности текста. Другой возможный критерий – это длина предложения. Напишите программу, которая читает текст из файла либо со стандартного ввода, строит вектор строк для каждого предложения и передает его алгоритму count(). Выведите предложения в порядке сложности. Любопытный способ сделать это – сохранить каждое предложение как одну большую строку во втором векторе строк, а затем передать этот вектор алгоритму sort() вместе с объектом-функцией, который считает, что чем строка короче, тем она меньше. (Более подробно с описанием конкретного обобщенного алгоритма, а также с иллюстрацией его применения вы может ознакомиться в Приложении, где все алгоритмы перечислены в алфавитном порядке.)
Упражнение 12.3
Более надежную оценку уровня трудности текста дает анализ структурной сложности предложений. Пусть каждой запятой присваивается 1 балл, каждому двоеточию или точке с запятой – 2 балла, а каждому тире – 3 балла. Модифицируйте программу из упражнения 12.2 так, чтобы она подсчитывала сложность каждого предложения. Воспользуйтесь алгоритмом count_if() для нахождения каждого из знаков препинания в векторе предложений. Выведите предложения в порядке сложности.
Использование пространства имен
Предположим, что мы хотим предоставить в общее пользование наш класс Array, разработанный в предыдущих примерах. Однако не мы одни занимались этой проблемой; возможно, кем-то где-то, скажем, в одном из подразделений компании Intel был создан одноименный класс. Из-за того что имена этих классов совпадают, потенциальные пользователи не могут задействовать оба класса одновременно, они должны выбрать один из них. Эта проблема решается добавлением к имени класса некоторой строки, идентифицирующей его разработчиков, скажем,
class Cplusplus_Primer_Third_Edition_Array { ... };
Конечно, это тоже не гарантирует уникальность имени, но с большой вероятностью избавит пользователя от данной проблемы. Как, однако, неудобно пользоваться столь длинными именами!
Стандарт С++ предлагает для решения проблемы совпадения имен механизм, называемый пространством имен. Каждый производитель программного обеспечения может заключить свои классы, функции и другие объекты в свое собственное пространство имен. Вот как выглядит, например, объявление нашего класса Array:
namespace Cplusplus_Primer_3E {
template <class elemType> class Array { ... };
}
Ключевое слово namespace задает пространство имен, определяющее видимость нашего класса и названное в данном случае Cplusplus_Primer_3E. Предположим, что у нас есть классы от других разработчиков, помещенные в другие пространства имен:
namespace IBM_Canada_Laboratory {
template <class elemType> class Array { ... };
class Matrix { ... };
}
namespace Disney_Feature_Animation {
class Point { ... };
template <class elemType> class Array { ... };
}
По умолчанию в программе видны объекты, объявленные без явного указания пространства имен; они относятся к глобальному пространству имен. Для того чтобы обратиться к объекту из другого пространства, нужно использовать его квалифицированное имя, которое состоит из идентификатора пространства имен и идентификатора объекта, разделенных оператором разрешения области видимости (::). Вот как выглядят обращения к объектам приведенных выше примеров:
Cplusplus_Primer_3E::Array<string> text;
IBM_Canada_Laboratory::Matrix mat;
Disney_Feature_Animation::Point origin(5000,5000);
Для удобства использования можно назначать псевдонимы пространствам имен. Псевдоним выбирают коротким и легким для запоминания. Например:
// псевдонимы
namespace LIB = IBM_Canada_Laboratory;
namespace DFA = Disney_Feature_Animation;
int main()
{
LIB::Array<int> ia(1024);
}
Псевдонимы употребляются и для того, чтобы скрыть использование пространств имен. Заменив псевдоним, мы можем сменить набор задействованных функций и классов, причем во всем остальном код программы останется таким же. Исправив только одну строчку в приведенном выше примере, мы получим определение уже совсем другого массива:
namespace LIB = Cplusplus_Primer_3E;
int main()
{
LIB::Array<int> ia(1024);
}
Конечно, чтобы это стало возможным, необходимо точное совпадение интерфейсов классов и функций, объявленных в этих пространствах имен. Представим, что класс Array из Disney_Feature_Animation не имеет конструктора с одним параметром – размером. Тогда следующий код вызовет ошибку:
namespace LIB = Disney_Feature_Animation;
int main()
{
LIB::Array<int> ia(1024);
}
Еще более удобным является способ использования простого, неквалифицированного имени для обращения к объектам, определенным в некотором пространстве имен. Для этого существует директива using:
#include "IBM_Canada_Laboratory.h"
using namespace IBM_Canada_Laboratory;
int main()
{
// IBM_Canada_Laboratory::Matrix
Matrix mat(4,4);
// IBM_Canada_Laboratory::Array
Array<int> ia(1024);
// ...
}
Пространство имен IBM_Canada_Laboratory становится видимым в программе. Можно сделать видимым не все пространство, а отдельные имена внутри него (селективная директива using):
#include "IBM_Canada_Laboratory.h"
using namespace IBM_Canada_Laboratory::Matrix;
// видимым становится только Matrix
int main()
{
// IBM_Canada_Laboratory::Matrix
Matrix mat(4,4);
// Ошибка: IBM_Canada_Laboratory::Array невидим
Array<int> ia(1024);
// ...
}
Как мы уже упоминали, все компоненты стандартной библиотеки С++ объявлены внутри пространства имен std. Поэтому простого включения заголовочного файла недостаточно, чтобы напрямую пользоваться стандартными функциями и классами:
#include <string>
// ошибка: string невидим
string current_chapter = "Обзор С++";
Необходимо использовать директиву using:
#include <string>
using namespace std;
// Ok: видим string
string current_chapter = "Обзор С++";
Заметим, однако, что таким образом мы возвращаемся к проблеме “засорения” глобального пространства имен, ради решения которой и был создан механизм именованных пространств. Поэтому лучше использовать либо квалифицированное имя:
#include <string>
// правильно: квалифицированное имя
std::string current_chapter = "Обзор С++";
либо селективную директиву using:
#include <string>
using namespace std::string;
// Ok: string видим
string current_chapter = "Обзор С++";
Мы рекомендуем пользоваться последним способом.
В большинстве примеров этой книги директивы пространств имен были опущены. Это сделано ради сокращения размера кода, а также потому, что большинство примеров были скомпилированы компилятором, не поддерживающим пространства имен – достаточно недавнего нововведения С++. (Детали применения using-объявлений при работе с стандартной библиотекой С++ обсуждаются в разделе 8.6.)
В нижеследующих главах мы создадим еще четыре класса: String, Stack, List и модификацию Stack. Все они будут заключены в одно пространство имен – Cplusplus_Primer_3E. (Более подробно работа с пространствами имен рассматривается в главе 8.)
Упражнение 2.21
Дано пространство имен
namespace Exercize {
template <class elemType>
class Array { ... };
template <class EType>
void print (Array< EType > );
class String { ... }
template <class ListType>
class List { ... };
}
и текст программы:
int main() {
const int size = 1024;
Array<String> as (size);
List<int> il (size);
// ...
Array<String> *pas = new Array<String>(as);
List<int> *pil = new List<int>(il);
print (*pas);
}
Программа не компилируется, поскольку объявления используемых классов заключены в пространство имен Exercise. Модифицируйте код программы, используя
(a) квалифицированные имена
(b) селективную директиву using
(c) механизм псевдонимов
(d) директиву using
Использование шаблонов
Наш класс IntArray служит хорошей альтернативой встроенному массиву целых чисел. Но в жизни могут потребоваться массивы для самых разных типов данных. Можно предположить, что единственным отличием массива элементов типа double от нашего является тип данных в объявлениях, весь остальной код совпадает буквально.
Для решения данной проблемы в С++ введен механизм шаблонов. В объявлениях классов и функций допускается использование параметризованных типов. Типы-параметры заменяются в процессе компиляции настоящими типами, встроенными или определенными пользователем. Мы можем создать шаблон класса Array, заменив в классе IntArray тип элементов int на обобщенный тип-параметр. Позже мы конкретизируем типы-параметры, подставляя вместо них реальные типы int, double и string. В результате появится способ использовать эти конкретизации так, как будто мы на самом деле определили три разных класса для этих трех типов данных.
Вот как может выглядеть шаблон класса Array:
template <class elemType>
class Array {
public:
explicit Array( int sz = DefaultArraySize );
Array( const elemType *ar, int sz );
Array( const Array &iA );
virtual ~Array() { delete[] _ia; }
Array& operator=( const Array & );
int size() const { return _size; }
virtual elemType& operator[]( int ix )
{ return _ia[ix]; }
virtual void sort( int,int );
virtual int find( const elemType& );
virtual elemType min();
virtual elemType max();
protected:
void init( const elemType*, int );
void swap( int, int );
static const int DefaultArraySize = 12;
int _size;
elemType *_ia;
};
Ключевое слово template говорит о том, что задается шаблон, параметры которого заключаются в угловые скобки (<>). В нашем случае имеется лишь один параметр elemType; ключевое слово class перед его именем сообщает, что этот параметр представляет собой тип.
При конкретизации класса-шаблона Array параметр elemType заменяется на реальный тип при каждом использовании, как показано в примере:
#include <iostream>
#include "Array.h"
int main()
{
а const int array_size = 4;
а // elemType чрьхэ хЄё эр int
а Array<int> ia(array_size);
а // elemType чрьхэ хЄё эр double
а Array<double> da(array_size);
а // elemType чрьхэ хЄё эр char
а Array<char> ca(array_size);
а int ix;
а for ( ix = 0; ix < array_size; ++ix ) {
ааааааааа ia[ix] = ix;
ааааааааа da[ix] = ix * 1.75;
ааааааааа ca[ix] = ix + 'a';
а }
а for ( ix = 0; ix < array_size; ++ix )
ааааааааа cout << "[ " << ix << " ]а ia: "а << ia[ix]
ааааааааааааа << "\tca: " << ca[ix]
ааааааааааааа << "\tda: " << da[ix] << endl;
а return 0;
}
¦фхё№ юяЁхфхыхэv ЄЁш ¤ъчхьяы Ёр ъырёёр Array:
Array<int> ia(array_size);
Array<double> da(array_size);
Array<char> ca(array_size);
+Єю фхырхЄ ъюьяшы ЄюЁ, тёЄЁхЄшт Єръюх юс· тыхэшх? ¦юфёЄрты хЄ ЄхъёЄ °рсыюэр Array, чрьхэ ярЁрьхЄЁ elemType эр ЄюЄ Єшя, ъюЄюЁvщ єърчрэ т ърцфюь ъюэъЁхЄэюь ёыєўрх. TыхфютрЄхы№эю, юс· тыхэш ўыхэют яЁшюсЁхЄр¦Є т яхЁтюь ёыєўрх Єръющ тшф:
// Array<int> ia(array_size);
int _size;
int *_ia;
¦рьхЄшь, ўЄю ¤Єю т ЄюўэюёЄш ёююЄтхЄёЄтєхЄ юяЁхфхыхэш¦ ьрёёштр IntArray.
-ы юёЄрт°шїё фтєї ёыєўрхт ьv яюыєўшь ёыхфє¦•шщ ъюф:
// Array<double> da(array_size);
intаааа _size;
double *_ia;
// Array<char> ca(array_size);
intаа _size;
char *_ia;
+Єю яЁюшёїюфшЄ ё ЇєэъЎш ьш-ўыхэрьш? T эшї Єюцх Єшя-ярЁрьхЄЁ elemType чрьхэ хЄё эр Ёхры№эvщ Єшя, юфэръю ъюьяшы ЄюЁ эх ъюэъЁхЄшчшЁєхЄ Єх ЇєэъЎшш, ъюЄюЁvх эх тvчvтр¦Єё т ъръюь-ышсю ьхёЄх яЁюуЁрььv. (¦юфЁюсэхх юс ¤Єюь т Ёрчфхых 16.8.)
¦Ёш тvяюыэхэшш яЁюуЁрььр ¤Єюую яЁшьхЁр тvфрёЄ ёыхфє¦•шщ Ёхчєы№ЄрЄ:
[ 0 ]а ia: 0ааа ca: aаа da: 0
[ 1 ]а ia: 1ааа ca: bаа da: 1.75
[ 2 ]а ia: 2ааа ca: cаа da: 3.5
[ 3 ]а ia: 3ааа ca: dаа da: 5.25
¦хїрэшчь °рсыюэют ьюцэю шёяюы№чютрЄ№ ш т эрёыхфєхьvї ъырёёрї. TюЄ ъръ тvуы фшЄ юяЁхфхыхэшх °рсыюэр ъырёёр ArrayRC:
#include <cassert>
#include "Array.h"
template <class elemType>
class ArrayRC : public Array<elemType> {
public:
ааа ArrayRC( int sz = DefaultArraySize )
ааааааааааа : Array<elemType>( sz ) {}
ааа ArrayRC( const ArrayRC& r )
ааааааааааа : Array<elemType>( r ) {}
ааа ArrayRC( const elemType *ar, int sz )
ааааааааааа : Array<elemType>( ar, sz ) {}
ааа elemType& ArrayRC<elemType>::operator[]( int ix )
ааа {
ааааааа assert( ix >= 0 && ix < Array<elemType>::_size );
ааааааа return _ia[ ix ];
ааа }
private:
// ...
};
¦юфёЄрэютър Ёхры№эvї ярЁрьхЄЁют тьхёЄю Єшяр-ярЁрьхЄЁр elemType яЁюшёїюфшЄ ъръ т срчютюь, Єръ ш т яЁюшчтюфэюь ъырёёрї. +яЁхфхыхэшх
ArrayRC<int> ia_rc(10);
тхфхЄ ёхс Єюўэю Єръ цх, ъръ юяЁхфхыхэшх IntArrayRC шч яЁхфvфє•хую Ёрчфхыр. Lчьхэшь яЁшьхЁ шёяюы№чютрэш шч яЁхфvфє•хую Ёрчфхыр. ¦Ёхцфх тёхую, ўЄюсv юяхЁрЄюЁ
// ЇєэъЎш¦ swap() Єюцх ёыхфєхЄ ёфхырЄ№ °рсыюэюь
swap( ia1, 1, ia1.size() );
сvы фюяєёЄшьvь, эрь яюЄЁхсєхЄё яЁхфёЄртшЄ№ ЇєэъЎш¦ swap() т тшфх °рсыюэр.
#include "Array.h"
template <class elemType>
inline void
swap( Array<elemType> &array, int i, int j )
{
а elemType tmp = array[ i ];
а array[ i ] = array[ j ];
а array[ j ] = tmp;
}
¦Ёш ърцфюь тvчютх swap() ухэхЁшЁєхЄё яюфїюф •р ъюэъЁхЄшчрЎш , ъюЄюЁр чртшёшЄ юЄ Єшяр ьрёёштр. TюЄ ъръ тvуы фшЄ яЁюуЁрььр, шёяюы№чє¦•р °рсыюэv Array ш ArrayRC:
#include <iostream>
#include "Array.h"
#include "ArrayRC.h"
template <class elemType>
inline void
swap( Array<elemType> &array, int i, int j )
{
а elemType tmp = array[ i ];
а array[ i ] = array[ j ];
а array[ j ] = tmp;
}
int main()
{
а Array<int>аа ia1;
а ArrayRC<int> ia2;
а cout << "swap() with Array<int> ia1" << endl;
а int size = ia1.size();
а swap( ia1, 1, size );
а cout << "swap() with ArrayRC<int> ia2" << endl;
а size = ia2.size();
а swap( ia2, 1, size );
а return 0;
}
LяЁрцэхэшх 2.13
¦єёЄ№ ьv шьххь ёыхфє¦•шх юс· тыхэш Єшяют:
template<class elemType> class Array;
enum Status { ... };
typedef string *Pstring;
+ёЄ№ ыш ю°шсъш т яЁштхфхээvї эшцх юяшёрэш ї юс·хъЄют?
(a) Array< int*& > pri(1024);
(b) Array< Array<int> > aai(1024);
(c) Array< complex< double > > acd(1024);
(d) Array< Status > as(1024);
(e) Array< Pstring > aps(1024);
LяЁрцэхэшх 2.14
¦хЁхяш°шЄх ёыхфє¦•хх юяЁхфхыхэшх, ёфхырт шч эхую °рсыюэ ъырёёр:
class example1 {
public:
а example1 (double min, double max);
а example1 (const double *array, int size);
а double& operator[] (int index);
а bool operator== (const example1&) const;
а bool insert (const double*, int);
а bool insert (double);
а double min (double) const { return _min; };
а double max (double) const { return _max; };
а void min (double);
а void max (double);
а int count (double value) const;
private:
а int size;
а double *parray;
а double _min;
а double _max;
}
LяЁрцэхэшх 2.15
LьххЄё ёыхфє¦•шщ °рсыюэ ъырёёр:
template <class elemType> class Example2 {
public:
а explicit Example2 (elemType val=0) : _val(val) {};
а bool min(elemType value) { return _val < value; }
а void value(elemType new_val) { _val = new_val; }
а void print (ostream &os) { os << _val; }
private:
а elemType _val;
}
template <class elemType>
ostream& operator<<(ostream &os,const Example2<elemType> &ex)
а { ex.print(os); return os; }
¦ръшх фхщёЄтш тvчvтр¦Є ёыхфє¦•шх шэёЄЁєъЎшш?
(a) Example2<Array<int>*> ex1;
(b) ex1.min (&ex1);
(c) Example2<int> sa(1024),sb;
(d) sa = sb;
(e) Example2<string> exs("Walden");
(f) cout << "exs: " << exs << endl;
LяЁрцэхэшх 2.16
¦ЁшьхЁ шч яЁхфvфє•хую єяЁрцэхэш эръырфvтрхЄ юяЁхфхыхээvх юуЁрэшўхэш эр Єшяv фрээvї, ъюЄюЁvх ьюуєЄ сvЄ№ яюфёЄртыхэv тьхёЄю elemType. Tръ,а ярЁрьхЄЁ ъюэёЄЁєъЄюЁр шьххЄ яю єьюыўрэш¦ чэрўхэшх 0:
explicit Example2 (elemType val=0) : _val(val) {};
+фэръю эх тёх Єшяv ьюуєЄ сvЄ№ шэшЎшрышчшЁютрэv эєыхь (эряЁшьхЁ, Єшя string), яю¤Єюьє юяЁхфхыхэшх юс·хъЄр
Example2<string> exs("Walden");
ты хЄё яЁртшы№эvь, р
Example2<string> exs2;
яЁштхфхЄ ъ ёшэЄръёшўхёъющ ю°шсъх[4]. Tръцх ю°шсюўэvь сєфхЄ тvчют ЇєэъЎшш min(), хёыш фы фрээюую Єшяр эх юяЁхфхыхэр юяхЁрЎш ьхэ№°х. T++ эх яючтюы хЄ чрфрЄ№ юуЁрэшўхэш фы Єшяют, яюфёЄрты хьvї т °рсыюэv. ¦ръ тv фєьрхЄх, сvыю сv яюыхчэvь шьхЄ№ Єръє¦ тючьюцэюёЄ№? +ёыш фр, яюяЁюсєщЄх яЁшфєьрЄ№ ёшэЄръёшё чрфрэш юуЁрэшўхэшщ ш яхЁхяш°шЄх т эхь юяЁхфхыхэшх ъырёёр Example2. +ёыш эхЄ, яю ёэшЄх яюўхьє.
LяЁрцэхэшх 2.17
¦ръ сvыю яюърчрэю т яЁхфvфє•хь єяЁрцэхэшш, яюяvЄър шёяюы№чютрЄ№ °рсыюэ Example2 ё Єшяюь, фы ъюЄюЁюую эх юяЁхфхыхэр юяхЁрЎш ьхэ№°х, яЁштхфхЄ ъ ёшэЄръёшўхёъющ ю°шсъх. +фэръю ю°шсър яЁю тшЄё Єюы№ъю Єюуфр, ъюуфр т ЄхъёЄх ъюьяшышЁєхьющ яЁюуЁрььv фхщёЄтшЄхы№эю тёЄЁхЄшЄё тvчют ЇєэъЎшш min(), т яЁюЄштэюь ёыєўрх ъюьяшы Ўш яЁющфхЄ єёях°эю. ¦ръ тv ёўшЄрхЄх, юяЁртфрэю ыш Єръюх яютхфхэшх? =х ыєў°х ыш яЁхфєяЁхфшЄ№ юс ю°шсъх ёЁрчє, яЁш юсЁрсюЄъх юяшёрэш °рсыюэр? ¦ю ёэшЄх ётюх ьэхэшх.
Итератор istream_iterator
В общем виде объявление потокового итератора чтения istream_iterator имеет форму:
istream_iterator<Type> identifier( istream& );1.
где Type– это любой встроенный или пользовательский тип класса, для которого определен оператор ввода. Аргументом конструктора может быть объект либо класса istream, например cin, либо производного от него класса с открытым типом наследования – ifstream:
#include <iterator>
#include <fstream>
#include <string>
#include <complex>
// прочитать последовательность объектов типа complex
// из стандартного ввода
istream_iterator< complex > is_complex( cin );
// прочитать последовательность строк из именованного файла
ifstream infile( "C++Primer" );
istream_iterator< string > is_string( infile );
При каждом применении оператора инкремента к объекту типа istream_iterator читается следующий элемент из входного потока, для чего используется оператор operator>>(). Чтобы сделать то же самое в обобщенных алгоритмах, необходимо предоставить пару итераторов, обозначающих начальную и конечную позицию в файле. Начальную позицию дает istream_iterator, инициализированный объектом istream, – такой, скажем, как is_string. Для получения конечной позиции мы используем специальный конструктор по умолчанию класса istream_iterator:
// конструирует итератор end_of_stream, который будет служить маркером
// конца потока в итераторной паре
istream_iterator< string > end_of_stream
vector<string> text;
// правильно: передаем пару итераторов
copy( is_string, end_of_stream,
inserter( text, text.begin() ));
Итератор ostream_iterator
Объявление потокового итератора записи ostream_iterator может быть представлено в двух формах:
ostream_iterator<Type> identifier( ostream& )
ostream_iterator<Type> identifier( ostream&, char * delimiter )
где Type – это любой встроенный или пользовательский тип класса, для которого определен оператор вывода (operator<<). Во второй форме delimiter – это разделитель, то есть C-строка символов, которая выводится в файл после каждого элемента. Такая строка должна заканчиваться двоичным нулем, иначе поведение программы не определено (скорее всего, она аварийно завершит выполнение). В качестве аргумента ostream может выступать объект класса ostream, например cout, либо производного от него класса с открытым типом наследования, скажем ofstream:
#include <iterator>
#include <fstream>
#include <string>
#include <complex>
// записать последовательность объектов типа complex
// в стандартный вывод, разделяя элементы пробелами
ostream_iterator< complex > os_complex( cin, " " );
// записать последовательность строк в именованный файл
ofstream outfile( "dictionary" );
ostream_iterator< string > os_string( outfile, "\n" );
Вот простой пример чтения из стандартного ввода и копирования на стандартный вывод с помощью безымянных потоковых итераторов и обобщенного алгоритма copy():
#include <iterator>
#include <algorithm>
#include <iostream>
int main()
{
copy( istream_iterator< int >( cin ),
istream_iterator< int >(),
ostream_iterator< int >( cout, " " ));
}
Ниже приведена небольшая программа, которая открывает указанный пользователем файл и копирует его на стандартный вывод, применяя для этого алгоритм copy() и потоковый итератор записи ostream_iterator:
#include <string>
#include <algorithm>
#include <fstream>
#include <iterator>
main()
{
string file_name;
cout << "please enter a file to open: ";
cin >> file_name;
if ( file_name.empty() || !cin ) {
cerr << "unable to read file name\n"; return -1;
}
ifstream infile( file_name.c_str());
if ( !infile ) {
cerr << "unable to open " << file_name << endl;
return -2;
}
istream_iterator< string > ins( infile ), eos;
ostream_iterator< string > outs( cout, " " );
copy( ins, eos, outs );
}
Итераторы
Итератор предоставляет обобщенный способ перебора элементов любого контейнера– как последовательного, так и ассоциативного. Пусть iter является итератором для какого-либо контейнера. Тогда
++iter;
перемещает итератор так, что он указывает на следующий элемент контейнера, а
*iter;
разыменовывает итератор, возвращая элемент, на который он указывает.
Все контейнеры имеют функции-члены begin() и end().
begin() возвращает итератор, указывающий на первый элемент контейнера.
end() возвращает итератор, указывающий на элемент, следующий за последним в контейнере.
Чтобы перебрать все элементы контейнера, нужно написать:
for ( iter = container. begin();
iter != container.end(); ++iter )
do_something_with_element( *iter );
Объявление итератора выглядит слишком сложным. Вот определение пары итераторов вектора типа string:
// vector<string> vec;
vector<string>::iterator iter = vec.begin();
vector<string>::iterator iter_end = vec.end();
В классе vector для определения iterator используется typedef. Синтаксис
vector<string>::iterator
ссылается на iterator, определенный с помощью typedef внутри класса vector, содержащего элементы типа string.
Для того чтобы напечатать все элементы вектора, нужно написать:
for( ; iter != iter_end; ++iter )
cout << *iter << '\n';
Здесь значением *iter выражения является, конечно, элемент вектора.
В дополнение к типу iterator в каждом контейнере определен тип const_iterator, который необходим для навигации по контейнеру, объявленному как const. const_iterator позволяет только читать элементы контейнера:
#include <vector>
void even_odd( const vector<int> *pvec,
vector<int> *pvec_even,
vector<int> *pvec_odd )
{
// const_iterator необходим для навигации по pvec
vector<int>::const_iterator c_iter = pvec->begin();
vector<int>::const_1terator c_iter_end = pvec->end();
for ( ; c_iter != c_iter_end; ++c_iter )
if ( *c_iter % 2 )
pvec_even->push_back( *c_iter );
else pvec_odd->push_back( *c_iter );
}
Что делать, если мы хотим просмотреть некоторое подмножество элементов, например взять каждый второй или третий элемент, или хотим начать с середины? Итераторы поддерживают адресную арифметику, а значит, мы можем прибавить некоторое число к итератору:
vector<int>::iterator iter = vec->begin()+vec.size()/2;
iter получает значение адреса элемента из середины вектора, а выражение
iter += 2;
сдвигает iter на два элемента.
Арифметические действия с итераторами возможны только для контейнеров vector и deque. list не поддерживает адресную арифметику, поскольку его элементы не располагаются в непрерывной области памяти. Следующее выражение к списку неприменимо:
ilist.begin() + 2;
так как для перемещения на два элемента необходимо два раза перейти по адресу, содержащемуся в закрытом члене next. У классов vector и deque перемещение на два элемента означает прибавление 2 к указателю на текущий элемент. (Адресная арифметика рассматривается в разделе 3.3.)
Объект контейнерного типа может быть инициализирован парой итераторов, обозначающих начало и конец последовательности копируемых в новый объект элементов. (Второй итератор должен указывать на элемент, следующий за последним копируемым.) Допустим, есть вектор:
#include <vector>
#include <string>
#include <iostream>
int main()
{
vector<string> svec;
string intext;
while ( cin >> intext )
svec.push_back( intext );
// обработать svec ...
}
Вот как можно определить новые векторы, инициализируя их элементами первого вектора:
int main() {
vector<string> svec;
// ...
// инициализация svec2 всеми элементами svec
vector<string> svec2( svec.begin(), svec.end() );
// инициализация svec3 первой половиной svec
vector<string>::iterator it =
svec.begin() + svec.size()/2;
vector<string> svec3 ( svec.begin(), it );
// ...
}
Использование специального типа istream_iterator (о нем рассказывается в разделе 12.4.3) упрощает чтение элементов из входного потока в svec:
#include <vector>
#include <string>
#include <iterator>
int mainQ
{
// привязка istream_iterator к стандартному вводу
istream_iterator<string> infile( cin );
// istream_iterator, отмечающий конец потока
istream_iterator<string> eos;
// инициализация svec элементами, считываемыми из cin;
vector<string> svec( infile, eos );
// ...
}
Кроме итераторов, для задания диапазона значений, инициализирующих контейнер, можно использовать два указателя на массив встроенного типа. Пусть есть следующий массив строк:
#include <string>
string words[4] = {
"stately", "plump", "buck", "mulligan"
};
Мы можем инициализировать вектор с помощью указателей на первый элемент массива и на элемент, следующий за последним:
vector< string > vwords( words, words+4 );
Второй указатель служит “стражем”: элемент, на который он указывает, не копируется.
Аналогичным образом можно инициализировать список целых элементов:
int ia[6] = { 0, 1, 2, 3, 4, 5 };
list< int > ilist( ia, ia+6 );
В разделе 12.4 мы снова обратимся к итераторам и опишем их более детально. Сейчас информации достаточно для того, чтобы использовать итераторы в нашей системе текстового поиска. Но прежде чем вернуться к ней, рассмотрим некоторые дополнительные операции, поддерживаемые контейнерами.
Упражнение 6.9
Какие ошибки допущены при использовании итераторов:
const vector< int > ivec;
vector< string > svec;
list< int > ilist;
(a) vector<int>::iterator it = ivec.begin();
(b) list<int>::iterator it = ilist.begin()+2;
(c) vector<string>::iterator it = &svec[0];
(d) for ( vector<string>::iterator
it = svec.begin(); it != 0; ++it )
// ...
Упражнение 6.10
Найдите ошибки в использовании итераторов:
int ia[7] = { 0, 1, 1, 2, 3, 5, 8 };
string sa[6] = {
"Fort Sumter", "Manassas", "Perryville", "Vicksburg",
"Meridian", "Chancellorsvine" };
(a) vector<string> svec( sa, &sa[6] );
(b) list<int> ilist( ia+4, ia+6 );
(c) list<int> ilist2( ilist.begin(), ilist.begin()+2 );
(d) vector<int> ivec( &ia[0], ia+8 );
(e) list<string> slist( sa+6, sa );
(f) vector<string> svec2( sa, sa+6 );
Итераторы вставки
Вот еще один фрагмент программы, в котором есть тонкая, но серьезная ошибка. Видите ли вы, в чем она заключается?
int ia[] = { 0, 1, 1, 2, 3, 5, 5, 8 };
vector< int > ivec( ia, ia+8 ), vres;
// ...
// поведение программы во время выполнения не определено
unique_copy( ivec.begin(), ivec.end(), vres.begin() );
Проблема вызвана тем, что алгоритм unique_copy() использует присваивание для копирования значения каждого элемента из вектора ivec, но эта операция завершится неудачно, поскольку в vres не выделено место для хранения девяти целых чисел.
Можно было бы написать две версии алгоритма unique_copy(): одна присваивает элементы, а вторая вставляет их. Эта последняя версия должна, в таком случае, поддерживать вставку в начало, в конец или в произвольное место контейнера.
Альтернативный подход, принятый в стандартной библиотеке, заключается в определении трех адаптеров, которые возвращают специальные итераторы вставки:
back_inserter() вызывает определенную для контейнера операцию вставки push_back() вместо оператора присваивания. Аргументом back_inserter() является сам контейнер. Например, вызов unique_copy() можно исправить, написав:
// правильно: теперь unique_copy() вставляет элементы с помощью
// vres.push_back()...
unique_copy( ivec.begin(), ivec.end(),
back_inserter( vres ) );
front_inserter() вызывает определенную для контейнера операцию вставки push_front() вместо оператора присваивания. Аргументом front_inserter() тоже является сам контейнер. Заметьте, однако, что класс vector не поддерживает push_front(), так что использовать такой адаптер для вектора нельзя:
// увы, ошибка:
// класс vector не поддерживает операцию push_front()
// следует использовать контейнеры deque или list
unique_copy( ivec.begin(), ivec.end(),
front_inserter( vres ) );
inserter() вызывает определенную для контейнера операцию вставки insert() вместо оператора присваивания. inserter() принимает два аргумента: сам контейнер и итератор, указывающий позицию, с которой должна начаться вставка:
unique_copy( ivec.begin(), ivec.end(),
inserter( vres ), vres.begin() );
Итератор, указывающий на позицию начала вставки, сдвигается вперед после каждой вставки, так что элементы располагаются в нужном порядке, как если бы мы написали:
vector< int >::iterator iter = vres.begin(),
iter2 = ivec.begin();
for ( ; iter2 != ivec.end() ++ iter, ++iter2 )
vres.insert( iter, *iter2 );
Изменения в третьем издании
Все изменения можно разбить на четыре основные категории:
материал, посвященный нововведениям языка: обработке исключений, идентификации типа во время выполнения, пространству имен, встроенному типу bool, новому синтаксису приведения типов;
материал, посвященный стандартной библиотеке С++, в том числе типам complex, string, auto_ptr, pair, последовательным и ассоциативным контейнерам (в основном это list, vector, map и set) и обобщенным алгоритмам;
коррективы в старом тексте, отражающие улучшения, расширения и изменения, которые новый стандарт С++ привнес в существовавшие ранее средства языка. Примером улучшения может служить использование предваряющих объявлений для вложенных типов, ранее отсутствовавшая. В качестве примера изменения можно привести возможность для экземпляра виртуальной функции производного класса возвращать тип, производный от типа значения, возвращаемого экземпляром той же функции из базового класса. Это изменение поддерживает операцию с классами, которую иногда называют клонированием
или фабрикацией классов (виртуальная функция clone() иллюстрируется в разделе 17.5.7). Пример расширения языка – возможность явно специализировать один или более параметров-типов для шаблонов функций (на самом деле, весь механизм шаблонов был радикально расширен – настолько, что его можно назвать новым средством языка!);
изменения в подходе к использованию большинства продвинутых средств языка – шаблонов и классов. Стен считает, что его переход из сравнительно узкого круга разработчиков языка С++ в широкий круг пользователей позволил ему глубже понять проблемы последних. Соответственно в этом издании мы уделили большее внимание концепциям, которые стояли за появлением того или иного средства языка, тому, как лучше его применять и как избежать подводных камней.
Явная специализация шаблона *
Не всегда удается написать шаблон функции, который годился бы для всех возможных типов, с которыми он может быть конкретизирован. В некоторых случаях имеется специальная информация о типе, позволяющая написать более эффективную функцию, чем конкретизированная по шаблону. А иногда общее определение, предоставляемое шаблоном, для некоторого типа просто не работает. Рассмотрим, например, следующее определение шаблона функции max():
// обобщенное определение шаблона
template <class T>
T max( T t1, T t2 ) {
return ( t1 > t2 ? t1 : t2 );
}
Когда этот шаблон конкретизируется с аргументом типа const char*, то обобщенное определение оказывается семантически некорректным, если мы интерпретируем каждый аргумент как строку символов в смысле языка C, а не как указатель на символ. В этом случае необходимо предоставить специализированное определение для конкретизации шаблона.
Явное определение специализации – это такое определение, в котором за ключевым словом template следует пара угловых скобок <>, а за ними – определение специализированного шаблона. Здесь указывается имя шаблона, аргументы, для которых он специализируется, список параметров функции и ее тело. В следующем примере для max(const char*, const char*) определена явная специализация:
#include <cstring>
// явная специализация для const char*:
// имеет приоритет над конкретизацией шаблона
// по обобщенному определению
typedef const char *PCC;
template<> PCC max< PCC >( PCC s1, PCC s2 ) {
return ( strcmp( s1, s2 ) > 0 ? s1 : s2 );
Поскольку имеется явная специализация, шаблон не будет конкретизирован с типом const char* при вызове в программе функции max(const char*, const char*). При любом обращении к max() с двумя аргументами типа const char* работает специализированное определение. Для любых других обращений функция сначала конкретизируется по обобщенному определению шаблона, а затем вызывается. Вот как это выглядит:
#include <iostream>
// здесь должно быть определение шаблона функции max()
// и его специализации для аргументов const char*
int main() {
// вызов конкретизированной функции: int max< int >( int, int );
int i = max( 10, 5 );
// вызов явной специализации:
// const char* max< const char* >( const char*, const char* );
const char *p = max( "hello", "world" );
cout << "i: " << i << " p: " << p << endl;
return 0;
}
Можно объявлять явную специализацию шаблона функции, не определяя ее. Например, для функции max(const char*, const char*) она объявляется так:
// объявление явной специализации шаблона функции
template< > PCC max< PCC >( PCC, PCC );
При объявлении или определении явной специализации шаблона функции нельзя опускать слово template и следующую за ним пару скобок <>. Кроме того, в объявлении специализации обязательно должен быть список параметров функции:
// ошибка: неправильные объявления специализации
// отсутствует template<>
PCC max< PCC >( PCC, PCC );
// отсутствует список параметров
template<> PCC max< PCC >;
Однако здесь можно опускать задание аргументов шаблона, если они выводятся из формальных параметров функции:
// правильно: аргумент шаблона const char* выводится из типов параметров
template<> PCC max( PCC, PCC );
В следующем примере шаблон функции sum() явно специализирован:
template <class T1, class T2, class T3>
T1 sum( T2 op1, T3 op2 );
// объявления явных специализаций
// ошибка: аргумент шаблона для T1 не может быть выведен;
// он должен быть задан явно
template<> double sum( float, float );
// правильно: аргумент для T1 задан явно,
// T2 и T3 выводятся и оказываются равными float
template<> double sum<double>( float, float );
// правильно: все аргументы заданы явно
template<> int sum<int,char>( char, char );
Пропуск части template<> в объявлении явной специализации не всегда является ошибкой. Например:
// обобщенное определение шаблона
template <class T>
T max( T t1, T t2 ) { /* ... */ }
// правильно: обычное объявление функции
const char* max( const char*, const char*);
Однако эта инструкция не является специализацией шаблона функции. Здесь просто объявляется обычная функция с типом возвращаемого значения и списком параметров, которые соответствуют полученным при конкретизации шаблона. Объявление обычной функции, являющееся конкретизацией шаблона, не считается ошибкой.
Так почему бы просто не объявить обычную функцию? Как было показано в разделе 10.3, для преобразования фактического аргумента функции, конкретизированной по шаблону, в соответствующий формальный параметр в случае, когда этот аргумент принимает участие в выводе аргумента шаблона, может быть применено лишь ограниченное множество преобразований типов. Точно так же обстоит дело и в ситуации, когда шаблон функции специализируется явно: к фактическим аргументам функции при этом тоже применимо лишь ограниченное множество преобразований. Явные специализации не помогают обойти соответствующие ограничения. Если мы хотим выйти за их пределы, то должны определить обычную функцию вместо специализации шаблона. (В разделе 10.8 этот вопрос рассматривается более подробно; там же показано, как работает разрешение перегруженной функции для вызова, который соответствует как обычной функции, так и экземпляру, конкретизированному из шаблона.)
Явную специализацию можно объявлять даже тогда, когда специализируемый шаблон объявлен, но не определен. В предыдущем примере шаблон функции sum() лишь объявлен к моменту специализации. Хотя определение шаблона не обязательно, объявление все же требуется. То, что sum() – шаблон, должно быть известно до того, как это имя может быть специализировано.
Такое объявление должно быть видимо до его использования в исходном файле. Например:
#include <iostream>
#include <cstring>
// обобщенное определение шаблона
template <class T>
T max( T t1, T t2 ) { /* ... */ }
int main() {
// конкретизация функции
// const char* max< const char* >( const char*, const char* );
const char *p = max( "hello", "world" );
cout << "p: " << p << endl;
return 0;
}
// некорректная программа: явная специализация const char *:
// имеет приоритет над обобщенным определением шаблона
typedef const char *PCC;
template<> PCC max< PCC >(PCC s1, PCC s2 ) { /* ... */ }
В предыдущем примере конкретизация max(const char*, const char*) предшествует объявлению явной специализации. Поэтому компилятор имеет право предположить, что функция должна быть конкретизирована по обобщенному определению шаблона. Однако в программе не может одновременно существовать явная специализация и экземпляр, конкретизированный по тому же шаблону с тем же множеством аргументов. Когда в исходном файле после конкретизации встречается явная специализация max(const char*, const char*), компилятор выдает сообщение об ошибке.
Если программа состоит из нескольких файлов, то объявление явной специализации шаблона должно быть видимо в каждом файле, в котором она используется. Не разрешается в одних файлах конкретизировать шаблон функции по обобщенному определению, а в других специализировать с тем же множеством аргументов. Рассмотрим следующий пример:
// --------- max.h -------
// обобщенное определение шаблона
template <class Type>
Type max( Type t1, Type t2 ) { /* ... */ }
// --------- File1.C -------
#include <iostream>
#include "max.h"
void another();
int main() {
// конкретизация функции
// const char* max< const char* >( const char*, const char* );
const char *p = max( "hello", "world" );
cout << "p: " << p << endl;
another();
return 0;
}
// --------- File2.C -------
#include <iostream>
#include <cstring>
#include "max.h"
// явная специализация шаблона для const char*
typedef const char *PCC;
template<> PCC max< PCC >( PCC s1, PCC s2 ) { /* ... */ }
void another() {
// явная специализация
// const char* max< const char* >( const char*, const char* );
const char *p = max( "hi", "again" );
cout << " p: " << p << endl;
return 0;
}
Эта программа состоит из двух файлов. В файле File1.C нет объявления явной специализации max(const char*, const char*). Вместо этого шаблон функции конкретизируется из обобщенного определения. В файле File2.C объявлена явная специализация, и при обращении к max("hi", "again") именно она и вызывается. Поскольку в одной и той же программе функция max(const char*, const char*) то конкретизируется по шаблону, то специализируется явно, компилятор считает программу некорректной. Для исправления этого объявление явной специализации шаблона должно предшествовать вызову функции max(const char*, const char*) в файле File1.C.
Чтобы избежать таких ошибок и гарантировать, что объявление явной специализации шаблона max(const char*, const char*) внесено в каждый файл, где используется шаблон функции max() с аргументами типа const char*, это объявление следует поместить в заголовочный файл "max.h" и включать его во все исходные файлы, в которых используется шаблон max():
// --------- max.h -------
// обобщенное определение шаблона
template <class Type>
Type max( Type t1, Type t2 ) { /* ... */ }
// объявление явной специализации шаблона для const char*
typedef const char *PCC;
template<> PCC max< PCC >( PCC s1, PCC s2 );
// --------- File1.C -------
#include <iostream>
#include "max.h"
void another();
int main() {
// специализация
// const char* max< const char* >( const char*, const char* );
const char *p = max( "hello", "world" );
// ....
}
Упражнение 10.10
Определите шаблон функции count() для подсчета числа появлений некоторого значения в массиве. Напишите вызывающую программу. Последовательно передайте в ней массив значений типа double, int и сhar. Напишите специализированный экземпляр шаблона count() для обработки строк.
Явное преобразование типов
Явное преобразование типов производится при помощи следующих операторов: static_cast, dynamic_cast, const_cast и reinterpret_cast. Заметим, что, хотя иногда явное преобразование необходимо, оно служит потенциальным источником ошибок, поскольку подавляет проверку типов, выполняемую компилятором. Давайте сначала посмотрим, зачем нужно такое преобразование.
Указатель на объект любого неконстантного типа может быть присвоен указателю типа void*, который используется в тех случаях, когда действительный тип объекта либо неизвестен, либо может меняться в ходе выполнения программы. Поэтому указатель void* иногда называют универсальным указателем. Например:
int iva1;
int *pi = 0;
char *pc = 0;
void *pv;
pv = pi; // правильно: неявное преобразование
pv = pc; // правильно: неявное преобразование
const int *pci = &iva1;
pv = pci; // ошибка: pv имеет тип, отличный от const void*;
const void *pcv = pci; // правильно
Однако указатель void* не может быть разыменован непосредственно. Компилятор не знает типа объекта, адресуемого этим указателем. Но это известно программисту, который хочет преобразовать указатель void* в указатель определенного типа. С++ не обеспечивает подобного автоматического преобразования:
#inc1ude <cstring>
int ival = 1024;
void *pv;
int *pi = &iva1;
const char *pc = "a casting call";
void mumble() {
pv = pi; // правильно: pv получает адрес ival
pc = pv; // ошибка: нет стандартного преобразования
char *pstr = new char[ str1en( pc )+1 ];
strcpy( pstr, pc );
}
Компилятор выдает сообщение об ошибке, так как в данном случае указатель pv содержит адрес целого числа ival, и именно этот адрес пытаются присвоить указателю на строку. Если бы такая программа была допущена до выполнения, то вызов функции strcpy(), которая ожидает на входе строку символов с нулем в конце, скорее всего привел бы к краху, потому что вместо этого strcpy() получает указатель на целое число. Подобные ошибки довольно просто не заметить, именно поэтому С++ запрещает неявное преобразование указателя на void в указатель на другой тип. Однако такой тип можно изменить явно:
void mumble 0 {
// правильно: программа по-прежнему содержит ошибку,
// но теперь она компилируется!
// Прежде всего нужно проверить
// явные преобразования типов...
pc = static_cast< char* >( pv );
char *pstr = new char[ str1en( pc )+1 ];
// скорее всего приведет к краху
strcpy( pstr, pc );
}
Другой причиной использования явного преобразования типов может служить необходимость избежать стандартного преобразования или выполнить вместо него собственное. Например, в следующем выражении ival сначала преобразуется в double, потом к нему прибавляется dval, и затем результат снова трансформируется в int.
double dval;
int iva1;
ival += dval;
Можно уйти от ненужного преобразования, явно заменив dval на int:
ival += static_cast< int >( dval );
Третьей причиной является желание избежать неоднозначных ситуаций, в которых возможно несколько вариантов применения правил преобразования по умолчанию. (Мы рассмотрим этот случай в главе 9, когда будем говорить о перегруженных функциях.)
Синтаксис операции явного преобразования типов таков:
cast-name< type >( expression );
Здесь cast-name – одно из ключевых слов static_cast, const_cast, dynamic_cast или reinterpret_cast, а type – тип, к которому приводится выражение expression.
Четыре вида явного преобразования введены для того, чтобы учесть все возможные формы приведения типов. Так const_cast служит для трансформации константного типа в неконстантный и подвижного (volatile) – в неподвижный. Например:
extern char *string_copy( char* );
const char *pc_str;
char *pc = string_copy( const_cast< char* >( pc_str ));
Любое иное использование const_cast вызывает ошибку компиляции, как и попытка подобного приведения с помощью любого из трех других операторов.
С применением static_cast осуществляются те преобразования, которые могут быть сделаны неявно, на основе правил по умолчанию:
double d = 97.0;
char ch = static_cast< char >( d );
Зачем использовать static_cast? Дело в том, что без него компилятор выдаст предупреждение о возможной потере точности. Применение оператора static_cast говорит и компилятору, и человеку, читающему программу, что программист знает об этом.
Кроме того, с помощью static_cast указатель void* можно преобразовать в указатель определенного типа, арифметическое значение – в значение перечисления (enum), а базовый класс – в производный. (О преобразованиях типов базовых и производных классов говорится в главе 19.)
Эти изменения потенциально опасны, поскольку их правильность зависит от того, какое конкретное значение имеет преобразуемое выражение в данный момент выполнения программы:
enum mumble { first = 1, second, third };
extern int ival;
mumble mums_the_word = static_cast< mumble >( ival );
Трансформация ival в mumble будет правильной только в том случае, если ival равен 1, 2 или 3.
reinterpret_cast работает с внутренними представлениями объектов (re-interpret – другая интерпретация того же внутреннего представления), причем правильность этой операции целиком зависит от программиста. Например:
complex<double> *pcom;
char *pc = reinterpret_cast< char* >( pcom );
Программист не должен забыть или упустить из виду, какой объект реально адресуется указателем char* pc. Формально это указатель на строку встроенного типа, и компилятор не будет препятствовать использованию pc для инициализации строки:
string str( pc );
хотя скорее всего такая команда вызовет крах программы.
Это хороший пример, показывающий, насколько опасны бывают явные преобразования типов. Мы можем присваивать указателям одного типа значения указателей совсем другого типа, и это будет работать до тех пор, пока мы держим ситуацию под контролем. Однако, забыв о подразумеваемых деталях, легко допустить ошибку, о которой компилятор не сможет нас предупредить.
Особенно трудно найти подобную ошибку, если явное преобразование типа делается в одном файле, а используется измененное значение в другом.
В некотором смысле это отражает фундаментальный парадокс языка С++: строгая проверка типов призвана не допустить подобных ошибок, в то же время наличие операторов явного преобразования позволяет “обмануть” компилятор и использовать объекты разных типов на свой страх и риск. В нашем примере мы “отключили” проверку типов при инициализации указателя pc и присвоили ему адрес комплексного числа. При инициализации строки str такая проверка производится снова, но компилятор считает, что pc указывает на строку, хотя, на самом-то деле, это не так!
Четыре оператора явного преобразования типов были введены в стандарт С++ как наименьшее зло при невозможности полностью запретить такое приведение. Устаревшая, но до сих пор поддерживаемая стандартом С++ форма явного преобразования выглядит так:
char *pc = (char*) pcom;
Эта запись эквивалентна применению оператора reinterpret_cast, однако выглядит не так заметно. Использование операторов xxx_cast позволяет четко указать те места в программе, где содержатся потенциально опасные трансформации типов.
Если поведение программы становится ошибочным и непонятным, возможно, в этом виноваты явные видоизменения типов указателей. Использование операторов явного преобразования помогает легко обнаружить места в программе, где такие операции выполняются. (Другой причиной непредсказуемого поведения программы может стать нечаянное уничтожение объекта (delete), в то время как он еще должен использоваться в работе. Мы поговорим об этом в разделе 8.4, когда будем обсуждать динамическое выделение памяти.)
Оператор dynamic_cast применяется при идентификации типа во время выполнения (run-time type identification). Мы вернемся к этой проблеме лишь в разделе 19.1.
Явное задание аргументов шаблона *
В некоторых ситуациях автоматически вывести типы аргументов шаблона невозможно. Как мы видели на примере шаблона функции min5(), если процесс вывода дает два различных типа для одного и того же параметра шаблона, то компилятор сообщает об ошибке – неудачном выводе аргументов.
В таких ситуациях приходится подавлять механизм вывода и задавать аргументы явно, указывая их с помощью заключенного в угловые скобки списка разделенных запятыми значений, который следует после имени конкретизируемого шаблона функции. Например, если мы хотим задать тип unsigned int в качестве значения аргумента шаблона T в рассмотренном выше примере использования min5(), то нужно записать вызов конкретизируемого шаблона так:
// конкретизируется min5( unsigned int, unsigned int )
min5< unsigned int >( ui, 1024 );
В этом случае список аргументов шаблона <unsigned int> явно задает их типы. Поскольку аргумент шаблона теперь известен, вызов функции больше не приводит к ошибке.
Обратите внимание, что при вызове функции min5() второй аргумент равен 1024, т.е. имеет тип int. Так как тип второго формального параметра функции при явном задании аргумента шаблона установлен в unsigned int, то второй фактический параметр функции приводится к типу unsigned int с помощью стандартного преобразования целых типов.
В предыдущем разделе мы говорили, что в процессе вывода аргументов шаблона к фактическим аргументам функции разрешается применять только ограниченное множество преобразований типов. Трансформация int в unsigned int в это множество не входит. Но если аргументы шаблона задаются явно, выполнять вывод типов не нужно, поскольку они уже зафиксированы. Следовательно, при явном задании аргументов шаблона для приведения типов фактических аргументов функции к типам формальных параметров можно применять любые стандартные преобразования.
Помимо разрешения любых преобразований фактических аргументов функции, явное задание аргументов шаблона помогает избежать и других проблем, встающих перед программистом. Рассмотрим следующую задачу. Мы хотим определить шаблон функции с именем sum() так, чтобы его конкретизация возвращала значения типа, достаточно большого для представления суммы двух значений любых двух типов, переданных в любом порядке. Как это сделать? Какой тип возвращаемого значения следует задать?
// каким должен быть тип возвращаемого значения: T или U
template <class T, class U>
??? sum( T, U );
В нашем случае нельзя использовать ни тот, ни другой параметрический тип, иначе мы неизбежно допустим ошибку:
char ch; unsigned int ui;
// ни T, ни U нельзя использовать в качестве типа возвращаемого значения
sum( ch, ui ); // правильно: U sum( T, U );
sum( ui, ch ); // правильно: T sum( T, U );
Решение заключается в том, чтобы ввести в шаблон третий параметр для обозначения типа возвращаемого значения:
// T1 не появляется в списке параметров шаблона функции
template <class T1, class T2, class T3>
T1 sum( T2, T3 );
Поскольку тип возвращаемого значения может отличаться от типов аргументов функции, T1 не упоминается в списке формальных параметров. Это потенциальная проблема, так как тип T1 не может быть выведен из фактических аргументов функции. Однако, если при конкретизации sum() мы зададим аргументы шаблона явно, то избегнем сообщения компилятора о невозможности вывести T1. Например:
typedef unsigned int ui_type;
ui_type calc( char ch, ui_type ui ) {
// ...
// ошибка: невозможно вывести T1
ui_type loc1 = sum( ch, ui );
// правильно: аргументы шаблона заданы явно
// T1 и T3 - это unsigned int, T2 - это char
ui_type loc2 = sum< ui_type, ui_type >( ch, ui );
}
Не хватает возможности явно задать T1, но не T2 и T3, поскольку их можно вывести из аргументов функции при вызове.
При явном задании аргументов шаблона необходимо перечислять только те, которые не могут быть выведены автоматически. Но, как и в случае аргументов функции со значениями по умолчанию, опускать можно исключительно “хвостовые”:
// правильно: T3 - это unsigned int
// T3 выведен из типа ui
ui_type loc3 = sum< ui_type, char >( ch, ui );
// правильно: T2 - это char, T3 - unsigned int
// T2 и T3 выведены из типа pf
ui_type (*pf)( char, ui_type ) = &sum< ui_type >;
// ошибка: опускать можно только “хвостовые” аргументы
ui_type loc4 = sum< ui_type, , ui_type >( ch, ui );
Встречаются ситуации, когда невозможно вывести аргументы шаблона в контексте, где конкретизируется шаблон функции; следовательно, необходимо их явно задать. Именно выявление таких ситуаций и необходимость решить проблему послужила причиной поддержки явного задания аргументов шаблона в стандартном C++.
В следующем примере берется адрес конкретизированной функции sum() и передается в качестве аргумента перегруженной функции manipulate(). Как мы показали в разделе 10.2, невозможно понять, как именно нужно конкретизировать sum(), если есть только списки параметров функций manipulate(). Имеется две разных функции sum(), и обе удовлетворяют условиям вызова. Следовательно, вызов manipulate() неоднозначен. Одним из способов разрешения такой неоднозначности является явное приведение типов. Однако лучше использовать явное задание аргументов шаблона: оно позволяет указать, как именно конкретизировать sum(), и, следовательно, выбрать нужный вариант перегруженной функции manipulate(). Например:
template <class T1, class T2, class T3>
T1 sum( T2 op1, T3 op2 ) { /* ... */ }
void manipulate( int (*pf)( int,char ) );
void manipulate( double (*pf)( float,float ) );
int main()
{
// ошибка: какой из возможных экземпляров sum:
// int sum( int,char ) или double sum( float, float )?
manipulate( &sum );
// берется адрес конкретизированного экземпляра
// double sum( float, float )
// вызывается: void manipulate( double (*pf)( float, float ) );
manipulate( &sum< double, float, float > );
}
Отметим, что явное задание аргументов шаблона следует использовать только тогда, когда это абсолютно необходимо для разрешения неоднозначности или для конкретизации шаблона функции в контексте, где вывести аргументы невозможно. Во-первых, определение типов и значений аргументов шаблона проще оставить компилятору. А во-вторых, если мы модифицируем объявления в программе, так что типы аргументов функции при вызове конкретизированного шаблона изменятся, то компилятор автоматически скорректирует вызов без нашего вмешательства. С другой стороны, если аргументы шаблона заданы явно, необходимо проверить, что они по-прежнему отвечают новым типам аргументов функции. Поэтому мы рекомендуем избегать явного задания аргументов шаблона.
Упражнение 10.6
Назовите две ситуации, когда использование явного задания аргументов шаблона необходимо.
Упражнение 10.7
Пусть дано следующее определение шаблона функции sum():
template <class T1, class T2, class T3>
T1 sum( T2, T3 );
Какие из приведенных ниже вызовов ошибочны? Почему?
double dobj1, dobj2;
float fobj1, fobj2;
char cobj1, cobj2;
(a) sum( dobj1, dobj2 );
(b) sum<double,double,double>( fobj1, fobj2 );
(c) sum<int>( cobj1, cobj2 );
(d) sum<double, ,double>( fobj2, dobj2 );
Явные объявления конкретизации
При использовании модели с включением определение шаблона функций включается в каждый исходный файл, где встречается конкретизация этого шаблона. Мы отмечали, что, хотя неизвестно, где и когда понадобится шаблон функции, программа должна вести себя так, как будто экземпляр шаблона для данного множества аргументов конкретизирован ровно один раз. В действительности некоторые компиляторы (особенно старые) конкретизируют шаблон функции с данным множеством аргументов шаблона неоднократно. В рамках этой модели для использования на этапе сборки или на одной из предшествующих ей стадий выбирается один из конкретизированных экземпляров, а остальные игнорируются.
Результат работы программы не зависит от того, сколько раз конкретизировался шаблон: в конечном итоге используется лишь один экземпляр. Но если приложение состоит из большого числа файлов, то время компиляции приложения заметно возрастает.
Подобные проблемы, характерные для старых компиляторов, затрудняли использование шаблонов. Поэтому в стандарте C++ введено понятие явного объявления конкретизации, помогающее программисту управлять моментом, когда конкретизация происходит.
В явном объявлении конкретизации за ключевым словом template идет объявление шаблона функции, в котором его аргументы указаны явно. Рассмотрим шаблон sum(int*, int):
template <typename Type>
Type sum( Type op1, Type op2 ) { /* ... */ }
// явное объявление конкретизации
template int* sum< int* >( int*, int );
Здесь в качестве аргумента явно задается int*. Явное объявление конкретизации с одним и тем же множеством аргументов шаблона может встречаться в программе не более одного раза.
Определение шаблона функции должно находиться в том же файле, где и явное объявление конкретизации. Если же его не видно, то явное объявление приводит к ошибке:
#include <vector>
template <typename Type>
Type sum( Type op1, int op2 ); // только объявление
// определяем typedef для vector< int >
typedef vector< int > VI;
// ошибка: sum() не определен
template VI sum< VI >( VI , int );
Если в некотором исходном файле встречается явное объявление конкретизации, то что произойдет в других файлах, где используется такая же конкретизация шаблона функции? Как сказать компилятору, что явное объявление находится в другом файле и что при использовании в этом файле шаблон конкретизировать не надо?
Явные объявления конкретизации используются в сочетании с опцией компилятора, которая подавляет неявную конкретизацию шаблонов. Название опции в разных компиляторах различно. Например, в VisualAge for C++ для Windows версии 3.5 фирмы IBM эта опция называется /ft-. Если приложение компилируется с данной опцией, то компилятор предполагает, что шаблоны будут конкретизироваться явно, и не выполняет автоматической конкретизации.
Разумеется, если мы не включили в программу явного объявления конкретизации для некоторого шаблона, но задали опцию /ft-, то при сборке произойдет ошибка из-за того, что функция не была конкретизирована.
Упражнение 10.8
Назовите две модели компиляции шаблонов, поддерживаемые в C++. Объясните, как организуются определения шаблонов функций в каждой модели.
Упражнение 10.9
Пусть дано следующее определение шаблона функции sum():
template <typename Type>
Type sum( Type op1, char op2 );
Как записать явное объявление конкретизации этого шаблона с аргументом типа string?
Явный вызов деструктора
Иногда вызывать деструктор для некоторого объекта приходится явно. Особенно часто такая необходимость возникает в связи с оператором new (см. раздел 8.4). Рассмотрим пример. Когда мы пишем:
char *arena = new char[ sizeof Image ];
то из хипа выделяется память, размер которой равен размеру объекта типа Image, она не инициализирована и заполнена случайными битами. Если же написать:
Image *ptr = new (arena) Image( "Quasimodo" );
то никакой новой памяти не выделяется. Вместо этого переменной ptr присваивается адрес, ассоциированный с переменной arena. Теперь память, на которую указывает ptr, интерпретируется как занимаемая объектом класса Image, и конструктор применяется к уже существующей области. Таким образом, оператор размещения new() позволяет сконструировать объект в ранее выделенной области памяти.
Закончив работать с изображением Quasimodo, мы можем произвести какие-то операции с изображением Esmerelda, размещенным по тому же адресу arena в памяти:
Image *ptr = new (arena) Image( "Esmerelda" );
Однако изображение Quasimodo при этом будет затерто, а мы его модифицировали и хотели бы записать на диск. Обычно сохранение выполняется в деструкторе класса Image, но если мы применим оператор delete:
// плохо: не только вызывает деструктор, но и освобождает память
delete ptr;
то, помимо вызова деструктора, еще и возвратим в хип память, чего делать не следовало бы. Вместо этого можно явно вызвать деструктор класса Image:
ptr->~Image();
сохранив отведенную под изображение память для последующего вызова оператора размещения new.
Отметим, что, хотя ptr и arena адресуют одну и ту же область памяти в хипе, применение оператора delete к arena
// деструктор не вызывается
delete arena;
не приводит к вызову деструктора класса Image, так как arena имеет тип char*, а компилятор вызывает деструктор только тогда, когда операндом в delete является указатель на объект класса, имеющего деструктор.
Как определить последовательный контейнер?
Для того чтобы определить объект контейнерного типа, необходимо сначала включить соответствующий заголовочный файл:
#include <vector>
#inclnde <list>
#include <deque>
#include <map>
#include <set>
Определение контейнера начинается именем его типа, за которым в угловых скобках следует тип данных его элементов[12]. Например:
vector< string > svec;
list< int > ilist;
Переменная svec определяется как вектор, способный содержать элементы типа string, а ilist – как список с элементами типа int. Оба контейнера при таком определении пусты. Чтобы убедиться в этом, можно вызвать функцию-член empty():
if ( svec.empty() != true )
; // что-то не так
Простейший метод вставки элементов – использование функции-члена push_back(), которая добавляет элементы в конец контейнера. Например:
string text_word;
while ( cin >> text_word )
svec.push_back( text_word );
Здесь строки из стандартного ввода считываются в переменную text_word, и затем копия каждой строки добавляется в контейнер svec с помощью push_back().
Список имеет функцию-член push_front(), которая добавляет элемент в его начало. Пусть есть следующий массив:
int ia[ 4 ] = { 0, 1, 2, 3 };
Использование push_back()
for ( int ix=0; ix<4; ++ix )
ilist.push_back( ia[ ix ] );
создаст последовательность 0, 1, 2, 3, а push_front()
for ( int ix=0; ix<4; ++ix )
ilist.push_front( ia[ ix ] );
создаст последовательность 3, 2, 1, 0.[13]
Мы можем при создании явно указать размер массива – как константным, так и неконстантным выражением:
#include <list>
#include <vector>
#include <string>
extern int get_word_count( string file_name );
const int list_size = 64;
list< int > ilist( list_size );
vector< string > svec(get_word_count(string("Chimera")));
Каждый элемент контейнера инициализируется значением по умолчанию, соответствующим типу данных. Для int это 0. Для строкового типа вызывается конструктор по умолчанию класса string.
Мы можем указать начальное значение всех элементов:
list< int > ilist( list_size, -1 );
vector< string > svec( 24, "pooh" );
Разрешается не только задавать начальный размер контейнера, но и впоследствии изменять его с помощью функции-члена resize(). Например:
svec.resize( 2 * svec.size() );
Размер svec в этом примере удваивается. Каждый новый элемент получает значение по умолчанию. Если мы хотим инициализировать его каким-то другим значением, то оно указывается вторым параметром функции-члена resize():
// каждый новый элемент получает значение "piglet"
svec.resize( 2 * svec.size(), "piglet" );
Кстати, какова наиболее вероятная емкость svec при определении, если его начальный размер равен 24? Правильно, 24! В общем случае минимальная емкость вектора равна его текущему размеру. При удвоении размера емкость, как правило, тоже удваивается
Мы можем инициализировать новый контейнер с помощью существующего. Например:
vector< string > svec2( svec );
list< int > ilist2( ilist ) ;
Каждый контейнер поддерживает полный набор операций сравнения: равенство, неравенство, меньше, больше, меньше или равно, больше или равно. Сопоставляются попарно все элементы контейнера. Если они равны и размеры контейнеров одинаковы, то эти контейнеры равны; в противном случае – не равны. Результат операций “больше” или “меньше” определяется сравнением первых двух неравных элементов. Вот что печатает программа, сравнивающая пять векторов:
ivecl: 1 3 5 7 9 12
ivec2: 0 1 1 2 3 5 8 13
ivec3: 1 3 9
ivec4: 1 3 5 7
ivec5: 2 4
// первый неравный элемент: 1, О
// ivecl больше чем ivec2
ivecl < ivec2 //false
ivec2 < ivecl //true
// первый неравный элемент: 5, 9
ivecl < ivec3 //true
// все элементы равны, но ivec4 содержит меньше элементов
// следовательно, ivec4 меньше, чем ivecl
ivecl < ivec4 //false
// первый неравный элемент: 1, 2
ivecl < ivec5 //true
ivecl == ivecl //true
ivecl == ivec4 //false
ivecl != ivec4 //true
ivecl > ivec2 //true
ivec3 > ivecl //true
ivec5 > ivec2 //true
Существуют три ограничения на тип элементов контейнера (практически это касается только пользовательских классов). Для должны быть определены:
операция “равно”;
операция “меньше” (все операции сравнения контейнеров, о которых говорилось выше, используют только эти две операции сравнения);
значение по умолчанию (для класса это означает наличие конструктора по умолчанию).
Все предопределенные типы данных, включая указатели и классы из стандартной библиотеки С++ удовлетворяют этим требованиям.
Упражнение 6.5
Объясните, что делает данная программа:
#include <string>
#include <vector>
#include <iostream>
#int main()
{
vector<string> svec;
svec.reserve( 1024 );
string text_word;
while ( cin >> text_word )
svec.push_back( text_word );
svec.resize( svec.size()+svec.size()/2 );
// ...
}
Упражнение 6.6
Может ли емкость контейнера быть меньше его размера? Желательно ли, чтобы емкость была равна размеру: изначально или после вставки элемента? Почему?
Упражнение 6.7
Если программа из упражнения 6.5 прочитает 256 слов, то какова наиболее вероятная емкость контейнера после изменения размера? А если она считает 512 слов? 1000? 1048?
Упражнение 6.8
Какие из данных классов не могут храниться в векторе:
(a) class cl1 {
public:
c11( int=0 );
bool operator==();
bool operator!=();
bool operator<=();
bool operator<();
// ...
};
(b) class c12 {
public:
c12( int=0 );
bool operator!=();
bool operator<=();
// ...
};
(с) class c13 {
public:
int ival;
};
(d) class c14 {
public:
c14( int, int=0 );
bool operator==();
bool operator!=();
// ...
}
Как перегрузить имя функции
В C++ двум или более функциям может быть дано одно и то же имя при условии, что их списки параметров различаются либо числом параметров, либо их типами. В данном примере мы объявляем перегруженную функцию max():
int max ( int, int );
int max( const vector<int> & );
int max( const matrix & );
Для каждого перегруженного объявления требуется отдельное определение функции max() с соответствующим списком параметров.
Если в некоторой области видимости имя функции объявлено более одного раза, то второе (и последующие) объявление интерпретируется компилятором так:
если списки параметров двух функций отличаются числом или типами параметров, то функции считаются перегруженными:
// перегруженные функции
void print( const string & );
void print( vector<int> & );
если тип возвращаемого значения и списки параметров в объявлениях двух функций одинаковы, то второе объявление считается повторным:
// объявления одной и той же функции
void print( const string &str );
void print( const string & );
Имена параметров при сравнении объявлений во внимание не принимаются;
если списки параметров двух функций одинаковы, но типы возвращаемых значений различны, то второе объявление считается неправильным (несогласованным с первым) и помечается компилятором как ошибка:
unsigned int max( int i1, int i2 );
int max( int i1, int i2 ); // ошибка: отличаются только типы
// возвращаемых значений
Перегруженные функции не могут различаться лишь типами возвращаемого значения;
если списки параметров двух функций разнятся только подразумеваемыми по умолчанию значениями аргументов, то второе объявление считается повторным:
// объявления одной и той же функции
int max ( int *ia, int sz );
int max ( int *ia, int = 10 );
Ключевое слово typedef создает альтернативное имя для существующего типа данных, новый тип при этом не создается. Поэтому если списки параметров двух функций различаются только тем, что в одном используется typedef, а в другом тип, для которого typedef служит псевдонимом, такие списки считаются одинаковыми, как, например, в следующих двух объявлениях функции calc(). В таком случае второе объявление даст ошибку компиляции, поскольку возвращаемое значение отличается от указанного раньше:
// typedef не вводит нового типа
typedef double DOLLAR;
// ошибка: одинаковые списки параметров, но разные типы
// возвращаемых значений
extern DOLLAR calc( DOLLAR );
extern int calc( double );
Спецификаторы const или volatile при подобном сравнении не принимаются во внимание. Так, следующие два объявления считаются одинаковыми:
// объявляют одну и ту же функцию
void f( int );
void f( const int );
Спецификатор const важен только внутри определения функции: он показывает, что в теле функции запрещено изменять значение параметра. Однако аргумент, передаваемый по значению, можно использовать в теле функции как обычную инициированную переменную: вне функции изменения не видны. (Способы передачи аргументов, в частности передача по значению, обсуждаются в разделе 7.3.) Добавление спецификатора const к параметру, передаваемому по значению, не влияет на его интерпретацию. Функции, объявленной как f(int), может быть передано любое значение типа int, равно как и функции f(const int). Поскольку они обе принимают одно и то же множество значений аргумента, то приведенные объявления не считаются перегруженными. f() можно определить как
void f( int i ) { }
или как
void f( const int i ) { }
Наличие двух этих определений в одной программе – ошибка, так как одна и та же функция определяется дважды.
Однако, если спецификатор const или volatile применяется к параметру указательного или ссылочного типа, то при сравнении объявлений он учитывается.
// объявляются разные функции
void f( int* );
void f( const int* );
// и здесь объявляются разные функции
void f( int& );
void f( const int& );
Как растет вектор?
Вектор может расти динамически. Как это происходит? Он должен выделить область памяти, достаточную для хранения всех элементов, скопировать в эту область все старые элементы и освободить ту память, в которой они содержались раньше. Если при этом элементы вектора являются объектами класса, то для каждого из них при таком копировании вызываются конструктор и деструктор. Поскольку у списка нет необходимости в таких дополнительных действиях при добавлении новых элементов, кажется очевидным, что ему проще поддерживать динамический рост контейнера. Однако на практике это не так. Давайте посмотрим почему.
Вектор может запрашивать память не под каждый новый элемент. Вместо этого она запрашивается с некоторым запасом, так что после очередного выделения вектор может поместить в себя некоторое количество элементов, не обращаясь за ней снова. (Каков размер этого запаса, зависит от реализации.) На практике такое свойство вектора обеспечивает значительное увеличение его эффективности, особенно для небольших объектов. Давайте рассмотрим некоторые примеры из реализации стандартной библиотеки С++ от компании Rogue Wave. Однако сначала определим разницу между размером и емкостью контейнера.
Емкость– это максимальное количество элементов, которое может вместить контейнер без дополнительного выделения памяти. (Емкостью обладают только те контейнеры, в которых элементы хранятся в непрерывной области памяти, – vector, deque и string. Для контейнера list это понятие не определено.) Емкость может быть получена с помощью функции capacity(). Размер – это реальное количество элементов, хранящихся в данный момент в контейнере. Размер можно получить с помощью функции size(). Например:
#include <vector>
#include <iostream>
int main()
{
vector< int > ivec;
cout << "ivec: размер: " << ivec.size()
<< " емкость: " << ivec.capacity() << endl;
for ( int ix = 0; -ix < 24; ++ix ) {
ivec.push_back( ix );
cout << "ivec: размер: " << ivec.size()
<< " емкость: " << ivec.capacity() << endl;
}
}
В реализации Rogue Wave и размер, и емкость ivec сразу после определения равны 0. После вставки первого элемента размер становится равным 1, а емкость – 256. Это значит, что до первого дополнительного выделения памяти в ivec можно вставить 256 элементов. При добавлении 256-го элемента вектор должен увеличиться: выделить память объемом в два раза больше текущей емкости, скопировать в нее старые элементы и освободить прежнюю память. Обратите внимание: чем больше и сложнее тип данных элементов, тем менее эффективен вектор в сравнении со списком. В таблице 6.1 показана зависимость начальной емкости вектора от используемого типа данных.
Таблица 6.1. Размер и емкость для различных типов данных
Тип данных |
Размер в байтах |
Емкость после первой вставки |
int |
4 |
256 |
double |
8 |
128 |
простой класс #1 |
12 |
85 |
string |
12 |
85 |
большой простой класс |
8000 |
1 |
большой сложный класс |
8000 |
1 |
Таблица 6.2. Время в секундах для вставки 10 000 000 элементов
Тип данных |
List |
Vector |
int |
10.38 |
3.76 |
double |
10.72 |
3.95 |
простой класс |
12.31 |
5.89 |
string |
14.42 |
11.80 |
Таблица 6.3. Время в секундах для вставки 10 000 элементов
Тип данных |
List |
Vector |
большой простой класс |
0.36 |
2.23 |
большой сложный класс |
2.37 |
6.70 |
Вставка элемента как в список, так и в вектор, требует вызова копирующего конструктора, если он определен. (Копирующий конструктор инициализирует один объект значением другого. В разделе 2.2 приводится начальная информация, а в разделе 14.5 о таких конструкторах рассказывается подробно). Это и объясняет различие в поведении простых и сложных объектов при вставке в контейнер. Объекты простого класса вставляются побитовым копированием (биты одного объекта пересылаются в биты другого), а для строк и сложных классов это производится вызовом копирующего конструктора.
Вектор должен вызывать их для каждого элемента при перераспределении памяти. Более того, освобождение памяти требует работы деструкторов для всех элементов (понятие деструктора вводится в разделе 2.2). Чем чаще происходит перераспределение памяти, тем больше времени тратится на эти дополнительные вызовы конструкторов и деструкторов.
Конечно, одним из решений может быть переход от вектора к списку, когда эффективность вектора становится слишком низкой. Другое, более предпочтительное решение состоит в том, чтобы хранить в векторе не объекты сложного класса, а указатели на них. Такая замена позволяет уменьшить затраты времени на 10 000 вставок с 6.70 секунд до 0.82 секунды. Почему? Емкость возросла с 1 до 256, что существенно снизило частоту перераспределения памяти. Кроме того, копирующий конструктор и деструктор не вызываются больше для каждого элемента при копировании прежнего содержимого вектора.
Функция reserve() позволяет программисту явно задать емкость контейнера[11]. Например:
int main() {
vector< string > svec;
svec.reserve( 32 ); // задает емкость равной 32
// ...
}
svec получает емкость 32 при размере 0. Однако эксперименты показали, что любое изменение начальной емкости для вектора, у которого она по умолчанию отлична от 1, ведет к снижению производительности. Так, для векторов типа string и double увеличение емкости с помощью reserve() дало худшие показатели. С другой стороны, увеличение емкости для больших сложных типов дает значительный рост производительности, как показано в таблице 6.4.
Таблица 6.4. Время в секундах для вставки 10 000 элементов при различной емкости*
Емкость |
Время в секундах |
1 по умолчанию |
670 |
4,096 |
555 |
8,192 |
444 |
10,000 |
222 |
*Сложный класс размером 8000 байт с конструктором копирования и деструктором |
Упражнение 6.2
Объясните разницу между размером и емкостью контейнера. Почему понятие емкости необходимо для контейнера, содержащего элементы в непрерывной области памяти, и не нужно для списка?
Упражнение 6.3
Почему большие сложные объекты удобнее хранить в контейнере в виде указателей на них, а для коллекции целых чисел применение указателей снижает эффективность?
Упражнение 6.4
Объясните, какой из типов контейнера – вектор или список – больше подходит для приведенных примеров (во всех случаях происходит вставка неизвестного заранее числа элементов):.
(a) Целые числа
(b) Указатели на большие сложные объекты
(c) Большие сложные объекты
Класс bitset
Таблица 4.4. Операции с классом bitset
Операция | Значение | Использование | |||
test(pos) | Бит pos равен 1? | a.test(4) | |||
any() | Хотя бы один бит равен 1? | a.any() | |||
none() | Ни один бит не равен 1? | a.none() | |||
count() | Количество битов, равных 1 | a.count() | |||
size() | Общее количество битов | a.size() | |||
[pos] | Доступ к биту pos | a[4] | |||
flip() | Изменить значения всех | a.flip() | |||
flip(pos) | Изменить значение бита pos | a.flip(4) | |||
set() | Выставить все биты в 1 | a.set() | |||
set(pos) | Выставить бит pos в 1 | a.set(4) | |||
reset() | Выставить все биты в 0 | a.reset() | |||
reset(pos) | Выставить бит pos в 0 | a.reset(4) |
Как мы уже говорили, необходимость создавать сложные выражения для манипуляции битовыми векторами затрудняет использование встроенных типов данных. Класс bitset упрощает работу с битовым вектором. Вот какое выражение нам приходилось писать в предыдущем разделе для того, чтобы “взвести” 27-й бит:
quiz1 |= 1<<27;
При использовании bitset то же самое мы можем сделать двумя способами:
quiz1[27] = 1;
или
quiz1.set(27);
(В нашем примере мы не используем нулевой бит, чтобы сохранить “естественную” нумерацию. На самом деле, нумерация битов начинается с 0.)
Для использования класса bitset необходимо включить заголовочный файл:
#include <bitset>
Объект типа bitset может быть объявлен тремя способами. В определении по умолчанию мы просто указываем размер битового вектора:
bitset<32> bitvec;
Это определение задает объект bitset, содержащий 32 бита с номерами от 0 до 31. Все биты инициализируются нулем. С помощью функции any() можно проверить, есть ли в векторе единичные биты. Эта функция возвращает true, если хотя бы один бит отличен от нуля. Например:
bool is_set = bitvec.any();
Переменная is_set получит значение false, так как объект bitset по умолчанию инициализируется нулями. Парная функция none() возвращает true, если все биты равны нулю:
bool is_not_set = bitvec.none();
Изменить значение отдельного бита можно двумя способами: воспользовавшись функциями set() и reset() или индексом. Так, следующий цикл выставляет в 1 каждый четный бит:
for ( int index=0; index<32; ++index )
if ( index % 2 == 0 )
bitvec[ index ] = 1;
Аналогично существует два способа проверки значений каждого бита– с помощью функции test() и с помощью индекса. Функция () возвращает true, если соответствующий бит равен 1, и false в противном случае. Например:
if ( bitvec.test( 0 ))
// присваивание bitvec[0]=1 сработало!;
Значения битов с помощью индекса проверяются таким образом:
cout << "bitvec: включенные биты:\n\t";
for ( int index = 0; index < 32; ++-index )
if ( bitvec[ index ] )
cout << index << " ";
cout << endl;
Следующая пара операторов демонстрирует сброс первого бита двумя способами:
bitvec.reset(0);
bitvec[0] = 0;
Функции set() и reset() могут применяться ко всему битовому вектору в целом. В этом случае они должны быть вызваны без параметра. Например:
// сброс всех битов
bitvec.reset();
if (bitvec.none() != true)
// что-то не сработало
// установить в 1 все биты вектора bitvec
if ( bitvec.any() != true )
// что-то опять не сработало
Функция flip() меняет значение отдельного бита или всего битового вектора:
bitvec.f1ip( 0 ); // меняет значение первого бита
bitvec[0].flip(); // тоже меняет значение первого бита
bitvec.flip(); // меняет значения всех битов
Существуют еще два способа определить объект типа bitset. Оба они дают возможность проинициализировать объект определенным набором нулей и единиц. Первый способ – явно задать целое беззнаковое число как аргумент конструктору. Начальные N позиций битового вектора получат значения соответствующих двоичных разрядов аргумента. Например:
bitset< 32 > bitvec2( Oxffff );
инициализирует bitvec2 следующим набором значений:
00000000000000001111111111111111
В результате определения
bitset< 32 > bitvec3( 012 );
у bitvec3 окажутся ненулевыми биты на местах 1 и 3:
00000000000000000000000000001010
В качестве аргумента конструктору может быть передано и строковое значение, состоящее из нулей и единиц. Например, следующее определение инициализирует bitvec4 тем же набором значений, что и bitvec3:
// эквивалентно bitvec3
string bitva1( "1010" );
bitset< 32 > bitvec4( bitval );
Можно также указать диапазон символов строки, выступающих как начальные значения для битового вектора. Например:
// подстрока с шестой позиции длиной 4: 1010
string bitval ( "1111110101100011010101" );
bitset< 32 > bitvec5( bitval, 6, 4 );
Мы получаем то же значение, что и для bitvec3 и bitvec4. Если опустить третий параметр, подстрока берется до конца исходной строки:
// подстрока с шестой позиции до конца строки: 1010101
string bitva1( "1111110101100011010101" );
bitset< 32 > bitvec6( bitval, 6 );
Класс bitset предоставляет две функции-члена для преобразования объекта bitset в другой тип. Для трансформации в строку, состоящую из символов нулей и единиц, служит функция to_string():
string bitva1( bitvec3.to_string() );
Вторая функция, to_long(), преобразует битовый вектор в его целочисленное представление в виде unsigned long, если, конечно, оно помещается в unsigned long. Это видоизменение особенно полезно, если мы хотим передать битовый вектор функции на С или С++, не пользующейся стандартной библиотекой.
К объектам типа bitset можно применять побитовые операции. Например:
bitset<32> bitvec7 = bitvec2 & bitvec3;
Объект bitvec7 инициализируется результатом побитового И двух битовых векторов bitvec2 и bitvec3.
bitset<32> bitvec8 = bitvec2 | bitvec3;
Здесь bitvec8 инициализируется результатом побитового ИЛИ векторов bitvec2 и bitvec3. Точно так же поддерживаются и составные операции присваивания и сдвига.
Упражнение 4.15
Допущены ли ошибки в приведенных определениях битовых векторов?
(a) bitset<64> bitvec(32);
(b) bitset<32> bv( 1010101 );
(c) string bstr; cin >> bstr; bitset<8>bv( bstr );
(d) bitset<32> bv; bitset<16> bvl6( bv );
Упражнение 4.16
Допущены ли ошибки в следующих операциях с битовыми векторами?
extern void bitstring(const char*);
bool bit_on (unsigned long, int);
bitset<32> bitvec;
(a) bitsting( bitvec.to_string().c_str() );
(b) if ( bit_on( bitvec.to_1ong(), 64 )) ...
(c) bitvec.f1ip( bitvec.count() );
Упражнение 4.17
Дана последовательность: 1,2,3,5,8,13,21. Каким образом можно инициализировать объект bitset<32> для ее представления? Как присвоить значения для представления этой последовательности пустому битовому вектору? Напишите вариант инициализации и вариант с присваиванием значения каждому биту.
Класс complex
Класс комплексных чисел complex – еще один класс из стандартной библиотеки. Как обычно, для его использования нужно включить заголовочный файл:
#include <comp1ex>
Комплексное число состоит из двух частей – вещественной и мнимой. Мнимая часть представляет собой квадратный корень из отрицательного числа. Комплексное число принято записывать в виде
2 + 3i
где 2 – действительная часть, а 3i – мнимая. Вот примеры определений объектов типа complex:
// чисто мнимое число: 0 + 7-i
comp1ex< double > purei( 0, 7 );
// мнимая часть равна 0: 3 + Oi
comp1ex< float > rea1_num( 3 );
// и вещественная, и мнимая часть равны 0: 0 + 0-i
comp1ex< long double > zero;
// инициализация одного комплексного числа другим
comp1ex< double > purei2( purei );
Поскольку complex, как и vector, является шаблоном, мы можем конкретизировать его типами float, double и long double, как в приведенных примерах. Можно также определить массив элементов типа complex:
complex< double > conjugate[ 2 ] = {
complex< double >( 2, 3 ),
complex< double >( 2, -3 )
};
Вот как определяются указатель и ссылка на комплексное число:
complex< double > *ptr = &conjugate[0];
complex< double > &ref = *ptr;
Комплексные числа можно складывать, вычитать, умножать, делить, сравнивать, получать значения вещественной и мнимой части. (Более подробно мы будем говорить о классе complex в разделе 4.6.)
Класс для обработки параметров командной строки
Чтобы не перегружать функцию main() деталями, касающимися обработки параметров командной строки, лучше отделить этот фрагмент. Можно написать для этого функцию. Например:
extern int parse_options( int arg_count, char *arg_vector );
int main( int argc, char *argv[] ) {
// ...
int option_status;
option_status = parse_options( argc, argv );
// ...
}
Как вернуть несколько значений? Обычно для этого используются глобальные объекты, которые не передаются ни в функцию для их обработки, ни обратно. Альтернативной стратегией является инкапсуляция обработки параметров командной строки в класс.
Данные-члены класса представляют собой параметры, заданные пользователем в командной строке. Набор открытых встроенных функций-членов позволяет получать их значения. Конструктор инициализирует параметры значениями по умолчанию. Функция-член получает argc и argv в качестве аргументов и обрабатывает их:
#include <vector>
#include <string>
class CommandOpt {
public:
CommandOpt() : _limit( -1 ), _debug_on( false ) {}
int parse_options( int argc, char *argv[] );
string out_file() { return _out_file; }
bool debug_on() { return _debug_on; }
int files() { return _file_names.size(); }
string& operator[]( int ix );
private:
inline void usage( int exit_value = 0 );
bool _debug_on;
int _limit;
string _out_file;
vector<string> _file_names;
static const char *const program_name;
static const char *const program_version;
};
Так выглядит модифицированная функция main()[18]:
#include "CommandOpt.h"
int main( int argc, char *argv[] ) {
// ...
CommandOpt com_opt;
int option_status;
opttion_status = com_opt. parse_options (argc, argv);
// ...
}
Упражнение 7.15
Добавьте обработку опций -t (включение таймера) и -b (задание размера буфера bufsize). Не забудьте обновить usage(). Например:
prog -t -b 512 dataO
Упражнение 7.16
Наша реализация не обрабатывает случая, когда между опцией и ассоциированным с ней значением нет пробела. Модифицируйте программу для поддержки такой обработки.
Упражнение 7.17
Наша реализация не может различить лишний пробел между дефисом и опцией:
prog - d dataO
Модифицируйте программу так, чтобы она распознавала подобную ошибку и сообщала о ней.
Упражнение 7.18
В нашей программе не предусмотрен случай, когда опции -l или -o задаются несколько раз. Реализуйте такую возможность. Какова должна быть стратегия при разрешении конфликта?
Упражнение 7.19
В нашей реализации задание неизвестной опции приводит к фатальной ошибке. Как вы думаете, это оправдано? Предложите другое поведение.
Упражнение 7.20
Добавьте поддержку опций, начинающихся со знака плюс (+), обеспечив обработку +s и +pt, а также +sp и +ps. Предположим, что +s включает строгую проверку синтаксиса, а +p допускает использование устаревших конструкций. Например:
prog +s +p -d -b 1024 dataO
Класс массива с множественным наследованием
Определим отсортированный массив с контролем выхода за границы. Для этого можно применить множественное наследование от Array_RC и Array_Sort. Вот как выглядит наша реализация (напомним еще раз, что мы ограничились тремя конструкторами и оператором взятия индекса). Определение находится в заголовочном файле Array_RC_S.h:
#ifndef ARRAY_RC_S_H
#define ARRAY_RC_S_H
#include "Array_S.C"
#include "Array_RC.C"
template <class Type>
class Array_RC_S : public Array_RC<Type>,
public Array_Sort<Type>
{
public:
Array_RC_S( int sz = Array<Type>::ArraySize )
: Array<Type>( sz )
{ clear_bit(); }
Array_RC_S( const Array_RC_S &rca )
: Array<Type>( rca )
{ sort( 0,Array<Type>::_size-1 ); clear_bit(); }
Array_RC_S( const Type* arr, int sz )
: Array<Type>( arr, sz )
{ sort( 0,Array<Type>::_size-1 ); clear_bit(); }
Type& operator[]( int index )
{
set_bit();
return Array_RC<Type>::operator[]( index );
}
};
#endif
Этот класс наследует две реализации каждой интерфейсной функции Array: из Array_Sort и из виртуального базового класса Array через Array_RC (за исключением оператора взятия индекса, для которого из обоих базовых классов наследуется замещенный экземпляр). При невиртуальном наследовании вызов find() был бы помечен компилятором как неоднозначный, поскольку он не знает, какой из унаследованных экземпляров мы имели в виду. В нашем случае замещенным в Array_Sort экземплярам отдается предпочтение по сравнению с экземплярами, унаследованными из виртуального базового класса через Array_RC (см. раздел 18.5.4). Таким образом, при виртуальном наследовании неквалифицированный вызов find() разрешается в пользу экземпляра, унаследованного из класса Array_Sort.
Оператор взятия индекса переопределен в классах Array_RC и Array_Sort, и обе реализации имеют равный приоритет. Поэтому внутри Array_RC_S неквалифицированное обращение к оператору взятия индекса неоднозначно. Класс Array_RC_S должен предоставить собственную реализацию, иначе пользователи не смогут напрямую применять такой оператор к объектам этого класса. Но какова семантика его вызова в Array_RC_S? При учете отсортированности массива он должен установить в true унаследованный член dirty_bit. А чтобы учесть наследование от класса с контролем выхода за границы массива – проверить указанный индекс. После этого можно возвращать элемент массива с данным индексом. Последние два шага выполняет унаследованный из Array_RC оператор взятия индекса. При обращении
Представленная в этой главе реализация иерархии класса Array иллюстрирует применение множественного и виртуального наследования. Детально проектирование класса массива описано в [NACKMAN94]. Однако, как правило, достаточно класса vector из стандартной библиотеки.
Упражнение 18.16
Добавьте в Array функцию-член spy(). Она запоминает операции, примененные к объекту класса: число доступов по индексу; количество вызовов каждого члена; какой элемент искали с помощью find() и сколько было успешных поисков. Поясните свои проектные решения. Модифицируйте все подтипы Array так, чтобы spy() можно было использовать и для них тоже.
Упражнение 18.17
Стандартный библиотечный класс map (отображение) называют еще ассоциативным массивом, поскольку он поддерживает индексирование значением ключа. Как вы думаете, является ли ассоциативный массив кандидатом на роль подтипа нашего класса Array? Почему?
Упражнение 18.18
Перепишите иерархию Array, пользуясь контейнерными классами из стандартной библиотеки и применяя обобщенные алгоритмы.
Класс pair
Класс pair (пара) стандартной библиотеки С++ позволяет нам определить одним объектом пару значений, если между ними есть какая-либо семантическая связь. Эти значения могут быть одинакового или разного типа. Для использования данного класса необходимо включить заголовочный файл:
#inc1ude <uti1ity>
Например, инструкция
pair< string, string > author( "James", "Joyce" );
создает объект author типа pair, состоящий из двух строковых значений.
Отдельные части пары могут быть получены с помощью членов first и second:
string firstBook;
if ( Joyce.first == "James" &&
Joyce.second == "Joyce" )
firstBook = "Stephen Hero";
Если нужно определить несколько однотипных объектов этого класса, удобно использовать директиву typedef:
typedef pair< string, string > Authors;
Authors proust( "marcel", "proust" );
Authors joyce( "James", "Joyce" );
Authors musil( "robert", "musi1" );
Вот другой пример употребления пары. Первое значение содержит имя некоторого объекта, второе – указатель на соответствующий этому объекту элемент таблицы.
class EntrySlot;
extern EntrySlot* 1ook_up( string );
typedef pair< string, EntrySlot* > SymbolEntry;
SymbolEntry current_entry( "author", 1ook_up( "author" ));
// ...
if ( EntrySlot *it = 1ook_up( "editor" ))
{
current_entry.first = "editor";
current_entry.second = it;
}
(Мы вернемся к рассмотрению класса pair в разговоре о контейнерных типах в главе 6 и об обобщенных алгоритмах в главе 12.)
Класс string
Как мы только что видели, применение встроенного строкового типа чревато ошибками и не очень удобно из-за того, что он реализован на слишком низком уровне. Поэтому достаточно распространена разработка собственного класса или классов для представления строкового типа– чуть ли не каждая компания, отдел или индивидуальный проект имели свою собственную реализацию строки. Да что говорить, в предыдущих двух изданиях этой книги мы делали то же самое! Это порождало проблемы совместимости и переносимости программ. Реализация стандартного класса string стандартной библиотекой С++ призвана была положить конец этому изобретению велосипедов.
Попробуем специфицировать минимальный набор операций, которыми должен обладать класс string:
инициализация массивом символов (строкой встроенного типа) или другим объектом типа string. Встроенный тип не обладает второй возможностью;
копирование одной строки в другую. Для встроенного типа приходится использовать функцию strcpy();
доступ к отдельным символам строки для чтения и записи. Во встроенном массиве для этого применяется операция взятия индекса или косвенная адресация;
сравнение двух строк на равенство. Для встроенного типа используется функция strcmp();
конкатенация двух строк, получая результат либо как третью строку, либо вместо одной из исходных. Для встроенного типа применяется функция strcat(), однако чтобы получить результат в новой строке, необходимо последовательно задействовать функции strcpy() и strcat();
вычисление длины строки. Узнать длину строки встроенного типа можно с помощью функции strlen();
возможность узнать, пуста ли строка. У встроенных строк для этой цели приходится проверять два условия:
char str = 0;
//...
if ( ! str || ! *str )
return;
Класс string стандартной библиотеки С++ реализует все перечисленные операции (и гораздо больше, как мы увидим в главе 6). В данном разделе мы научимся пользоваться основными операциями этого класса.
Для того чтобы использовать объекты класса string, необходимо включить соответствующий заголовочный файл:
#include <string>
Вот пример строки из предыдущего раздела, представленной объектом типа string и инициализированной строкой символов:
#include <string>
string st( "Цена бутылки вина\n" );
Длину строки возвращает функция-член size() (длина не включает завершающий нулевой символ).
cout << "Длина "
<< st
<< ": " << st.size()
<< " символов, включая символ новой строки\n";
Вторая форма определения строки задает пустую строку:
string st2; // пустая строка
Как мы узнаем, пуста ли строка? Конечно, можно сравнить ее длину с 0:
if ( ! st.size() )
// правильно: пустая
Однако есть и специальный метод empty(), возвращающий true для пустой строки и false для непустой:
if ( st.empty() )
// правильно: пустая
Третья форма конструктора инициализирует объект типа string другим объектом того же типа:
string st3( st );
Строка st3 инициализируется строкой st. Как мы можем убедиться, что эти строки совпадают? Воспользуемся оператором сравнения (==):
if ( st == st3 )
// инициализация сработала
Как скопировать одну строку в другую? С помощью обычной операции присваивания:
st2 = st3; // копируем st3 в st2
Для конкатенации строк используется операция сложения (+) или операция сложения с присваиванием (+=). Пусть даны две строки:
string s1( "hello, " );
string s2( "world\n" );
Мы можем получить третью строку, состоящую из конкатенации первых двух, таким образом:
string s3 = s1 + s2;
Если же мы хотим добавить s2 в конец s1, мы должны написать:
s1 += s2;
Операция сложения может конкатенировать объекты класса string не только между собой, но и со строками встроенного типа. Можно переписать пример, приведенный выше, так, чтобы специальные символы и знаки препинания представлялись встроенным типом, а значимые слова – объектами класса string:
const char *pc = ", ";
string s1( "hello" );
string s2( "world" );
string s3 = s1 + pc + s2 + "\n";
Подобные выражения работают потому, что компилятор знает, как автоматически преобразовывать объекты встроенного типа в объекты класса string. Возможно и простое присваивание встроенной строки объекту string:
string s1;
const char *pc = "a character array";
s1 = pc; // правильно
Обратное преобразование, однако, не работает. Попытка выполнить следующую инициализацию строки встроенного типа вызовет ошибку компиляции:
char *str = s1; // ошибка компиляции
Чтобы осуществить такое преобразование, необходимо явно вызвать функцию-член с несколько странным названием c_str():
char *str = s1.c_str(); // почти правильно
Функция c_str() возвращает указатель на символьный массив, содержащий строку объекта string в том виде, в каком она находилась бы во встроенном строковом типе.
Приведенный выше пример инициализации указателя char *str все еще не совсем корректен. c_str() возвращает указатель на константный массив, чтобы предотвратить возможность непосредственной модификации содержимого объекта через этот указатель, имеющий тип
const char *
(В следующем разделе мы расскажем о ключевом слове const). Правильный вариант инициализации выглядит так:
const char *str = s1.c_str(); // правильно
К отдельным символам объекта типа string, как и встроенного типа, можно обращаться с помощью операции взятия индекса. Вот, например, фрагмент кода, заменяющего все точки символами подчеркивания:
string str( "fa.disney.com" );
int size = str.size();
for ( int ix = 0; ix < size; ++ix )
if ( str[ ix ] == '.' )
str[ ix ] = '_';
Вот и все, что мы хотели сказать о классе string прямо сейчас. На самом деле, этот класс обладает еще многими интересными свойствами и возможностями. Скажем, предыдущий пример реализуется также вызовом одной-единственной функции replace():
replace( str.begin(), str.end(), '.', '_' );
replace() – один из обобщенных алгоритмов, с которыми мы познакомились в разделе 2.8 и которые будут детально разобраны в главе 12. Эта функция пробегает диапазон от begin() до end(), которые возвращают указатели на начало и конец строки, и заменяет элементы, равные третьему своему параметру, на четвертый.
Упражнение 3.12
Найдите ошибки в приведенных ниже операторах:
(a) char ch = "The long and winding road";
(b) int ival = &ch;
(c) char *pc = &ival;
(d) string st( &ch );
(e) pc = 0; (i) pc = '0';
(f) st = pc; (j) st = &ival;
(g) ch = pc[0]; (k) ch = *pc;
(h) pc = st; (l) *pc = ival;
Упражнение 3.13
Объясните разницу в поведении следующих операторов цикла:
while ( st++ )
++cnt;
while ( *st++ )
++cnt;
Упражнение 3.14
Даны две семантически эквивалентные программы. Первая использует встроенный строковый тип, вторая – класс string:
// ***** Реализация с использованием C-строк *****
#include <iostream>
#include <cstring>
int main()
{
int errors = 0;
const char *pc = "a very long literal string";
for ( int ix = 0; ix < 1000000; ++ix )
{
int len = strlen( pc );
char *pc2 = new char[ len + 1 ];
strcpy( pc2, pc );
if ( strcmp( pc2, pc ))
++errors;
delete [] pc2;
}
cout << "C-строки: "
<< errors << " ошибок.\n";
}
// ***** Реализация с использованием класса string *****
#include <iostream>
#include <string>
int main()
{
int errors = 0;
string str( "a very long literal string" );
for ( int ix = 0; ix < 1000000; ++ix )
{
int len = str.size();
string str2 = str;
if ( str != str2 )
}
cout << "класс string: "
<< errors << " ошибок.\n;
}
Что эти программы делают?
Оказывается, вторая реализация выполняется в два раза быстрее первой. Ожидали ли вы такого результата? Как вы его объясните?
Упражнение 3.15
Могли бы вы что-нибудь улучшить или дополнить в наборе операций класса string, приведенных в последнем разделе? Поясните свои предложения.
Класс type_info
Точное определение класса type_info зависит от реализации, но некоторые его характерные черты остаются неизменными в любой программе на C++:
class type_info {
// представление зависит от реализации
private:
type_info( const type_info& );
type_info& operator= ( const type_info& );
public:
virtual ~type_info();
int operator==( const type_info& );
int operator!=( const type_info& );
const char * name() const;
};
Поскольку копирующие конструктор и оператор присваивания – закрытые члены класса type_info, то пользователь не может создать его объекты в своей программе:
#include <typeinfo>
type_info t1; // ошибка: нет конструктора по умолчанию
// ошибка: копирующий конструктор закрыт
type_info t2 (typeid( unsigned int ) );
Единственный способ создать объект класса type_info – воспользоваться оператором typeid.
В классе определены также операторы сравнения. Они позволяют сравнивать два объекта type_info, а следовательно, и результаты, возвращенные двумя операторами typeid. (Мы говорили об этом в предыдущем подразделе.)
typeid( re ) == typeid( manager ) // истинно
typeid( *pe ) != typeid( employee ) // ложно
Функция name() возвращает C-строку с именем типа, представленного объектом type_info. Этой функцией можно пользоваться в программах следующим образом:
#include <typeinfo>
int main() {
employee *pe = new manager;
// печатает: "manager"
cout << typeid( *pe ).name() << endl;
}
Для работы с функцией-членом name() нужно включить заголовочный файл <typeinfo>.
Имя типа – это единственная информация, которая гарантированно возвращается всеми реализациями C++, при этом используется функция-член name() класса type_info. В начале этого раздела упоминалось, что поддержка RTTI зависит от реализации и иногда в классе type_info бывают дополнительные функции-члены. Чтобы узнать, каким образом обеспечивается поддержка RTTI в вашем компиляторе, обратитесь к справочному руководству по нему. Кроме того, можно получить любую информацию, которую компилятор знает о типе, например:
список функций-членов класса;
способ размещения объекта в памяти, т.е. взаимное расположение подобъектов базового и производных классов.
Одним из способов расширения поддержки RTTI является включение дополнительной информации в класс, производный от type_info. Поскольку в классе type_info есть виртуальный деструктор, то оператор dynamic_cast позволяет выяснить, имеется ли некоторое конкретное расширение RTTI. Предположим, что некоторый компилятор предоставляет расширенную поддержку RTTI посредством класса extended_type_info, производного от type_info. С помощью оператора dynamic_cast программа может узнать, принадлежит ли объект типа type_info, возвращенный оператором typeid, к типу extended_type_info. Если да, то пользоваться расширенной поддержкой RTTI разрешено.
#include <typeinfo>
// Файл typeinfo содержит определение типа extended_type_info
void func( employee* p )
{
// понижающее приведение типа type_info* к extended_type_info*
if ( eti *eti_p = dynamic_cast<eti *>( &typeid( *p ) ) )
{
// если dynamic_cast завершается успешно,
// можно пользоваться информацией из extended_type_info через eti_p
}
else
{
// если dynamic_cast завершается неудачно,
// можно пользоваться только стандартным type_info
}
}
Если dynamic_cast завершается успешно, то оператор typeid вернет объект класса extended_type_info, т.е. компилятор обеспечивает расширенную поддержку RTTI, чем программа может воспользоваться. В противном случае допустимы только базовые средства RTTI.
Упражнение 19.1
Дана иерархия классов, в которой у каждого класса есть конструктор по умолчанию и виртуальный деструктор:
class X { ... };
class A { ... };
class B : public A { ... };
class C : public B { ... };
class D : public X, public C { ... };
Какие из данных операторов dynamic_cast завершатся неудачно?
(a) D *pd = new D;
A *pa = dynamic_cast< A* > ( pd );
(b) A *pa = new C;
C *pc = dynamic_cast< C* > ( pa );
(c) B *pb = new B;
D *pd = dynamic_cast< D* > ( pb );
(d) A *pa = new D;
X *px = dynamic_cast< X* > ( pa );
Упражнение 19.2
Объясните, когда нужно пользоваться оператором dynamic_cast вместо виртуальной функции?
Упражнение 19.3
Пользуясь иерархией классов из упражнения 19.1, перепишите следующий фрагмент так, чтобы в нем использовался ссылочный вариант dynamic_cast для преобразования *pa в тип D&:
if ( D *pd = dynamic_cast< D* >( pa ) ) {
// использовать члены D
}
else {
// использовать члены A
}
Упражнение 19.4
Дана иерархия классов, в которой у каждого класса есть конструктор по умолчанию и виртуальный деструктор:
class X { ... };
class A { ... };
class B : public A { ... };
class C : public B { ... };
class D : public X, public C { ... };
Какое имя типа будет напечатано в каждом из следующих случаев:
(a) A *pa = new D;
cout << typeid( pa ).name() << endl;
(b) X *px = new D;
cout << typeid( *px ).name() << endl;
(c) C obj;
A& ra = cobj;
cout << typeid( &ra ).name() << endl;
(d) X *px = new D;
A& ra = *px;
cout << typeid( ra ).name() << endl;
Класс vector
Использование класса vector (см. раздел 2.8) является альтернативой применению встроенных массивов. Этот класс предоставляет гораздо больше возможностей, поэтому его использование предпочтительней. Однако встречаются ситуации, когда не обойтись без массивов встроенного типа. Одна из таких ситуаций– обработка передаваемых программе параметров командной строки, о чем мы будем говорить в разделе 7.8. Класс vector, как и класс string, является частью стандартной библиотеки С++.
Для использования вектора необходимо включить заголовочный файл:
#include <vector>
Существуют два абсолютно разных подхода к использованию вектора, назовем их идиомой массива и идиомой STL. В первом случае объект класса vector используется точно так же, как массив встроенного типа. Определяется вектор заданной размерности:
vector< int > ivec( 10 );
что аналогично определению массива встроенного типа:
int ia[ 10 ];
Для доступа к отдельным элементам вектора применяется операция взятия индекса:
void simp1e_examp1e()
{
const int e1em_size = 10;
vector< int > ivec( e1em_size );
int ia[ e1em_size ];
for ( int ix = 0; ix < e1em_size; ++ix )
ia[ ix ] = ivec[ ix ];
// ...
}
Мы можем узнать размерность вектора, используя функцию size(), и проверить, пуст ли вектор, с помощью функции empty(). Например:
void print_vector( vector<int> ivec )
{
if ( ivec.empty() )
return;
for ( int ix=0; ix< ivec.size(); ++ix )
cout << ivec[ ix ] << ' ';
}
Элементы вектора инициализируются значениями по умолчанию. Для числовых типов и указателей таким значением является 0. Если в качестве элементов выступают объекты класса, то инициатор для них задается конструктором по умолчанию (см. раздел 2.3). Однако инициатор можно задать и явно, используя форму:
vector< int > ivec( 10, -1 );
Все десять элементов вектора будут равны -1.
Массив встроенного типа можно явно инициализировать списком:
int ia[ 6 ] = { -2, -1, О, 1, 2, 1024 };
Для объекта класса vector аналогичное действие невозможно. Однако такой объект может быть инициализирован с помощью массива встроенного типа:
// 6 элементов ia копируются в ivec
vector< int > ivec( ia, ia+6 );
Конструктору вектора ivec передаются два указателя – указатель на начало массива ia и на элемент, следующий за последним. В качестве списка начальных значений допустимо указать не весь массив, а некоторый его диапазон:
// копируются 3 элемента: ia[2], ia[3], ia[4]
vector< int > ivec( &ia[ 2 ], &ia[ 5 ] );
Еще одним отличием вектора от массива встроенного типа является возможность инициализации одного объекта типа vector другим и использования операции присваивания для копирования объектов. Например:
vector< string > svec;
void init_and_assign()
{
// один вектор инициализируется другим
vector< string > user_names( svec );
// ...
// один вектор копируется в другой
svec = user_names;
}
Говоря об идиоме STL[6], мы подразумеваем совсем другой подход к использованию вектора. Вместо того чтобы сразу задать нужный размер, мы определяем пустой вектор:
vector< string > text;
Затем добавляем к нему элементы при помощи различных функций. Например, функция push_back()вставляет элемент в конец вектора. Вот фрагмент кода, считывающего последовательность строк из стандартного ввода и добавляющего их в вектор:
string word;
while ( cin >> word ) {
text.push_back( word );
// ...
}
Хотя мы можем использовать операцию взятия индекса для перебора элементов вектора:
cout << "считаны слова: \n";
for ( int ix =0; ix < text.size(); ++ix )
cout << text[ ix ] << ' ';
cout << endl;
более типичным в рамках данной идиомы будет использование итераторов:
cout << "считаны слова: \n";
for ( vector<string>::iterator it = text.begin();
it != text.end(); ++it )
cout << *it << ' ';
cout << endl;
Итератор – это класс стандартной библиотеки, фактически являющийся указателем на элемент массива.
Выражение
*it;
разыменовывает итератор и дает сам элемент вектора. Инструкция
++it;
сдвигает указатель на следующий элемент. Не нужно смешивать эти два подхода. Если следовать идиоме STL при определении пустого вектора:
vector<int> ivec;
будет ошибкой написать:
ivec[0] = 1024;
У нас еще нет ни одного элемента вектора ivec; количество элементов выясняется с помощью функции size().
Можно допустить и противоположную ошибку. Если мы определили вектор некоторого размера, например:
vector<int> ia( 10 );
то вставка элементов увеличивает его размер, добавляя новые элементы к существующим. Хотя это и кажется очевидным, тем не менее, начинающий программист вполне мог бы написать:
const int size = 7;
int ia[ size ] = { 0, 1, 1, 2, 3, 5, 8 };
vector< int > ivec( size );
for ( int ix = 0; ix < size; ++ix )
ivec.push_back( ia[ ix ] );
Имелась в виду инициализация вектора ivec значениями элементов ia, вместо чего получился вектор ivec размера 14.
Следуя идиоме STL, можно не только добавлять, но и удалять элементы вектора. (Все это мы рассмотрим подробно и с примерами в главе 6.)
Упражнение 3.24
Имеются ли ошибки в следующих определениях?
int ia[ 7 ] = { 0, 1, 1, 2, 3, 5, 8 };
(a) vector< vector< int > > ivec;
(b) vector< int > ivec = { 0, 1, 1, 2, 3, 5, 8 };
(c) vector< int > ivec( ia, ia+7 );
(d) vector< string > svec = ivec;
(e) vector< string > svec( 10, string( "null" ));
Упражнение 3.25
Реализуйте следующую функцию:
bool is_equa1( const int*ia, int ia_size,
const vector<int> &ivec );
Функция is_equal() сравнивает поэлементно два контейнера. В случае разного размера контейнеров “хвост” более длинного в расчет не принимается. Понятно, что, если все сравниваемые элементы равны, функция возвращает true, если отличается хотя бы один – false. Используйте итератор для перебора элементов. Напишите функцию main(), обращающуюся к is_equal().
Классы
Механизм классов в C++ позволяет пользователям определять собственные типы данных. По этой причине их часто называют пользовательскими типами. Класс может наделять дополнительной функциональностью уже существующий тип. Так, например, IntArray, введенный в главе 2, предоставляет больше возможностей, чем тип “массив int”. С помощью классов можно создавать абсолютно новые типы, например Screen (экран) или Account (расчетный счет). Как правило, классы используются для абстракций, не отражаемых встроенными типами адекватно.
В этой главе мы узнаем, как определять типы и использовать объекты классов; увидим, что определение класса вводит как данные-члены, описывающие его, так и функции-члены, составляющие набор операций, применимых к объектам класса. Мы покажем, как можно обеспечить сокрытие информации, объявив внутреннее представление и реализацию закрытыми, но открыв операции над объектами. Говорят, что закрытое внутреннее представление инкапсулировано, а открытую часть класса называют его интерфейсом.
Далее в этой главе мы познакомимся с особым видом членов класса– статическими членами. Мы расскажем также, как можно использовать указатели на члены и функции-члены класса, и рассмотрим объединения, представляющие собой специализированный вид класса для хранения объектов разных типов в одной области памяти. Завершается глава обсуждением области видимости класса и описанием правил разрешения имен в этой области; затрагиваются такие понятия, как вложенные классы, классы-члены пространства имен и локальные классы.
Классы как члены пространства имен *
Представленные до сих пор классы определены в области видимости глобального пространства имен. Но их можно определять и в объявленных пользователем пространствах. Имя класса, определенного таким образом, доступно только в области видимости этого пространства, т.е. оно не конфликтует с именами, объявленными в других пространствах имен. Например:
namespace cplusplus_primer {
class Node { /* ... */ };
}
namespace DisneyFeatureAnimation {
class Node { /* ... */ };
}
Node *pnode; // ошибка: Node не видно в глобальной области видимости
// правильно: объявляет nodeObj как объект
// квалифицированного типа DisneyFeatureAnimation::Node
DisneyFeatureAnimation::Node nodeObj;
// using-объявление делает Node видимым в глобальной области видимости
using cplusplus_primer::Node;
Node another; // cplusplus_primer::Node
Как было показано в двух предыдущих разделах, член класса (функция-член, статический член или вложенный класс) может быть определен вне его тела. Если мы реализуем библиотеку и помещаем определения наших классов в объявленное пользователем пространство имен, то где расположить определения членов, находящиеся вне тел своих классов? Их можно разместить либо в пространстве имен, которое содержит определение самого внешнего класса, либо в одном из объемлющих его пространств. Это дает возможность организовать код библиотеки следующим образом:
// --- primer.h ---
namespace cplusplus_primer {
class List {
// ...
private:
class ListItem {
public:
void check_status();
int action();
// ...
};
};
}
// --- primer.C ---
#include "primer.h"
namespace cplusplus_primer {
// правильно: check_status() определено в том же пространстве имен,
// что и List
void List::ListItem::check_status() { }
}
// правильно: action() определена в глобальной области видимости
// в пространстве имен, объемлющем определение класса List
// Имя члена квалифицировано именем пространства
int cplusplus_primer::List::ListItem::action() { }
Члены вложенного класса ListItem можно определить в пространстве имен cplusplus_primer, которое содержит определение List, или в глобальном пространстве, включающем определение cplusplus_primer. В любом случае имя члена в определении должно быть квалифицировано именами объемлющих классов и объявленных пользователем пространств, вне которых находится объявление члена.
Как происходит разрешение имени в определении члена, которое находится в объявленном пользователем пространстве? Например, как будет разрешено someVal:
int cplusplus_primer::List::ListItem::action() {
int local = someVal;
// ...
}
Сначала просматриваются локальные области видимости в определении функции-члена, затем поиск продолжается в области видимости ListItem, затем – в области видимости List. До этого момента все происходит так же, как в процессе разрешения имен, описанном в разделе 13.10. Далее просматриваются объявления из пространства cplusplus_primer и наконец объявления в глобальной области видимости, причем во внимание принимаются только те, которые расположены до определения функции-члена action():
// --- primer.h ---
namespace cplusplus_primer {
class List {
// ...
private:
class ListItem {
public:
int action();
// ...
};
};
const int someVal = 365;
}
// --- primer.C ---
#include "primer.h"
namespace cplusplus_primer {
int List::ListItem::action() {
// правильно: cplusplus_primer::someVal
int local = someVal;
// ошибка: calc() еще не объявлена
double result = calc( local );
// ...
}
double calc(int) { }
// ...
}
Определение пространства имен cplusplus_primer не является непрерывным. Определения класса List и объекта someVal размещены в первом его разделе, который находится в заголовочном файле primer.h. Определение функции calc() появляется в определении пространства имен, расположенном в файле реализации primer.C. Использование calc() внутри action() ошибочно, так как она объявлена после использования. Если calc() – часть интерфейса cplusplus_primer, ее следовало бы объявить в той части данного пространства, которая находится в заголовочном файле:
// --- primer.h ---
namespace cplusplus_primer {
class List {
// ...
}
const int someVal = 365;
double calc(int);
}
Если же calc() используется только в action() и не является частью интерфейса пространства имен, то ее нужно объявить перед action(), чтобы можно было ссылаться на нее внутри определения action().
Здесь прослеживается аналогия с процессом поиска объявлений в глобальной области видимости, о котором мы говорили в предыдущих разделах: объявления, предшествующие определению члена, принимаются во внимание, тогда как следующие за ним игнорируются.
Довольно просто запомнить, в каком порядке просматриваются области видимости при поиске имени из определения функции, расположенного вне определения класса. Имена, которыми квалифицировано имя члена, указывают порядок рассмотрения пространств. Например, имя action() в предыдущем примере квалифицируется так:
cplusplus_primer::List::ListItem::action()
Квалификаторы cplusplus_primer::List::ListItem:: записаны в порядке, обратном тому, в котором просматриваются имена областей видимости классов и пространств имен. Сначала поиск ведется в области ListItem, затем продолжается в объемлющем классе List и наконец в пространстве cplusplus_primer, предшествующем той области, в которой находится определение action(). Во время поиска в любой области видимости класса просматриваются все объявления членов, а в любом пространстве имен – только те объявления, которые встречались перед определением члена.
Класс, определенный в области видимости пространства имен, потенциально виден во всей программе. Если заголовочный файл primer.h включен в несколько исходных файлов, то имя cplusplus_primer::List везде относится к одному и тому же классу. Класс – это сущность, для которой в программе может быть более одного определения. Определение класса должно присутствовать один раз в каждом исходном файле, где определяются или используются сам класс или его члены. Однако оно должно быть одинаковым во всех файлах, где встречается, поэтому его следует помещать в заголовочный файл, например primer.h. Затем такой файл можно включать в любой исходный, где определяются или используются члены класса. Это предотвратит несоответствия в случае, когда определение класса записывается более одного раза.
Невстроенные функции-члены и статические данные-члены класса в пространстве имен – это также программные сущности. Однако они могут быть определены лишь один раз во всей программе. Поэтому их определения помещаются не в заголовочный, а в отдельный исходный файл типа primer.C.
Упражнение 13.22
Используя класс iStack, определенный в упражнении 13.21, объявите классы исключений pushOnFull и popOnEmpty как члены пространства имен LibException:
namespace LibException {
class pushOnFull{ };
class popOnEmpty{ };
}
а сам iStack – членом пространства имен Container. Модифицируйте соответствующим образом определение данного класса и его функций-членов, а также определение main().
Когда использовать указатель this
Наша функция main() вызывает функции-члены класса Screen для объектов myScreen и bufScreen таким образом, что каждое действие– это отдельная инструкция. У нас есть возможность определить функции-члены так, чтобы конкатенировать их вызовы при обращении к одному и тому же объекту. Например, все вызовы внутри main() будут выглядеть так:
int main() {
// ...
myScreen.clear().move( 2, 2 ), set( '*' ). display();
bufScreen.reSize( 5, 5 ).display();
}
Именно так интуитивно представляется последовательность операций с экраном: очистить экран myScreen, переместить курсор в позицию (2,2), записать в эту позицию символ '*' и вывести результат.
Операторы доступа “точка” и “стрелка” левоассоциативны, т.е. их последовательность выполняется слева направо. Например, сначала вызывается myScreen.clear(), затем myScreen.move() и т.д. Чтобы myScreen.move() можно было вызвать после myScreen.clear(), функция clear() должна возвращать объект myScreen, для которого она была вызвана. Мы уже видели, что доступ к объекту внутри функции-члена класса производится в помощью указателя this. Вот реализация clear():
// объявление clear() находится в теле класса
// в нем задан аргумент по умолчанию bkground = '#'
Screen& Screen::clear( char bkground )
{ // установить курсор в левый верхний угол и очистить экран
_cursor = 0;
_screen.assign( // записать в строку
_screen.size(), // size() символов
bkground // со значением bkground
);
// вернуть объект, для которого была вызвана функция
return *this;
}
Обратите внимание, что возвращаемый тип этой функции-члена – Screen& – ссылка на объект ее же класса. Чтобы конкатенировать вызовы, необходимо также пересмотреть реализацию move() и set(). Возвращаемый тип следует изменить с void на Screen&, а в определении возвращать *this.
Аналогично функцию-член display() можно написать так:
Screen& Screen::display()
{
typedef string::size_type idx_type;
for ( idx_type ix = 0; ix < _height; ++ix )
{ // для каждой строки
idx_type offset = _width * ix; // смещение строки
for ( idx_type iy = 0; iy < _width; ++iy )
// для каждой колонки вывести элемент
cout << _screen[ offset + iy ];
cout << endl;
}
return *this;
}
А вот реализация reSize():
// объявление reSize() находится в теле класса
// в нем задан аргумент по умолчанию bkground = '#'
Screen& Screen::reSize( int h, int w, char bkground )
{ // сделать высоту экрана равной h, а ширину - равной w
// запомнить содержимое экрана
string local(_screen);
// заменить строку _screen
_screen.assign( // записать в строку
h * w, // h * w символов
bkground // со значением bkground
);
typedef string::size_type idx_type;
idx_type local_pos = 0;
// скопировать содержимое старого экрана в новый
for ( idx_type ix = 0; ix < _height; ++ix )
{ // для каждой строки
idx_type offset = w * ix; // смещение строки
for ( idx_type iy = 0; iy < _width; ++iy )
// для каждой колонки присвоить новое значение
_screen[ offset + iy ] = local[ local_pos++ ];
}
_height = h;
_width = w;
// _cursor не меняется
return *this;
}
Работа указателя this не исчерпывается возвратом объекта, к которому была применена функция-член. При рассмотрении copy() в разделе 13.3 мы видели и другой способ его использования:
void Screen::copy( const Screen& sobj )
{
// если этот объект Screen и sobj - одно и то же,
// копирование излишне
if ( this != sobj )
{
// скопировать значение sobj в this
}
}
Указатель this хранит адрес объекта, для которого была вызвана функция-член. Если адрес, на который ссылается sobj, совпадает со значением this, то sobj и this относятся к одному и тому же объекту, так что операция копирования не нужна. (Мы еще встретимся с этой конструкцией, когда будем рассматривать копирующий оператор присваивания в разделе 14.7.)
Упражнение 13.7
Указатель this можно использовать для модификации адресуемого объекта, а также для его замены другим объектом того же типа. Например, функция-член assign() класса classType выглядит так. Можете ли вы объяснить, что она делает?
classType& classType::assign( const classType &source )
{
if ( this != &source )
{
this->~classType();
new (this) classType( source );
}
return *this;
}
Напомним, что ~classType – это имя деструктора. Оператор new выглядит несколько причудливо, но мы уже встречались с подобным в разделе 8.4.
Как вы относитесь к такому стилю программирования? Безопасна ли эта операция? Почему?
Когда использовать встроенные функции-члены
Обратите внимание, что определения функций home(), get(), height() и width() приведены прямо в теле класса. Такие функции называются встроенными. (Мы говорили об этом в разделе 7.6.)
Функции-члены можно объявить в теле класса встроенными и явно, поместив перед типом возвращаемого значения ключевое слово inline:
class Screen {
public:
// использование ключевого слова inline
// для объявления встроенных функций-членов
inline void home() { _cursor = 0; }
inline char get() { return _screen[_cursor]; }
// ...
};
Определения home() и get() в приведенных примерах эквивалентны. Поскольку ключевое слово inline избыточно, мы в этой книге не пишем его явно для функций-членов, определенных в теле класса.
Функции-члены, состоящие из двух или более строк, лучше определять вне тела. Для идентификации функции как члена некоторого класса требуется специальный синтаксис объявления: имя функции должно быть квалифицировано именем ее класса. Вот как выглядит определение функции checkRange(), квалифицированное именем Screen:
#include <iostream>
#include "screen.h"
// имя функции-члена квалифицировано именем Screen::
bool Screen::checkRange( int row, int col )
{ // проверить корректность координат
if ( row < 1 || row > _height ||
col < 1 || col > _width ) {
cerr << "Screen coordinates ( "
<< row << ", " << col
<< " ) out of bounds.\n";
return false;
}
return true;
}
Прежде чем определять функцию-член вне тела класса, необходимо объявить ее внутри тела, обеспечив ее видимость. Например, если бы перед определением функции checkRange() не был включен заголовочный файл Screen.h, то компилятор выдал бы сообщение об ошибке. Тело класса определяет полный список его членов. Этот список не может быть расширен после закрытия тела.
Обычно функции-члены, определенные вне тела класса, не делают встроенными. Но объявить такую функцию встроенной можно, если явно добавить слово inline в объявление функции внутри тела класса или в ее определение вне тела, либо сделав то и другое одновременно. В следующем примере move() определена как встроенная функция-член класса Screen:
inline void Screen::move( int r, int c )
{ // переместить курсор в абсолютную позицию
if ( checkRange( r, c ) ) // позиция на экране задана корректно?
{
int row = (r-1) * _width; // смещение начала строки
_cursor = row + c - 1;
}
}
Функция get(int, int) объявляется встроенной с помощью слова inline:
class Screen {
public:
inline char get( int, int );
// объявления других функций-членов не изменяются
};
Определение функции следует после объявления класса. При этом слово inline можно опустить:
char Screen::get( int r, int c )
{
move( r, c ); // устанавливаем _cursor
return get(); // вызываем другую функцию-член get()
}
Так как встроенные функции-члены должны быть определены в каждом исходном файле, где они вызываются, то встроенную функцию, не определенную в теле класса, следует поместить в тот же заголовочный файл, в котором определен ее класс. Например, представленные ранее определения move() и get() должны находиться в заголовочном файле Screen.h после определения класса Screen.
Когда не надо перегружать имя функции
В каких случаях перегрузка имени не дает преимуществ? Например, тогда, когда присвоение функциям разных имен облегчает чтение программы. Вот несколько примеров. Следующие функции оперируют одним и тем же абстрактным типом даты. На первый взгляд, они являются подходящими кандидатами для перегрузки:
void setDate( Date&, int, int, int );
Date &convertDate( const string & );
void printDate( const Date& );
Эти функции работают с одним типом данных– классом Date, но выполняют семантически различные действия. В этом случае лексическая сложность, связанная с употреблением различных имен, проистекает из принятого программистом соглашения об обеспечении набора операций над типом данных и именования функций в соответствии с семантикой этих операций. Правда, механизм классов C++ делает такое соглашение излишним. Следовало бы сделать такие функции членами класса Date, но при этом оставить разные имена, отражающие смысл операции:
#include <string>
class Date {
public:
set( int, int, int );
Date& convert( const string & );
void print();
// ...
};
Приведем еще один пример. Следующие пять функций-членов Screen выполняют различные операции над экранным курсором, являющимся принадлежностью того же класса. Может показаться, что разумно перегрузить эти функции под общим названием move():
Screen& moveHome();
Screen& moveAbs( int, int );
Screen& moveRel( int, int, char *direction );
Screen& moveX( int );
Screen& moveY( int );
Впрочем, последние две функции перегрузить нельзя, так как у них одинаковые списки параметров. Чтобы сделать сигнатуру уникальной, объединим их в одну функцию:
// функция, объединяющая moveX() и moveY()
Screen& move( int, char xy );
Теперь у всех функций разные списки параметров, так что их можно перегрузить под именем move(). Однако этого делать не следует: разные имена несут информацию, без которой программу будет труднее понять. Так, выполняемые данными функциями операции перемещения курсора различны. Например, moveHome() осуществляет специальный вид перемещения в левый верхний угол экрана. Какой из двух приведенных ниже вызовов более понятен пользователю и легче запоминается?
// какой вызов понятнее?
myScreen.home(); // мы считаем, что этот!
myScreen.move();
В некоторых случаях не нужно ни перегружать имя функции, ни назначать разные имена: применение подразумеваемых по умолчанию значений аргументов позволяет объединить несколько функций в одну. Например, функции управления курсором
moveAbs(int, int);
moveAbs(int, int, char*);
различаются наличием третьего параметра типа char*. Если их реализации похожи и для третьего аргумента можно найти разумное значение по умолчанию, то обе функции можно заменить одной. В данном случае на роль значения по умолчанию подойдет указатель со значением 0:
move( int, int, char* = 0 );
Применять те или иные возможности следует тогда, когда этого требует логика приложения. Вовсе не обязательно включать перегруженные функции в программу только потому, что они существуют.
Когда нельзя использовать обобщенные алгоритмы
Ассоциативные контейнеры (отображения и множества) поддерживают определенный порядок элементов для быстрого поиска и извлечения. Поэтому к ним не разрешается применять обобщенные алгоритмы, меняющие порядок, такие, как sort() и partition(). Если в ассоциативном контейнере требуется переставить элементы, то необходимо сначала скопировать их в последовательный контейнер, например в вектор или список.
Контейнер list (список) реализован в виде двусвязного списка: в каждом элементе, помимо собственно данных, хранятся два члена-указателя– на следующий и на предыдущий элементы. Основное преимущество списка – это эффективная вставка и удаление одного элемента или целого диапазона в произвольное место списка, а недостаток – невозможность произвольного доступа. Например, можно написать:
vector<string>::iterator vec_iter = vec.begin() + 7;
Такая форма вполне допустима и инициализирует vec_iter адресом восьмого элемента вектора, но запись
// ошибка: арифметические операции над итераторами
// не поддерживаются списком
list<string>::iterator list_iter = slist.begin() + 7;
некорректна, так как элементы списка не занимают непрерывную область памяти. Для того чтобы добраться до восьмого элемента, необходимо посетить все промежуточные.
Поскольку список не поддерживает произвольного доступа, то алгоритмы merge(), remove(), reverse(), sort() и unique() лучше к таким контейнерам не применять, хотя ни один из них явно не требует наличия соответствующего итератора. Вместо этого для списка определены специализированные версии названных операций в виде функций-членов, а также операция splice():
list::merge() объединяет два отсортированных списка
list::remove() удаляет элементы с заданным значением
list::remove_if()удаляет элементы, удовлетворяющие некоторому условию
list::reverse() переставляет элементы списка в обратном порядке
list::sort() сортирует элементы списка
list::splice() перемещает элементы из одного списка в другой
list::unique() оставляет один элемент из каждой цепочки одинаковых смежных элементов
Композиция объектов
Есть две формы композиции объектов:
композиция по значению, когда членом одного класса объявляется сам объект другого класса. Мы показывали это в исправленной реализации PeekbackStack;
композиция по ссылке, когда членом одного класса является указатель или ссылка на объект другого класса.
Композиция по значению обеспечивает автоматическое управление временем жизни объекта и семантику копирования. Кроме того, прямой доступ к объекту оказывается более эффективным. А в каких случаях следует предпочесть композицию по ссылке?
Предположим, что мы решили с помощью композиции представить класс Endangered. Надо ли определить его объект непосредственно внутри ZooAnimal или сослаться на него с помощью указателя или ссылки? Сначала выясним, все ли объекты ZooAnimal обладают этой характеристикой, а если нет, то может ли она изменяться с течением времени (допустимо ли добавлять или удалять эту характеристику).
Если ответ на первый вопрос положительный, то, как правило, лучше применить композицию по значению. (Как правило, но не всегда, поскольку с точки зрения эффективности включение больших объектов не оптимально, особенно когда они часто копируются. В таких случаях композиция по ссылке позволит обойтись без ненужных копирований, если применять при этом подсчет ссылок и технику, называемую копированием при записи. Увеличение эффективности, правда, достигается за счет усложнения управления объектом. Обсуждение этой техники не вошло в наш вводный курс; тем, кому это интересно, рекомендуем прочитать книгу [KOENIG97], главы 6 и 7.)
Если же оказывается, что только некоторые объекты класса ZooAnimal обладают указанной характеристикой, то лучшим вариантом будет композиция по ссылке (скажем, в примере с зоопарком не имеет смысла включать в процветающие виды большой объект, описывающий виды вымирающие).
Поскольку объекта Endangered может и не существовать, то представлять его надо указателем, а не ссылкой. (Предполагается, что нулевой указатель не адресует объект. Ссылка же всегда должна именовать определенный объект. В разделе 3.6 это различие объяснялось более подробно.)
Если ответ на второй вопрос положительный, то необходимо задать функции, позволяющие вставить и удалить объект Endangered во время выполнения.
В нашем примере лишь небольшая часть всего множества животных в зоопарке находится под угрозой вымирания. Кроме того, по крайней мере теоретически, данная характеристика не является постоянной, и, допустим, в один прекрасный день это может перестать грозить панде.
class ZooAnimal {
public:
// ...
const Endangered* Endangered() const;
void addEndangered( Endangered* );
void removeEndangered();
// ...
protected:
Endangered *_endangered;
// ...
};
Если предполагается, что наше приложение будет работать на разных платформах, то полезно инкапсулировать всю платформенно-зависимую информацию в иерархию абстрактных классов, чтобы запрограммировать платформенно-независимый интерфейс. Например, для вывода объекта ZooAnimal на дисплей UNIX-машины и ПК, можно определить иерархию классов DisplayManager:
class DisplayManager { ... };
class DisplayUNIX : public DisplayManager { ... };
class DisplayPC : public DisplayManager { ... };
Наш класс ZooAnimal не является разновидностью класса DisplayManager, но содержит экземпляр последнего посредством композиции, а не наследования. Возникает вопрос: использовать композицию по значению или по ссылке?
Композиция по значению не может представить объект DisplayManager, с помощью которого можно будет адресовать либо объект DisplayUNIX, либо объект DisplayPC. Только ссылка или указатель на объект DisplayManager позволят нам полиморфно манипулировать его подтипами. Иначе говоря, объектно-ориентированное программирование поддерживается только композицией по ссылке (подробнее см. [LIPPMAN96a].)
Теперь нужно решить, должен ли член класса ZooAnimal быть ссылкой или указателем на DisplayManager:
член может быть объявлен ссылкой лишь в том случае, если при создании объекта ZooAnimal имеется реальный объект DisplayManager, который не будет изменяться по ходу выполнения программы;
если применяется стратегия отложенного выделения памяти, когда память для объекта DisplayManager выделяется только при попытке вывести объект на дисплей, то объект следует представить указателем, инициализировав его значением 0;
если мы хотим переключать режим вывода во время выполнения, то тоже должны представить объект указателем, который инициализирован нулем. Под переключением
мы понимаем предоставление пользователю возможности выбрать один из подтипов DisplayManager в начале или в середине работы программы.
Конечно, маловероятно, что для каждого подобъекта ZooAnimal в нашем приложении будет нужен собственный подтип DisplayManager для отображения. Скорее всего мы ограничимся статическим членом в классе ZooAnimal, указывающим на объект DisplayManager.
Упражнение 18.6
Объясните, в каких случаях имеет место наследование типа, а в каких– наследование реализации:
(a) Queue : List // очередь : список
(b) EncryptedString : String // зашифрованная строка : строка
(c) Gif : FileFormat
(d) Circle : Point // окружность : точка
(e) Dqueue : Queue, List
(f) DrawableGeom : Geom, Canvas // рисуемая фигура : фигура, холст
Упражнение 18.7
Замените член IntArray в реализации PeekbackStack (см. раздел 18.3.1) на класс deque из стандартной библиотеки. Напишите небольшую программу для тестирования.
Упражнение 18.8
Сравните композицию по ссылке с композицией по значению, приведите примеры их использования.