Язык программирования C++. Вводный курс

         

Разрешение перегрузки при конкретизации A


В предыдущем разделе мы видели, что шаблон функции может быть перегружен. Кроме того, допускается использование одного и того же имени для шаблона и обычной функции:

// шаблон функции

template <class Type>

   Type sum( Type, int ) { /* ... */ }

// обычная функция (не шаблон)

double sum( double, double );

Когда программа обращается к sum(), вызов разрешается либо в пользу конкретизированного экземпляра шаблона, либо в пользу обычной функции – это зависит от того, какая функция лучше соответствует фактическим аргументам. (Для решения такой проблемы применяется процесс разрешения перегрузки, описанный в главе 9.) Рассмотрим следующий пример:

void calc( int ii, double dd ) {

   // что будет вызвано: конкретизированный экземпляр шаблона

   // или обычная функция?

   sum( dd, ii );

}

Будет ли при обращении к sum(dd,ii)

вызвана функция, конкретизированная из шаблона, или обычная функция? Чтобы ответить на этот вопрос, выполним по шагам процедуру разрешения перегрузки. Первый шаг заключается в построении множества функций-кандидатов состоящего из одноименных вызванной функций, объявления которых видны в точке вызова.

Если существует шаблон функции и на основе фактических аргументов вызова из него может быть конкретизирована функция, то она будет являться кандидатом. Так ли это на самом деле, зависит от результата процесса вывода аргументов шаблона. (Этот процесс описан в разделе 10.3.) В предыдущем примере для вывода значения аргумента Type

шаблона используется фактический аргумент функции dd. Тип выведенного аргумента оказывается равным double, и к множеству функций-кандидатов добавляется функция sum(double, int). Таким образом, для данного вызова имеются два кандидата: конкретизированная из шаблона функция sum(double, int) и обычная функция sum(double, double).

После того как функции, конкретизированные из шаблона, включены в множество кандидатов, процесс вывода аргументов шаблона продолжается как обычно.

Второй шаг процедуры разрешения перегрузки заключается в выборе устоявших функций из множества кандидатов. Напомним, что устоявшей называется функция, для которой существуют преобразования типов, приводящие каждый фактический аргумент функции к типу соответствующего формального параметра. (В разделе 9.3 описаны преобразования типов, применимые к фактическим аргументам функции.) Нужные трансформации существуют как для конкретизированной функции sum(double, int), так и для обычной функции sum(double, double). Следовательно, обе они являются устоявшими.


Проведем ранжирование преобразований типов, примененных к фактическим аргументам для выбора наилучшей из устоявших функций. В нашем примере оно происходит следующим образом:

Для конкретизированной из шаблона функции sum(double, int):

·         для первого фактического аргумента как сам этот аргумент, так и формальный параметр имеют тип double, т.е. мы видим точное соответствие;

·         для второго фактического аргумента как сам аргумент, так и формальный параметр имеют тип int, т.е. снова точное соответствие.

Для обычной функции sum(double, double):

·         для первого фактического аргумента как сам этот аргумент, так и формальный параметр имеют тип double – точное соответствие;

·         для второго фактического аргумента сам этот аргумент имеет тип int, а формальный параметр – тип double, т.е. необходимо стандартное преобразование между целым и плавающим типами.

Если рассматривать только первый аргумент, то обе функции одинаково хороши. Однако для второго аргумента конкретизированная из шаблона функция лучше. Поэтому наиболее подходящей (лучшей из устоявших) считается функция sum(double, int).

Функция, конкретизированная из шаблона, включается в множество кандидатов только тогда, когда процесс вывода аргументов завершается успешно. Неудачное завершение в данном случае не является ошибкой, но кандидатом функция считаться не будет. Предположим, что шаблон функции sum() объявлен следующим образом:



// шаблон функции

template <class T>
   int sum( T*, int ) { ... }

Для описанного вызова функции вывод аргументов шаблона будет неудачным, так как фактический аргумент типа double не может соответствовать формальному параметру типа T*. Поскольку для данного вызова и данного шаблона конкретизировать функцию невозможно, в множество кандидатов ничего не добавляется, т.е. единственным его элементом останется обычная функция sum(double, double). Именно она вызывается при обращении, и ее второй фактический аргумент приводится к типу double.



А если вывод аргументов шаблона завершается удачно, но для них есть явная специализация? Тогда именно она, а не функция, конкретизированная из обобщенного шаблона, попадает в множество кандидатов. Например:



// определение шаблона функции

template <class Type> Type sum( Type, int ) { /* ... */ }

// явная специализация для Type == double

template<> double sum<double>( double,int );

// обычная функция

double sum( double, double );

void manip( int ii, double dd ) {

   // вызывается явная специализация шаблона sum<double>()

   sum( dd, ii );
}

При обращении к sum()

внутри manip() в процессе вывода аргументов шаблона обнаруживается, что функция sum(double,int), конкретизированная из обобщенного шаблона, должна быть добавлена к множеству кандидатов. Но для нее имеется явная специализация, которая и становится кандидатом. На более поздних стадиях анализа выясняется, что эта специализация дает наилучшее соответствие фактическим аргументам вызова, так что разрешение перегрузки завершается в ее пользу.

Явные специализации шаблона не включаются в множество кандидатов автоматически. Лишь в том случае, когда вывод аргументов завершается успешно, компилятор будет рассматривать явные специализации данного шаблона:



// определение шаблона функции

template <class Type>

   Type min( Type, Type ) { /* ... */ }

// явная специализация для Type == double

template<> double min<double>( double, double );

void manip( int ii, double dd ) {

   // ошибка: вывод аргументов шаблона неудачен,

   // нет функций-кандидатов для данного вызова

   min( dd, ii );
}

Шаблон функции min()

специализирован для аргумента double. Однако эта специализация не попадает в множество функций-кандидатов. Процесс вывода для вызова min()

завершился неудачно, поскольку аргументы шаблона, выведенные для Type на основе разных фактических аргументов функции, оказались различными: для первого аргумента выводится тип double, а для второго – int. Поскольку вывести аргументы не удалось, в множество кандидатов никакая функция не добавляется, и специализация min(double, double)



игнорируется. Так как других функций-кандидатов нет, вызов считается ошибочным.

Как отмечалось в разделе 10.6, тип возвращаемого значения и список формальных параметров обычной функции может точно соответствовать аналогичным атрибутам функции, конкретизированной из шаблона. В следующем примере min(int,int) – это обычная функция, а не специализация шаблона min(), поскольку, как вы, вероятно, помните, объявление специализации должно начинаться с template<>:



// объявление шаблона функции

template <class T>

   T min( T, T );

// обычная функция min(int,int)
int min( int, int ) { }

Вызов может точно соответствовать как обычной функции, так и функции, конкретизированной из шаблона. В следующем примере оба аргумента в min(ai[0],99) имеют тип int. Для этого вызова есть две устоявших функции: обычная min(int,int) и конкретизированная из шаблона функция с тем же типом возвращаемого значения и списком параметров:



int ai[4] = { 22, 33, 44, 55 };

int main() {

   // вызывается обычная функция min( int, int )

   min( ai[0], 99 );
}

Однако такой вызов не является неоднозначным. Обычной функции, если она существует, всегда отдается предпочтение, поскольку она реализована явно, так что перегрузка разрешается в пользу обычной функции min(int,int).

Если перегрузка разрешилась таким образом, то изменений уже не будет: если позже обнаружится, что в программе нет определения этой функции, компилятор не станет конкретизировать ее тело из шаблона. Вместо этого на этапе сборки мы получим ошибку. В следующем примере программа вызывает, но не определяет обычную функцию min(int,int), и редактор связей выдает сообщение об ошибке:



// шаблон функции

template <class T>

   T min( T, T ) { ... }

// это обычная функция, не определенная в программе

int min( int, int );

int ai[4] = { 22, 33, 44, 55 };

int main() {

   // ошибка сборки: min( int, int ) не определена

   min( ai[0], 99 );
}

Зачем определять обычную функцию, если ее тип возвращаемого значения и список параметров соответствуют функции, конкретизированной из шаблона? Вспомните, что при вызове конкретизированной функции к ее фактическим аргументам в ходе вывода аргументов шаблона можно применять только ограниченное множество преобразований. Если же объявлена обычная функция, то для приведения типов аргументов допустимы любые трансформации, так как типы формальных параметров обычной функции фиксированы. Рассмотрим пример, показывающий, зачем может потребоваться объявить обычную функцию.



Предположим, что мы хотим определить специализацию шаблона функции min<int>(int,int). Нужно, чтобы именно эта функция вызывалась при обращении к min() с аргументами любых целых типов, пусть даже неодинаковых. Из-за ограничений, наложенных на преобразования типов, при передаче фактических аргументов разных типов функция min<int>(int,int) не будет конкретизирована из шаблона. Мы могли бы заставить компилятор выполнить конкретизацию, явно задав аргументы шаблона, однако решение, при котором не требуется модифицировать каждый вызов, предпочтительнее. Определив обычную функцию, мы добьемся того, что программа будет вызывать специальную версию min(int,int) для любых фактических аргументов целых типов без явного указания аргументов шаблона:



// определение шаблона функции

template <class Type>

   Type min( Type t1, Type t2 ) { ... }

int ai[4] = { 22, 33, 44, 55 };

short ss = 88;

void call_instantiation() {

   // ошибка: для этого вызова нет функции-кандидата

   min( ai[0], ss );

}

// обычная функция

int min( int a1, int a2 ) {

   min<int>( a1, a2 );

}

int main() {

   call_instantiation() {

   // вызывается обычная функция

   min( ai[0], ss );
}

Для вызова

min(ai[0],ss) из call_instantiation нет ни одной функции-кандидата. Попытка сгенерировать ее из шаблона min()

провалится, поскольку для аргумента шаблона Type из фактических аргументов функции выводятся два разных значения. Следовательно, такой вызов ошибочен. Однако при обращении к min(ai[0],ss)

внутри main()

видимо объявление обычной функции min(int, int). Тип первого фактического аргумента этой функции точно соответствует типу формального параметра, а второй аргумент может быть преобразован в тип формального параметра с помощью расширения типа. Поскольку для второго вызова устояла только данная функция, то она и вызывается.

Разобравшись с разрешением перегрузки функций, конкретизированных из шаблонов, специализацией шаблонов функций и обычных функций с тем же именем, подытожим все, что мы об этом рассказали:



1.      Построить множество функций-кандидатов.

Рассматриваются шаблоны функций с тем же именем, что и вызванная. Если аргументы шаблона выведены из фактических аргументов функции успешно, то в множество функций-кандидатов включается либо конкретизированный шаблон, либо специализация шаблона для выведенных аргументов, если она существует.

2.      Построить множество устоявших функций (см. раздел 9.3).

В множестве функций-кандидатов остаются только функции, которые можно вызвать с данными фактическими аргументами.

3.      Ранжировать преобразования типов (см. раздел 9.3).

a.   Если есть только одна функция, вызвать именно ее.

b.   Если вызов неоднозначен, удалить из множества устоявших функции, конкретизированные из шаблонов.

4.      Разрешить перегрузку, рассматривая среди всех устоявших только обычные функции (см. раздел 9.3).

a.   Если есть только одна функция, вызвать именно ее.

b.   В противном случае вызов неоднозначен.

Проиллюстрируем эти шаги на примере. Предположим, есть два объявления – шаблона функции и обычной функции. Оба принимают аргументы типа double:



template <class Type>

   Type max( Type, Type ) { ... }

// обычная функция
double max( double, double );

А вот три вызова max(). Можете ли вы сказать, какая функция будет вызвана в каждом случае?



int main() {

   int ival;

   double dval;

   float fd;

   // ival, dval и fd присваиваются значения

   max( 0, ival );

   max( 0.25, dval );

   max( 0, fd );
}

Рассмотрим последовательно все три вызова:

1.      max(0,ival). Оба аргумента имеют тип int. Для вызова есть два кандидата: конкретизированная из шаблона функция max(int, int) и обычная функция max(double, double). Конкретизированная функция точно соответствует фактическим аргументам, поэтому она и вызывается;

2.      max(0.25,double). Оба аргумента имеют тип double. Для вызова есть два кандидата: конкретизированная из шаблона max(double, double) и обычная max(double, double). Вызов неоднозначен, поскольку точно соответствует обеим функциям. Правило 3b говорит, что в таком случае выбирается обычная функция;.



3.      max(0,fd). Аргументы имеют тип int и float

соответственно. Для вызова существует только один кандидат: обычная функция max(double, double). Вывод аргументов шаблона заканчивается неудачей, так как значения типа Type, выведенные из разных фактических аргументов функции, различны. Поэтому в множество кандидатов конкретизированная из шаблона функция не попадает. Обычная же функция устояла, поскольку существуют преобразования типов фактических аргументов в типы формальных параметров; она и выбирается. Если бы обычная функция не была объявлена, вызов закончился бы ошибкой.

А если бы мы определили еще одну обычную функцию для max()? Например:



template <class T> T max( T, T ) { ... }

// две обычные функции

char max( char, char );
double max( double, double );

Будет ли в таком случае третий вызов разрешен по-другому? Да.



int main() {

   float fd;

   // в пользу какой функции разрешается вызов?

   max( 0, fd );
}

Правило 3b говорит, что, поскольку вызов неоднозначен, следует рассматривать только обычные функции. Ни одна из них не считается наилучшей из устоявших, так как преобразования типов фактических аргументов одинаково плохи: в обоих случаях для установления соответствия требуется стандартная трансформация. Таким образом, вызов неоднозначен, и компилятор сообщает об ошибке.

Упражнение 10.11

Вернемся к представленному ранее примеру:



template <class Type>

   Type max( Type, Type ) { ... }

double max( double, double );



int main() {

   int ival;

   double dval;

   float fd;

   max( 0, ival );

   max( 0.25, dval );

   max( 0, fd );

}

Добавим в множество объявлений в глобальной области видимости следующую специализацию шаблона функции:

template <> char max<char>* char, char ) { ... }

Составьте список кандидатов и устоявших функций для каждого вызова max()

внутри main().

Предположим, что в main()

добавлен следующий вызов:



int main() {

   // ...

   max( 0, 'j' );
<


}

В пользу какой функции он будет разрешен? Почему?

Упражнение 10.12

Предположим, что есть следующее множество определений и специализаций шаблонов, а также объявления переменных и функций:



int i;             unsigned int ui;

char str[24];      int ia[24];

template <class T> T calc( T*, int );

template <class T> T calc( T, T );

template<> chat calc( char*. int );
double calc( double, double );

Выясните, какая функция или конкретизированный шаблон вызывается в каждом из показанных ниже случаев. Для каждого вызова перечислите функции-кандидаты и устоявшие функции; объясните, какая из устоявших функций будет наилучшей.



(a) cslc( str, 24 );        (d) calc( i, ui );

(b) calc( is, 24 );         (e) calc( ia, ui );
(c) calc( ia[0], 1 );       (f) calc( &i, i );


Реализация объекта-функции


При реализации программы в разделе 12.2 нам уже приходилось определять ряд объектов-функций. В этом разделе мы изучим необходимые шаги и возможные вариации при определении класса объекта-функции. (В главе 13 определение класса рассматривается детально; в главе 15 обсуждается перегрузка операторов.)

В самой простой форме определение класса объекта-функции сводится к перегрузке оператора вызова. Вот, например, унарный объект-функция, определяющий, что некоторое значение меньше или равно 10:

// простейшая форма класса

объекта-функции

class less_equal_ten {

public:

   bool operator() ( int val )

        { return val <= 10; }

};

Теперь такой объект-функцию можно использовать точно так же, как предопределенный. Вызов алгоритма count_if() с помощью нашего объекта-функции выглядит следующим образом:

count_if( vec.begin(), vec.end(), less_equal_ten() );

Разумеется, возможности этого класса весьма ограничены. Попробуем применить отрицатель, чтобы подсчитать, сколько в контейнере элементов, больших 10:

count_if( vec.begin(), vec.end(),

          not1(less_equal_then ()));

или обобщить реализацию, разрешив пользователю задавать значение, с которым надо сравнивать каждый элемент контейнера. Для этого достаточно ввести в класс член для хранения такого значения и реализовать конструктор, инициализирующий данный член указанной пользователем величиной:

class less_equal_value {

public:

   less_equal_value( int val ) : _val( val ) {}

   bool operator() ( int val ) { return val <= _val; }

private:

   int _val;

};

Новый объект-функция применяется для задания произвольного целого значения. Например, при следующем вызове подсчитывается число элементов, меньших или равных 25:

count_if( vec.begin(), vec.end(), less_equal_value( 25 ));

Разрешается реализовать класс и без конструктора, если параметризовать его значением, с которым производится сравнение:

template < int _val >

class less_equal_value {

public:

   bool operator() ( int val ) { return val <= _val; }

};

Вот как надо было бы вызвать такой класс для подсчета числа элементов, меньших или равных 25:

count_if( vec.begin(), vec.end(), less_equal_value<25>());

(Другие примеры определения собственных объектов-функций можно найти в Приложении.)

Упражнение 12.4

Используя предопределенные объекты-функции и адаптеры, создайте объекты-функции для решения следующих задач:

(a)    Найти все значения, большие или равные 1024.

(b)   Найти все строки, не равные "pooh".

(c)    Умножить все значения на 2.

Упражнение 12.5

Определите объект-функцию для возврата среднего из трех объектов. Определите функцию для выполнения той же операции. Приведите примеры использования каждого объекта непосредственно и путем передачи его функции. Покажите, в чем сходство и различие этих решений.



Регистровые автоматические объекты


Автоматические объекты, интенсивно используемые в функции, можно объявить с ключевым словом register, тогда компилятор будет их загружать в машинные регистры. Если же это невозможно, объекты останутся в основной памяти. Индексы массивов и указатели, встречающиеся в циклах, – хорошие кандидаты в регистровые объекты.

for ( register int ix =0; ix < sz; ++-ix ) // ...

for ( register int *p = array ; p < arraySize; ++p ) // ...

Параметры также можно объявлять как регистровые переменные:

bool find( register int *pm, int Val ) {

    while ( *pm )

        if ( *pm++ == Val ) return true;

    return false;

}

Их активное использование может заметно увеличить скорость выполнения функции.

Указание ключевого слова register – только подсказка компилятору. Некоторые компиляторы игнорируют такой запрос, применяя специальные алгоритмы для определения наиболее подходящих кандидатов на размещение в свободных регистрах.

Поскольку компилятор учитывает архитектуру машины, на которой будет выполняться программа, он зачастую может принять более обоснованное решение об использовании машинных регистров.



Рекурсия


Функция, которая прямо или косвенно вызывает сама себя, называется рекурсивной. Например:

int rgcd( int vl, int v2 )

{

    if ( v2 != 0 )

        return rgcd( v2, vl%v2 );

    return vl;

}

Такая функция обязательно должна определять условие окончания, в противном случае рекурсия будет продолжаться бесконечно. Подобную ошибку так иногда и называют– бесконечная рекурсия. Для rgcd()

условием окончания является равенство нулю остатка.

Вызов

rgcd( 15, 123 );

возвращает 3 (см. табл. 7.1).

Таблица 7.1. Трассировка вызова rgcd (15,123)

vl

v2

return

15

123

rgcd(123,15)

123

15

rgcd(15,3)

15

3

rgcd(3,0)

3

0

3

Последний вызов,

rgcd(3,0);

удовлетворяет условию окончания. Функция возвращает наибольший общий делитель, он же возвращается и каждым предшествующим вызовом. Говорят, что значение всплывает (percolates) вверх, пока управление не вернется в функцию, вызвавшую rgcd() в первый раз.

Рекурсивные функции обычно выполняются медленнее, чем их нерекурсивные (итеративные) аналоги. Это связано с затратами времени на вызов функции. Однако, как правило, они компактнее и понятнее.

Приведем пример. Факториалом числа n

является произведение натуральных чисел от 1 до n. Так, факториал 5

равен 120: 1 ´ 2 ´ 3 ´ 4 ´ 5 = 120.

Вычислять факториал удобно с помощью рекурсивной функции:

unsigned long

factorial( int val ) {

    if ( val > 1 )

        return val * factorial( val-1 );

    return 1;

}

Рекурсия обрывается по достижении val значения 1.

Упражнение 7.12

Перепишите factorial() как итеративную функцию.

Упражнение 7.13

Что произойдет, если условием окончания factorial() будет следующее:

if ( val != 0 )



Решение задачи


Программы обычно пишутся для того, чтобы решить какую-то конкретную задачу. Например, книжный магазин ведет запись проданных книг. Регистрируется название книги и издательство, причем запись идет в том порядке, в каком книги продаются. Каждые две недели владелец магазина вручную подсчитывает количество проданных книг с одинаковым названием и количество проданных книг от каждого издателя. Этот список сортируется по издателям и используется для составления последующего заказа книг. Нас попросили написать программу для автоматизации этой деятельности.

Один из методов решения большой задачи состоит в разбиении ее на ряд задач поменьше. В идеале, с маленькими задачами легче справиться, а вместе они помогают одолеть большую. Если подзадачи все еще слишком сложны, мы, в свою очередь, разобьем их на еще меньшие, пока каждая из подзадач не будет решена. Такую стратегию называют пошаговой детализацией или принципом “разделяй и властвуй”. Задача книжного магазина делится на четыре подзадачи:

Прочитать файл с записями о продажах.

Подсчитать количество продаж по названиям и по издателям.

Отсортировать записи по издателям.

Вывести результаты.

Решения для подзадач 1, 2 и 4 известны, их не нужно делить на более мелкие подзадачи. А вот третья подзадача все еще слишком сложна. Будем дробить ее дальше.

3a.       Отсортировать записи по издателям.

3b.       Для каждого издателя отсортировать записи по названиям.

3c.       Сравнить соседние записи в группе каждого издателя. Для каждой одинаковой пары увеличить счетчик для первой записи и удалить вторую.

Эти подзадачи решаются легко. Теперь мы знаем, как решить исходную, большую задачу. Более того, мы видим, что первоначальный список подзадач был не совсем правильным. Правильная последовательность действий такова:

Прочитать файл с записями о продажах.

Отсортировать этот файл: сначала по издателям, внутри каждого издателя– по названиям.

Удалить повторяющиеся названия, наращивая счетчик.

Вывести результат в новый файл.

Результирующая последовательность действий называется алгоритмом. Следующий шаг – перевести наш алгоритм на некоторый язык программирования, в нашем случае – на С++.



Открытый интерфейс каждого из четырех


Открытый интерфейс каждого из четырех производных классов состоит из их открытых членов и унаследованных открытых членов Query. Когда мы пишем:
Query *pq = new NmaeQuery( "Monet" );
то получить доступ к открытому интерфейсу Query
можно только через pq. А если пишем:
pq->eval();
то вызывается реализация виртуальной eval() из производного класса, на объект которого указывает pq, в данном случае – из класса NameQuery. Строкой
pq->display();
всегда вызывается невиртуальная функция display() из Query. Однако она выводит разрешающее множество строк объекта того производного класса, на который указывает pq. В этом случае мы не стали полагаться на механизм виртуализации, а вынесли разделяемую операцию и необходимые для нее данные в общий абстрактный базовый класс Query. display() – это пример полиморфного программирования, которое поддерживается не виртуальностью, а исключительно с помощью  наследования. Вот ее реализация (это пока только промежуточное решение, как мы увидим в последнем разделе):


void
Query::
display()
{
   if ( ! _solution->size() ) {
      cout << "\n\tИзвините, "
           << " подходящих строк в тексте не найдено.\n"
           << endl;
   }
   set<short>::const_iterator
       it = _solution->begin(),
       end_it = _solution->end();
   for ( ; it != end_it; ++it ) {
      int line = *it;
      // не будем пользоваться нумерацией строк с 0...
      cout << "(" << line+1 << " ) "
           << (*_text_file)[line] << '\n';
   }
   cout << endl;

}

Упражнение 17.3
Рассмотрите приведенные члены иерархии классов для поддержки библиотеки из упражнения 17.1 (раздел 17.1). Выявите возможные кандидаты на роль виртуальных функций, а также те члены, которые являются общими для всех предметов, выдаваемых библиотекой, и, следовательно, могут быть представлены в базовом классе. (Примечание: LibMember – это абстракция человека, которому разрешено брать из библиотеки различные предметы; Date – класс, представляющий календарную дату.)


class Library
{
public:
   bool check_out( LibMember* );   // выдать
   bool check_in ( LibMember* );   // принять назад
   bool is_late( const Date& today );  // просрочил
   double apply_fine();                // наложить штраф
   ostream& print( ostream&=cout );
   Date* due_date() const;             // ожидаемая дата возврата
   Date* date_borrowed() const;        // дата выдачи
   string title() const;               // название
   const LibMember* member() const;    // записавшийся

};
Упражнение 17.4
Идентифицируйте члены базового и производных классов для той иерархии, которую вы выбрали в упражнении 17.2 (раздел 17.1). Задайте виртуальные функции, а также открытые и защищенные члены.
Упражнение 17.5
Какие из следующих объявлений неправильны:


class base { ... };
(a) class Derived : public Derived { ... };
(b) class Derived : Base { ... };
(c) class Derived : private Base { ... };
(d) class Derived : public Base;

(e) class Derived inherits Base { ... };

В этом разделе мы попытались определить иерархию классов Query. Однако вопрос о том, как же построить с ее помощью структуру данных, описывающую запрос пользователя, остался без ответа. Когда мы приступим к реализации, это определение придется пересмотреть и расширить. Но прежде нам предстоит более детально изучить механизм наследования в языке C++.

Шаблон auto_ptr А


В стандартной библиотеке С++ auto_ptr

является шаблоном класса, призванным помочь программистам в манипулировании объектами, которые создаются посредством оператора new. (К сожалению, подобного шаблона для манипулирования динамическими массивами нет. Использовать auto_ptr для создания массивов нельзя, это приведет к непредсказуемым результатам.)

Объект auto_ptr

инициализируется адресом динамического объекта, созданного с помощью оператора new. Такой объект автоматически уничтожается, когда заканчивается время жизни auto_ptr. В этом подразделе мы расскажем, как ассоциировать auto_ptr с динамически размещаемыми объектами.

Для использования шаблона класса auto_ptr необходимо включить заголовочный файл:

#include <memory>

Определение объекта auto_ptr имеет три формы:

auto_ptr< type_pointed_to > identifier( ptr_allocated_by_new );

auto_ptr< type_pointed_to > identifier( auto_ptr_of_same_type );

auto_ptr< type_pointed_to > identifier;

Здесь type_pointed_to

представляет собой тип нужного объекта. Рассмотрим последовательно каждое из этих определений. Как правило, мы хотим непосредственно инициализировать объект auto_ptr

адресом объекта, созданного с помощью оператора new. Это можно сделать следующим образом:

auto_ptr< int > pi ( new int( 1024 ) );

В результате значением

pi

является адрес созданного объекта, инициализированного числом 1024. С объектом, на который указывает auto_ptr, можно работать обычным способом:

if ( *pi != 1024 )

    // ошибка, что-то не так

else *pi *= 2;

Объект, на который указывает pi, будет автоматически уничтожен по окончании времени жизни pi. Если указатель pi

является локальным, то объект, который он адресует, будет уничтожен при выходе из блока, где он определен. Если же pi глобальный, то объект, на который он ссылается, уничтожается при выходе из программы.

Что будет, если мы инициализируем auto_ptr

адресом объекта класса, скажем, стандартного класса string? Например:

auto_ptr< string >

<
    pstr_auto( new string( "Brontosaurus" ) );

Предположим, что мы хотим выполнить какую-то операцию со строками. С обычной строкой мы бы поступили таким образом:



string *pstr_type = new string( "Brontosaurus" );

if ( pstr_type->empty() )
    // ошибка, что-то не так

А как обратиться к операции empty(), используя объект auto_ptr? Точно так же:



auto_ptr< string > pstr_auto( new     string( "Brontosaurus" ) );

if ( pstr_type->empty() )
    // ошибка, что-то не так

Создатели шаблона класса auto_ptr не в последнюю очередь стремились сохранить привычный синтаксис, употребляемый с обычными указателями, а также обеспечить дополнительные возможности автоматического удаления объекта, на который ссылается auto_ptr. При этом время выполнения не увеличивается. Применение встроенных функций (которые подставляются по месту вызова) позволило сделать использование объекта auto_ptr

немногим более дорогим, чем непосредственное употребление указателя.

Что произойдет, если мы проинициализируем pstr_auto2 значением pstr_auto, который является объектом auto_ptr, указывающим на строку?



// кто несет ответственность за уничтожение строки?
auto_ptr< string > pstr_auto2( pstr_auto );

Представим, что мы непосредственно инициализировали один указатель на строку другим:

string *pstr_type2( pstr_type );

Оба указателя теперь содержат адрес одной и той же строки, и мы должны быть внимательными, чтобы не удалить строку дважды.

В противоположность этому шаблон класса auto_ptr

поддерживает понятие владения. Когда мы определили pstr_auto, он стал владельцем строки, адресом которой был инициализирован, и принял на себя ответственность за ее уничтожение.

Вопрос в том, кто станет владельцем строки, когда мы инициализируем pstr_auto2

адресом, указывающим на тот же объект, что и pstr_auto?

Нежелательно, чтобы оба объекта владели одной и той же строкой: это вернет нас к проблемам повторного удаления, от которых мы стремились уйти с помощью шаблона класса auto_ptr.



Когда один объект auto_ptr

инициализируется другим или получает его значение в результате присваивания, одновременно он получает и право владения адресуемым объектом. Объект auto_ptr, стоящий справа от оператора присваивания, передает право владения и ответственность auto_ptr, стоящему слева. В нашем примере ответственность за уничтожение строки несет pstr_auto2, а не pstr_auto. pstr_auto больше не может употребляться для ссылки на эту строку.

Аналогично ведет себя и операция присваивания. Пусть у нас есть два объекта auto_ptr:



auto_ptr< int > p1( new int( 1024 ) );
auto_ptr< int > p2( new int( 2048 ) );

Мы можем скопировать один объекта auto_ptr в другой с помощью этой операции:

p1 = p2;

Перед присваиванием объект, на который ссылался p1, удаляется.

После присваивания p1

владеет объектом типа int со значением 2048. p2

больше не может использоваться как ссылка на этот объект.

Третья форма определения объекта auto_ptr

создает его, но не инициализирует значением указателя на область памяти из хипа. Например:



// пока не ссылается ни на какой объект
auto_ptr< int > p_auto_int;

Поскольку p_auto_int не инициализирован адресом какого-либо объекта, значение хранящегося внутри него указателя равно 0. Разыменование таких указателей приводит к непредсказуемому поведению программы:



// ошибка: разыменование нулевого указателя

if ( *p_auto_int != 1024 )
    *p_auto_int = 1024;

Обычный указатель можно проверить на равенство 0:



int *pi = 0;
if ( pi ! = 0 ) ...;

А как проверить, адресует auto_ptr

какой-либо объект или нет? Операция get() возвращает внутренний указатель, использующийся в объекте auto_ptr. Значит, мы должны применить следующую проверку:



// проверяем, указывает ли p_auto_int на объект

if ( p_auto_int.get() != 0 &&

    *p_auto_int != 1024 )
      *p_auto_int = 1024;

Если auto_ptr ни на что не указывает, то как заставить его адресовать что-либо? Другими словами, как мы можем присвоить значение внутреннему указателю объекта auto_ptr? Это делается с помощью операции reset(). Например:





else

   // хорошо, присвоим ему значение
   p_auto_int.reset( new int( 1024 ) );

Объекту auto_ptr

нельзя присвоить адрес объекта, созданного с помощью оператора new:



void example() {

    // инициализируется нулем по умолчанию

    auto_ptr< int > pi;

    {

        // не поддерживается

        pi = new int( 5 ) ;

    }
}

В этом случае надо использовать функцию reset(), которой можно передать указатель или 0, если мы хотим обнулить объект auto_ptr. Если auto_ptr

указывает на объект и является его владельцем, то этот объект уничтожается перед присваиванием нового значения внутреннему указателю auto_ptr. Например:



auto_ptr< string >

    pstr_auto( new string( "Brontosaurus" ) );

// "Brontosaurus" уничтожается перед присваиванием
pstr_auto.reset( new string( "Long-neck" ) );

В последнем случае лучше, используя операцию assign(), присвоить новое значение существующей строке, чем уничтожать одну строку и создавать другую:



// более эффективный способ присвоить новое значение

// используем операцию assign()
pstr_auto->assign( "Long-neck" );

Одна из трудностей программирования состоит в том, что получить правильный результат не всегда достаточно. Иногда накладываются и временные ограничения. Такая мелочь, как удаление и создание заново строкового объекта, вместо использования функции assign() при определенных обстоятельствах может вызвать значительное замедление работы. Подобные детали не должны вас беспокоить при проектировании, но при доводке программы на них следует обращать внимание.

Шаблон класса auto_ptr

обеспечивает значительные удобства и безопасность использования динамически выделяемой памяти. Однако все равно надо не терять бдительности, чтобы не навлечь на себя неприятности:

·                  нельзя инициализировать объект auto_ptr

указателем, полученным не с помощью оператора new, или присвоить ему такое значение. В противном случае после применения к этому объекту оператора delete



поведение программы непредсказуемо;

·                  два объекта auto_ptr не должны получать во владение один и тот же объект. Очевидный способ допустить такую ошибку – присвоить одно значение двум объектам. Менее очевидный – с помощью операции get(). Вот пример:



auto_ptr< string >

    pstr_auto( new string( "Brontosaurus" ) );

// ошибка: теперь оба указывают на один объект

// и оба являются его владельцами
auto_ptr< string > pstr_auto2( pstr_auto.get() );

Операция release()

гарантирует, что несколько указателей не являются владельцами одного и того же объекта. release() не только возвращает адрес объекта, на который ссылается auto_ptr, но и передает владение им. Предыдущий фрагмент кода нужно переписать так:



// правильно: оба указывают на один объект,

// но pstr_auto больше не является его владельцем

auto_ptr< string >
pstr_auto2( pstr_auto.release() );


Шаблон класса Array


В этом разделе мы завершим реализацию шаблона класса Array, введенного в разделе 2.5 (этот шаблон будет распространен на одиночное наследование в разделе 18.3 и на множественное наследование в разделе 18.6). Так выглядит полный заголовочный файл:

#ifndef ARRAY_H

#define ARRAY_H

#include <iostream>

template <class elemType> class Array;

template <class elemType> ostream&

   operator<<( ostream &, Array<elemType> & );

template <class elemType>

class Array {

public:

   explicit Array( int sz = DefaultArraySize )

      { init( 0, sz ); }

   Array( const elemType *ar, int sz )

      { init( ar, sz ); }

   Array( const Array &iA )

      { init( iA._ia, iA._size ); }

   ~Array() { delete[] _ia; }

   Array & operator=( const Array & );

   int size() const { return _size; }

   elemType& operator[]( int ix ) const

      { return _ia[ix]; }

   ostream &print( ostream& os = cout ) const;

   void grow();

   void sort( int,int );

   int find( elemType );

   elemType min();

   elemType max();

private:

   void init( const elemType*, int );

   void swap( int, int );

   static const int DefaultArraySize = 12;

   int _size;

   elemType *_ia;

};

#endif

Код, общий для реализации всех трех конструкторов, вынесен в отдельную функцию-член init(). Поскольку она  не должна напрямую вызываться пользователями шаблона класса Array, мы поместили ее в закрытую секцию:

template <class elemType>

   void Array<elemType>::init( const elemType *array, int sz )

{

   _size = sz;

   _ia = new elemType[ _size ];

   for ( int ix = 0; ix < _size; ++ix )

      if ( ! array )

         _ia[ ix ] = 0;

      else _ia[ ix ] = array[ ix ];

}

Реализация копирующего оператора присваивания не вызывает затруднений. Как отмечалось в разделе 14.7, в код включена защита от копирования объекта в самого себя:




template <class elemType> Array<elemType>&

   Array<elemType>::operator=( const Array<elemType> &iA )

{

   if ( this != &iA ) {

      delete[] _ia;

      init( iA._ia, iA._size );

   }

   return *this;
}

Функция-член print()

отвечает за вывод объекта того типа, которым конкретизирован шаблон Array. Возможно, реализация несколько сложнее, чем необходимо, зато данные аккуратно размещаются на странице. Если экземпляр конкретизированного класса Array<int>

содержит элементы 3, 5, 8, 13 и 21, то выведены они будут так:

(5) < 3, 5, 8, 13, 21 >

Оператор потокового вывода просто вызывает print(). Ниже приведена реализация обеих функций:



template <class elemType> ostream&

   operator<<( ostream &os, Array<elemType> &ar )

{

   return ar.print( os );

}

template <class elemType>

   ostream & Array<elemType>::print( ostream &os ) const

{

   const int lineLength = 12;

   os << "( " << _size << " )< ";

   for ( int ix = 0; ix < _size; ++ix )

   {

      if ( ix % lineLength == 0 && ix )

         os << "\n\t";

      os << _ia[ ix ];

      // не выводить запятую за последним элементом в строке,

      // а также за последним элементом массива

      if ( ix % lineLength != lineLength-1 && ix != _size-1 )

         os << ", ";

   }

   os << " >\n";

   return os;
}

Вывод значения элемента массива в функции print() осуществляет такая инструкция:

os << _ia[ ix ];

Для ее правильной работы должно выполняться требование к типам, которыми конкретизируется шаблон Array: такой тип должен быть встроенным либо иметь собственный оператор вывода. В противном случае любая попытка распечатать содержимое класса Array

приведет к ошибке компиляции в том месте, где используется несуществующий оператор.

Функция-член grow() увеличивает размер объекта класса Array. В нашем примере – в полтора раза:





template <class elemType>

    void Array<elemType>::grow()

{

    elemType *oldia = _ia;

    int oldSize = _size;

    _size = oldSize + oldSize/2 + 1;

    _ia   = new elemType[_size];

    int ix;

    for ( ix = 0; ix < oldSize; ++ix )

          _ia[ix] = oldia[ix];

    for ( ; ix < _size; ++ix )

          _ia[ix] = elemType();

    delete[] oldia;
}

Функции-члены find(), min() и max() осуществляют последовательный поиск во внутреннем массиве _ia. Если бы массив был отсортирован, то, конечно, их можно было бы реализовать гораздо эффективнее.



template <class elemType>

    elemType Array<elemType>::min( )

{

    assert( _ia != 0 );

    elemType min_val = _ia[0];

    for ( int ix = 1; ix < _size; ++ix )

       if ( _ia[ix] < min_val )

          min_val = _ia[ix];

    return min_val;

}

template <class elemType>

    elemType Array<elemType>::max()

{

    assert( _ia != 0 );

    elemType max_val = _ia[0];

    for ( int ix = 1; ix < _size; ++ix )

       if ( max_val < _ia[ix] )

          max_val = _ia[ix];

    return max_val;

}

template <class elemType>

    int Array<elemType>::find( elemType val )

{

    for ( int ix = 0; ix < _size; ++ix )

       if ( val == _ia[ix] )

          return ix;

    return -1;
}

В шаблоне класса Array

есть функция-член sort(), реализованная с помощью алгоритма быстрой сортировки. Она очень похожа на шаблон функции, представленный в разделе 10.11. Функция-член swap() – вспомогательная утилита для sort(); она не является частью открытого интерфейса шаблона и потому помещена в закрытую секцию:



template <class elemType>

    void Array<elemType>::swap( int i, int j )

{

     elemType tmp = _ia[i];

     _ia[i] = _ia[j];

     _ia[j] = tmp;

}

template <class elemType>

    void Array<elemType>::sort( int low, int high )

{

    if ( low >= high ) return;

    int lo = low;

    int hi = high + 1;

    elemType elem = _ia[low];

    for ( ;; ) {

         while ( _ia[++lo] < elem ) ;

         while ( _ia[--hi] > elem ) ;

         if ( lo < hi )

              swap( lo,hi );

         else break;

    }

    swap( low, hi );

    sort( low, hi-1 );

    sort( hi+1, high );
<


}

То, что код реализован, разумеется, не означает, что он работоспособен. try_array() – это шаблон функции, предназначенный для тестирования реализации шаблона Array:



#include "Array.h"

template <class elemType>

    void try_array( Array<elemType> &iA )

{

    cout << "try_array: начальные значения массива\n";

    cout << iA << endl;

    elemType find_val = iA [ iA.size()-1 ];

    iA[ iA.size()-1 ] = iA.min();

    int mid = iA.size()/2;

    iA[0] = iA.max();

    iA[mid] = iA[0];

    cout << "try_array: после присваиваний\n";

    cout << iA << endl;

    Array<elemType> iA2 = iA;

    iA2[mid/2] = iA2[mid];

    cout << "try_array: почленная инициализация\n";

    cout << iA << endl;

    iA = iA2;

    cout << "try_array: после почленного копирования\n";

    cout << iA << endl;

    iA.grow();

    cout << "try_array: после вызова grow\n";

    cout << iA << endl;

    int index = iA.find( find_val );

    cout << "искомое значение: " << find_val;

    cout << "\tвозвращенный индекс: " << index << endl;

    elemType value = iA[index];

    cout << "значение элемента с этим индексом: ";

    cout << value << endl;
}

Рассмотрим шаблон функции try_array(). На первом шаге печатается исходный объект Array, что подтверждает успешную конкретизацию оператора вывода шаблона, а заодно дает начальную картину, с которой можно будет сверяться при последующих модификациях. В переменной find_val хранится значение, которое мы впоследствии передадим find(). Если бы try_array()

была обычной функцией, роль такого значения сыграла бы константа. Но поскольку никакая константа не может обслужить все типы, которыми допустимо конкретизировать шаблон, то приходится выбирать другой путь. Далее одним элементам Array



случайным образом присваиваются значения других элементов, чтобы протестировать min(), max(), size() и, конечно, оператор взятия индекса.

Затем объект iA2

почленно инициализируется объектом iA, что приводит к вызову копирующего конструктора. После этого тестируется оператор взятия индекса с объектом ia2: производится присваивание элементу с индексом mid/2. (Эти две строки представляют интерес в случае, когда iA – производный подтип Array, а оператор взятия индекса объявлен виртуальной функцией. Мы вернемся к этому в главе 18 при обсуждении наследования.) Далее в iA

почленно копируется модифицированный объект iA2, что приводит к вызову копирующего оператора присваивания класса Array. Затем проверяются функции-члены grow() и find(). Напомним, что find()

возвращает значение –1, если искомый элемент не найден. Попытка выбрать из “массива” Array

элемент с индексом –1 приведет к выходу за левую границу. (В главе 18 для перехвата этой ошибки мы построим производный от Array

класс, который будет проверять выход за границы массива.)

Убедиться, что наша реализация шаблона работает для различных типов данных, например целых чисел, чисел с плавающей точкой и строк, поможет программа main(), которая вызывает try_array() с каждым из указанных типов:



#include "Array.C"

#include "try_array.C"

#include <string>

int main()

{

    static int ia[] = { 12,7,14,9,128,17,6,3,27,5 };

    static double da[] = { 12.3,7.9,14.6,9.8,128.0 };

    static string sa[] = {

        "Eeyore", "Pooh", "Tigger",

        "Piglet", "Owl", "Gopher", "Heffalump"

    };

    Array<int>    iA( ia, sizeof(ia)/sizeof(int) );

    Array<double> dA( da, sizeof(da)/sizeof(double) );

    Array<string> sA( sa, sizeof(sa)/sizeof(string) );

    cout << "template Array<int> class\n" << endl;

    try_array(iA);

    cout << "template Array<double> class\n" << endl;

    try_array(dA);

    cout << "template Array<string> class\n" << endl;

    try_array(sA);

    return 0;
<


}

Вот что программа выводит при конкретизации шаблона Array типом double:

try_array: начальные значения массива

( 5 )< 12.3, 7.9, 14.6, 9.8, 128 >

try_array: после присваиваний

( 5 )< 14.6, 7.9, 14.6, 9.8, 7.9 >

try_array: почленная инициализация

( 5 )< 14.6, 7.9, 14.6, 9.8, 7.9 >

try_array: после почленного копирования

( 5 )< 14.6, 14.6, 14.6, 9.8, 7.9 >

try_array: после вызова grow

( 8 )< 14.6, 14.6, 14.6, 9.8, 7.9, 0, 0, 0 >

искомое значение: 128      возвращенный индекс: -1

значение элемента с этим индексом: 3.35965e-322

Выход индекса за границу массива приводит к тому, что последнее напечатанное программой значение неверно. Конкретизация шаблона Array типом string

заканчивается крахом программы:

template Array<string> class

try_array: начальные значения массива

( 7 )< Eeyore, Pooh, Tigger, Piglet, Owl, Gopher, Heffalump >

try_array: после присваиваний

( 7 )< Tigger, Pooh, Tigger, Tigger, Owl, Gopher, Eeyore >

try_array: почленная инициализация

( 7 )< Tigger, Pooh, Tigger, Tigger, Owl, Gopher, Eeyore >

try_array: после почленного копирования

( 7 )< Tigger, Tigger, Tigger, Tigger, Owl, Gopher, Eeyore >

try_array: после вызова grow

( 11 )< Tigger, Tigger, Tigger, Tigger, Owl, Gopher, Eeyore, <пусто>, <пусто>, <пусто>, <пусто> >

искомое значение: Heffalump           возвращенный индекс: -1

Memory fault (coredump)

Упражнение 16.11

Измените шаблон класса Array, убрав из него функции-члены sort(), find(), max(), min() и swap(), и модифицируйте шаблон try_array()

так, чтобы она вместо них пользовалась обобщенными алгоритмами (см. главу 12).

Часть V

Объектно-ориентированное программирование

Объектно-ориентированное программирование расширяет объектное программирование, вводя отношения тип-подтип с помощью механизма, именуемого наследованием. Вместо того чтобы заново реализовывать общие свойства, класс наследует данные-члены и функции-члены родительского класса. В языке C++ наследование осуществляется посредством так называемого порождения производных классов. Класс, свойства которого наследуются, называется базовым, а новый класс – производным. Все множество базовых и производных классов образует иерархию наследования.



Например, в трехмерной компьютерной графике классы OrthographicCamera и PerspectiveCamera

обычно являются производными от базового Camera. Множество операций и данных, общее для всех камер, определено в абстрактном классе Camera. Каждый производный от него класс реализует лишь отличия от абстрактной камеры, предоставляя альтернативный код для унаследованных функций-членов либо вводя дополнительные члены.

Если базовый и производный классы имеют общий открытый интерфейс, то производный называется подтипом базового. Так, PerspectiveCamera

является подтипом класса Camera. В C++ существует специальное отношение между типом и подтипом, позволяющее указателю или ссылке на базовый класс адресовать любой из производных от него подтипов без вмешательства программиста. (Такая возможность манипулировать несколькими типами с помощью указателя или ссылки на базовый класс называется полиморфизмом.) Если дана функция:

void lookAt( const Camera *pCamera );

то мы реализуем lookAt(), программируя интерфейс базового класса Camera и не заботясь о том, на что указывает pCamera: на объект класса PerspectiveCamera, на объект класса OrthographicCamera или на объект, описывающий еще какой-то вид камеры, который мы пока не определили.

При каждом вызове lookAt() ей передается адрес объекта, принадлежащего к одному из подтипов Camera. Компилятор автоматически преобразует его в указатель на подходящий базовый класс:



// правильно: автоматически преобразуется в Camera*

OrthographicCamera ocam;

lookAt( &ocam );

// ...

// правильно: автоматически преобразуется в Camera*

PerspectiveCamera *pcam = new PerspectiveCamera;
lookAt( pcam );

Наша реализация lookAt() не зависит от набора подтипов класса Camera, реально существующих в приложении. Если впоследствии потребуется добавить новый подтип или исключить существующий, то изменять реализацию lookAt() не придется.

Полиморфизм подтипов позволяет написать ядро приложения так, что оно не будет зависеть от конкретных типов, которыми мы манипулируем. Мы программируем открытый интерфейс базового класса придуманной нами абстракции, пользуясь только ссылками и указателями на него. При работе программы будет определен фактический тип адресуемого объекта и вызвана подходящая реализация открытого интерфейса.



Нахождение ( или разрешение) нужной функции во время выполнения называется динамическим связыванием (dynamic binding) (по умолчанию функции разрешаются статически

во время компиляции). В C++ динамическое связывание поддерживается с помощью механизма виртуальных функций

класса. Полиморфизм подтипов и динамическое связывание формируют основу объектно-ориентированного программирования, которому посвящены следующие главы.

В главе 17 рассматриваются имеющиеся в C++ средства поддержки объектно-ориентированного программирования и изучается влияние наследование на такие механизмы, как конструкторы, деструкторы, почленная инициализация и присваивание; для примера разрабатывается иерархия классов Query, поддерживающая систему текстового поиска, введенную в главе 6.

Темой главы 18 является изучение более сложных иерархий, возможных за счет использования множественного и виртуального наследования. С его помощью мы развернем шаблон класса из главы 16 в трехуровневую иерархию.

В главе 19 обсуждается идентификация типов во время выполнения (RTTI), а также изучается вопрос о влиянии наследования на разрешение перегруженных функций. Здесь мы снова обратимся к средствам обработки исключений, чтобы разобраться в иерархии классов исключений, которую предлагает стандартная библиотека. Мы покажем также, как написать собственные такие классы.

Глава 20 посвящена углубленному рассмотрению библиотеки потокового ввода/вывода iostream. Эта библиотека представляет собой иерархию классов, поддерживающую как виртуальное, так и множественное наследование.

17


Шаблоны-члены


Шаблон функции или класса может быть членом обычного класса или шаблона класса. Определение шаблона-члена похоже на определение шаблона: ему предшествует ключевое слово template, за которым идет список параметров:

template <class T>

class Queue {

private:

   // шаблон класса-члена

   template <class Type>

      class CL

   {

      Type member;

      T mem;

   };

   // ...

public:

   // шаблон функции-члена

   template <class Iter>

      void assign( Iter first, Iter last )

   {

      while ( ! is_empty() )

         remove();     // вызывается Queue<T>::remove()

      for ( ; first != last; ++first )

          add( *first );  // вызывается Queue<T>::add( const T & )

   }

}

(Отметим, что шаблоны-члены не поддерживаются компиляторами, написанными до принятия стандарта C++. Эта возможность была добавлена в язык для поддержки реализации абстрактных контейнерных типов, представленных в главе 6.)

Объявление шаблона-члена имеет собственные параметры. Например, у шаблона класса CL

есть параметр Type, а у шаблона функции assign() – параметр Iter. Помимо этого, в определении шаблона-члена могут использоваться параметры объемлющего шаблона класса. Например, у шаблона CL

есть член типа T, представляющего параметр включающего шаблона Queue.

Объявление шаблона-члена в шаблоне класса Queue означает, что конкретизация Queue

потенциально может содержать бесконечное число различных вложенных классов CL

функций-членов assign(). Так, конкретизированный экземпляр Queue<int>

включает вложенные типы:

Queue<int>::CL<char>

Queue<int>::CL<string>

и вложенные функции:

void Queue<int>::assign( int *, int * )

void Queue<int>::assign( vector<int>::iterator,

                         vector<int>::iterator )

Для шаблона-члена действуют те же правила доступа, что и для других членов класса. Так как шаблон CL

является закрытым членом шаблона Queue, то лишь функции-члены и друзья Queue


могут ссылаться на его конкретизации. С другой стороны, шаблон функции assign()

объявлен открытым членом и, значит, доступен во всей программе.

Шаблон-член конкретизируется при его использовании в программе. Например, assign()

конкретизируется в момент обращения к ней из main():



int main()

{

   // конкретизация Queue<int>

   Queue<int> qi;

   // конкретизация Queue<int>::assign( int *, int * )

   int ai[4] = { 0, 3, 6, 9 };

   qi.assign( ai, ai + 4 );

   // конкретизация Queue<int>::assign( vector<int>::iterator,

   //                                     vector<int>::iterator )

   vector<int> vi( ai, ai + 4 );

   qi.assign( vi.begin(), vi.end() );
}

Шаблон функции assign(), являющийся членом шаблона класса Queue, иллюстрирует необходимость применения шаблонов-членов для поддержки контейнерных типов. Предположим, имеется очередь типа Queue<int>, в которую нужно поместить содержимое любого другого контейнера (списка, вектора или обычного массива), причем его элементы имеют либо тип int

(т.е. тот же, что у элементов очереди), либо приводимый к типу int. Шаблон-член assign()позволяет это сделать. Поскольку может быть использован любой контейнерный тип, то интерфейс assign()

программируется в расчете на употребление итераторов; в результате реализация оказывается не зависящей от фактического типа, на который итераторы указывают.

В функции main()

шаблон-член assign()

сначала конкретизируется типом int*, что позволяет поместить в qi

содержимое массива элементов типа int. Затем шаблон-член конкретизируется типом vector<int>::iterator – это дает возможность поместить в очередь qi содержимое вектора элементов типа int. Контейнер, содержимое которого помещается в очередь, не обязательно должен состоять из элементов типа int. Разрешен любой тип, который приводится к int. Чтобы понять, почему это так, еще раз посмотрим на определение assign():



template <class Iter>

      void assign( Iter first, Iter last )

{

   // удалить все элементы из очереди

   for ( ; first != last; ++first )

      add( *first );
<


}

Вызываемая из assign()

функция add() – это функция-член Queue<Type>::add(). Если Queue

конкретизируется типом int, то у add()

будет следующий прототип:

void Queue<int>::add( const int &val );

Аргумент *first

должен иметь тип int

либо тип, которым можно инициализировать параметр-ссылку на const int. Преобразования типов допустимы. Например, если воспользоваться классом SmallInt из раздела 15.9, то содержимое контейнера, в котором хранятся элементы типа SmallInt, с помощью шаблона-члена assign()

помещается в очередь типа Queue<int>. Это возможно потому, что в классе SmallInt имеется конвертер для приведения SmallInt к int:



class SmallInt {

public:

   SmallInt( int ival = 0 ) : value( ival ) { }

   // конвертер: SmallInt ==> int

   operator int() { return value; }

   // ...

private:

   int value;

};

int main()

{

   // конкретизация Queue<int>

   Queue<int> qi;

   vector<SmallInt> vsi;

   // заполнить вектор

   // конкретизация

   // Queue<int>::assign( vector<SmallInt>::iterator,

   //                     vector<SmallInt>::iterator )

   qi.assign( vsi.begin(), vsi.end() );

   list<int*> lpi;

   // заполнить список

   // ошибка при конкретизации шаблона-члена assign():

   // нет преобразования из int* в int

   qi.assign( lpi.begin(), lpi.end() );
}

Первая конкретизация assign()

правильна, так как существует неявное преобразование из типа SmallInt в тип int и, следовательно, обращение к add() корректно. Вторая же конкретизация ошибочна: объект типа int* не может инициализировать ссылку на тип const int, поэтому вызвать функцию add() невозможно.

Для контейнерных типов из стандартной библиотеки C++ имеется функция assign(), которая ведет себя так же, как функция-шаблон assign() для нашего класса Queue.

Любую функцию-член можно задать в виде шаблона. Это относится, в частности, к конструктору. Например, для шаблона класса Queue его можно определить следующим образом:





template <class T>

class Queue {

   // ...

public:

   // шаблон-член конструктора

   template <class Iter>

   Queue( Iter first, Iter last )

        : front( 0 ), back( 0 )

   {

      for ( ; first != last; ++first )

          add( * first );

   }
};

Такой конструктор позволяет инициализировать очередь содержимым другого контейнера. У  контейнерных типов из стандартной библиотеки C++ также есть предназначенные для этой цели конструкторы в виде шаблонов-членов. Кстати, в первом (в данном разделе) определении функции main() использовался конструктор-шаблон для вектора:

vector<int> vi( ai, ai + 4 );

Это определение конкретизирует шаблон конструктора для контейнера vector<int>

типом int*, что позволяет инициализировать вектор содержимым массива элементов типа int.

Шаблон-член, как и обычные члены, может быть определен вне определения объемлющего класса или шаблона класса. Так, являющиеся членами шаблон класса CL или шаблон функции assign()

могут быть следующим образом определены вне шаблона Queue:



template <class T>

class Queue {

private:

   template <class Type> class CL;

   // ...

public:

   template <class Iter>

      void assign( Iter first, Iter last );

   // ...

};

template <class T> template <class Type>

   class Queue<T>::CL<Type>

{

   Type member;

   T mem;

};

template <class T> template <class Iter>

   void Queue<T>::assign( Iter first, Iter last )

{

   while ( ! is_empty() )

      remove();

   for ( ; first != last; ++first )

      add( *first );
}

Определению шаблона-члена, которое находится вне определения объемлющего шаблона класса, предшествует список параметров объемлющего шаблона класса, а за ним должен следовать собственный такой список. Вот почему определение шаблона функции assign()

(члена шаблона класса Queue) начинается с

template <class T> template <class Iter>

Первый список параметров шаблона template <class T> относится к шаблону класса Queue. Второй – к самому шаблону-члену assign(). Имена параметров не обязаны совпадать с теми, которые указаны внутри определения объемлющего шаблона класса. Приведенная инструкция по-прежнему определяет шаблон-член assign():

template <class TT> template <class IterType>



void Queue<TT>::assign( IterType first, IterType last )

{ ... }


Шаблоны функций


В этой главе рассказывается, что такое шаблон функции, как его определять и использовать. Это довольно просто, и многие программисты применяют шаблоны, определенные в стандартной библиотеке, даже не понимая, с чем они работают. Только пользователи, хорошо знающие язык С++, самостоятельно определяют и применяют шаблоны функций так, как здесь описано. Поэтому материал данной главы следует рассматривать как переход к более сложным аспектам C++. Мы начнем с рассказа о том, что такое шаблон функции и как его определять, затем на простом примере проиллюстрируем использование шаблонов. Далее мы перейдем к темам, требующим больших знаний. Сначала посмотрим на усложненные примеры применения шаблонов, затем подробно остановимся на выведении (deduction) их аргументов и покажем, как их можно задавать при конкретизации (instantiation) шаблона функции. После этого мы посмотрим, каким образом компилятор конкретизирует шаблоны и какие требования предъявляются в этой связи к организации наших программ, а также обсудим, как определить специализацию для такой конкретизации. Затем в данной главе будут изложены вопросы, представляющие интерес для проектировщиков шаблонов функций. Мы объясним, как можно перегружать шаблоны и как применительно к ним работает разрешение перегрузки. Мы также расскажем о разрешении имен в определениях шаблонов функций и покажем, как можно определять шаблоны в пространствах имен. Глава завершается развернутым примером.



Шаблоны классов


В этой главе описывается, как определять и использовать шаблоны классов. Шаблон – это предписание для создания класса, в котором один или несколько типов либо значений параметризованы. Начинающий программист может использовать шаблоны, не понимая механизма, стоящего за их определениями и конкретизациями. Фактически на протяжении всей этой книги мы пользовались шаблонами классов, которые определены в стандартной библиотеке C++ (например, vector, list и т.д.), и при этом не нуждались в детальном объяснении механизма их работы. Только профессиональные программисты определяют собственные шаблоны классов и пользуются описанными в данной главе средствами. Поэтому этот материал следует рассматривать как введение в более сложные аспекты C++.

Глава 16 содержит вводные и продвинутые разделы. Во вводных разделах показано, как определяются шаблоны классов, иллюстрируются простые способы применения и обсуждается механизм их конкретизации. Мы расскажем, как можно задавать в шаблонах разные виды членов: функции-члены, статические данные-члены и вложенные типы. В продвинутых разделах представлен материал, необходимый для написания приложений промышленного уровня. Сначала мы рассмотрим, как компилятор конкретизирует шаблоны и какие требования в связи с этим предъявляются к организации

нашей программы. Затем покажем, как определять специализации и частичные специализации для шаблона класса и для его члена. Далее мы остановимся на двух вопросах, представляющих интерес для проектировщиков: как разрешаются имена в определениях шаблона класса и как можно определять шаблоны в пространствах имен. Завершается эта глава примером определения и использования шаблона класса.



Шаблоны классов и модель компиляции A


Определение шаблона класса– это лишь предписание для построения бесконечного множества типов классов. Сам по себе шаблон не определяет никакого класса. Например, когда компилятор видит:

template <class Type>

   class Queue { ... };

он только сохраняет внутреннее представление Queue. Позже, когда встречается реальное использование класса, конкретизированного по шаблону, скажем:

int main() {

   Queue<int> *p_qi = new Queue<int>;

}

компилятор конкретизирует тип класса Queue<int>, применяя сохраненное внутреннее представление определения шаблона Queue.

Шаблон конкретизируется только тогда, когда он употребляется в контексте, требующем полного определения класса. (Этот вопрос подробно обсуждался в разделе 16.2.) В примере выше класс Queue<int>

конкретизируется, потому что компилятор должен знать размер типа Queue<int>, чтобы выделить нужный объем памяти для объекта, созданного оператором new.

Компилятор может конкретизировать шаблон только тогда, когда он видел не только его объявление, но и фактическое определение, которое должно предшествовать тому месту программы, где этот шаблон используется:

// объявление шаблона класса

template <class Type>

   class Queue;

Queue<int>* global_pi = 0;  // правильно: определение класса не нужно

int main() {

   // ошибка: необходима конкретизация

   //         определение шаблона класса должно быть видимо

   Queue<int> *p_qi = new Queue<int>;

}

Шаблон класса можно конкретизировать одним и тем же типом в нескольких файлах. Как и в случае с типами классов, когда определение класса должно присутствовать в каждом файле, где используются его члены, компилятор конкретизирует шаблон некоторым типом во всех файлах, в которых данный экземпляр употребляется в контексте, требующем полного определения класса. Чтобы определение шаблона было доступно везде, где может понадобиться конкретизация, его следует поместить в заголовочный файл.

Функции-члены и статические данные-члены шаблонов классов, а также вложенные в них типы ведут себя почти так же, как сами шаблоны. Определения членов шаблона используются для порождения экземпляров членов в конкретизированном шаблоне. Если компилятор видит:




template <class Type>

void Queue<Type>::add( const Type &val )
   { ... }

он сохраняет внутреннее представление Queue<Type>::add(). Позже, когда в программе встречается фактическое употребление этой функции-члена, допустим через объект типа Queue<int>, компилятор конкретизирует Queue<int>::add(const int &), пользуясь таким представлением:



#include "Queue.h"

int main() {

   // конкретизация Queue<int>

   Queue<int> *p_qi = new Queue<int>;

   int ival;

   // ...

   // конкретизация Queue<int>::add( const int & )

   p_qi->add( ival );

   // ...
}

Конкретизация шаблона класса некоторым типом не приводит к автоматической конкретизации всех его членов тем же типом. Член конкретизируется только при использовании в таком контексте, где необходимо его определение (т.е. вложенный тип употреблен так, что требуется его полное определение; вызвана функция-член или взят ее адрес; имеется обращение к значению статического члена).

Конкретизация функций-членов и статических членов шаблонов класса поднимает те же вопросы, которые мы уже обсуждали для шаблонов функций в разделе 10.5. Чтобы компилятор мог конкретизировать функцию-член или статический член шаблона класса, должно ли определение члена быть видимым в момент конкретизации? Например, должно ли определение функции-члена add() появиться до ее конкретизации типом int в main()? Следует ли помещать определения функций-членов и статических членов шаблонов класса в заголовочные файлы (как мы поступаем с определениями встроенных функций), которые включаются всюду, где применяются их конкретизированные экземпляры? Или конкретизации определения шаблона достаточно для того, чтобы этими членами можно было пользоваться, так что определения членов можно оставлять в файлах с исходными текстами (где обычно располагаются определения невстроенных функций-членов и статических членов)?

Для ответа на эти вопросы нам придется вспомнить модель компиляции шаблонов в C++, где формулируются требования к организации программы, в которой определяются и употребляются шаблоны. Обе модели (с включением и с разделением), описанные в разделе 10.5, в полной мере применимы и к определениям функций-членов и статических данных-членов шаблонов классов. В оставшейся части этого раздела описываются обе модели и объясняется их использование с определениями членов.


Сильно типизированная библиотека


Библиотека iostream

сильно типизирована. Например, попытка прочитать из объекта класса ostream или записать в объект класса istream помечается компилятором как нарушение типизации. Так, если имеется набор объявлений:

#include <iostream>

#include <fstream>

class Screen;

extern istream& operator>>( istream&, const Screen& );

extern void print( ostream& );

ifstream inFile;

то следующие две инструкции приводят к нарушению типизации, обнаруживаемому во время компиляции:

int main()

{

   Screen myScreen;

   // ошибка: ожидается ostream&

   print( cin >> myScreen );

   // ошибка: ожидается оператор >>

   inFile << "ошибка: оператор вывода";

Средства ввода/вывода включены в состав стандартной библиотеки C++. В главе 20 библиотека iostream

описана не полностью, в частности вопрос о создании определенных пользователем манипуляторов и буферных классов остался за рамками введения в язык. Мы сосредоточили внимание лишь на той части библиотеки iostream, которая имеет основополагающее значение для программного ввода/вывода.

Приложение



Система текстового поиска


В систему текстового поиска входят текстовый файл, указанный пользователем, и средство для задания запроса, состоящего из слов и, возможно, логических операторов.

Если одно или несколько слов запроса найдены, печатается количество их вхождений. По желанию пользователя печатаются предложения, содержащие найденные слова. Например, если нужно найти все вхождения словосочетаний Civil War и Civil Rights, запрос может выглядеть таким образом[9]:

Civil && ( War || Rights )

Результат запроса:

Civil: 12 вхождений

War: 48 вхождений

Rights: 1 вхождение

Civil && War: 1 вхождение

Civil && Rights: 1 вхождение

(8) Civility, of course, is not to be confused with

Civil Rights, nor should it lead to Civil War

Здесь (8) представляет собой номер предложения в тексте. Наша система должна печатать фразы, содержащие найденные слова, в порядке возрастания их номеров (т.е. предложение номер 7 будет напечатано раньше предложения номер 9), не повторяя одну и ту же несколько раз.

Наша программа должна уметь:

·                  запросить имя текстового файла, а затем открыть и прочитать этот файл;

·                  организовать внутреннее представление этого файла так, чтобы впоследствии соотнести найденное слово с предложением, в котором оно встретилось, и определить порядковый номер этого слова ;

·                  понимать определенный язык запросов. В нашем случае он включает следующие операторы:

&&  два слова непосредственно следуют одно за другим в строке

||

одно или оба слова встречаются в строке

!

слово не встречается в строке

()

группировка слов в запросе

Используя этот язык, можно написать:

Lincoln

чтобы найти все предложения, включающие слово Lincoln, или


! Lincoln

для поиска фраз, не содержащих такого слова, или же

( Abe || Abraham ) && Lincoln

для поиска тех предложений, где есть словосочетания Abe Lincoln или Abraham Lincoln.

Представим две версии нашей системы. В этой главе мы решим проблему чтения и хранения текстового файла в отображении, где ключом является слово, а значением – номер строки и позиции в строке. Мы обеспечим поиск по одному слову. (В главе 17 мы реализуем полную систему поиска, поддерживающую все указанные выше операторы языка запросов с помощью класса Query.) .

Возьмем шесть строчек из неопубликованного детского рассказа Стена Липпмана (Stan Lippman)[10]:

Рис. 2.

Alice Emma has long flowing red hair. Her Daddy says when the wind blows through her hair, it looks almost alive, like a fiery bird in flight. A beautiful fiery bird, he tells her, magical but untamed. "Daddy, shush, there is no such thing," she tells him, at the same time wanting him to tell her more. Shyly, she asks, "I mean. Daddy, is there?"

После считывания текста его внутреннее представление выглядит так (процесс считывания включает ввод очередной строки, разбиение ее на слова, исключение знаков препинания, замену прописных букв строчными, минимальная поддержка работы с суффиксами и исключение таких слов,  как and, a, the):

alice ((0,0))

alive ((1,10))

almost ((1,9))

ask ((5,2))

beautiful ((2,7))

bird ((2,3),(2,9))

blow ((1,3))

daddy ((0,8),(3,3),(5,5))

emma ((0,1))

fiery ((2,2),(2,8))

flight ((2,5))

flowing ((0,4))

hair ((0,6),(1,6))

has ((0,2))

like ((2,0))

long ((0,3))

look ((1,8))

magical ((3,0))

mean ((5,4))

more ((4,12))

red ((0,5))

same ((4,5))

say ((0,9))

she ((4,0),(5,1))

shush ((3,4))

shyly ((5,0))

such ((3,8))

tell ((2,11),(4,1),(4,10))

there ((3,5),(5,7))

thing ((3,9))

through ((1,4))

time ((4,6))

untamed ((3,2))

wanting ((4,7))

wind ((1,2))

Ниже приводится пример работы программы, которая будет реализована в данном разделе (то, что задает пользователь, выделено курсивом):



please enter file name: alice_emma

enter a word against which to search the text.

to quit, enter a single character ==> alice

alice occurs 1 time:

    ( line 1 ) Alice Emma has long flowing red hair. Her Daddy says

enter a word against which to search the text.

to quit, enter a single character ==> daddy

daddy occurs 3 times:

    ( line 1 ) Alice Emma has long flow-ing red hair. Her Daddy says

    ( line 4 ) magical but untamed. "Daddy, shush, there is no such thing,"

    ( line 6 ) Shyly, she asks, "I mean, Daddy, is there?"

enter a word against which to search the text.

to quit, enter a single character ==> phoenix

Sorry. There are no entries for phoenix.

enter a word against which to search the text.

to quit, enter a single character ==> .

Ok, bye!

Для того чтобы реализация была достаточно простой, необходимо детально рассмотреть стандартные контейнерные типы и тип string, представленный в главе 3.


Словарь


Вот небольшая программа, иллюстрирующая построение отображения, поиск в нем и обход элементов. Здесь используются два отображения. Первое, необходимое для преобразования слов, содержит два элемента типа string. Ключом является слово, которое нуждается в специальной обработке, а значением– слово, заменяющее ключ. Для простоты мы задали пары ключ/значение непосредственно в тексте программы (вы можете модифицировать программу так, чтобы она читала их из стандартного ввода или из файла). Второе отображение используется для подсчета произведенных замен. Текст программы выглядит следующим образом:

#include <map>

#include <vector>

#include <iostream>

#include <string>

int main()

{

  map< string, string > trans_map;

  typedef map< string, string >::value_type valType;

  // первое упрощение:

  // жестко заданный словарь

  trans_map.insert( va1Type( "gratz", "grateful" ));

  trans_map.insert( va1Type( "'em",   "them"     ));

  trans_map.insert( va1Type( "cuz",   "because"  ));

  trans_map.insert( va1Type( "nah",   "no"       ));

  trans_map.insert( va1Type( "sez",   "says"     ));

  trans_map.insert( va1Type( "tanx",  "thanks"   ));

  trans_map.insert( va1Type( "wuz",   "was"      ));

  trans_map.insert( va1Type( "pos",   "suppose"  ));

  // напечатаем словарь

  map< string,string >::iterator it;

  cout << "Наш словарь подстановок: \n\n";

  for ( it = trans_map.begin();

          it != trans_map.end(); ++it )

      cout << "ключ: "   << (*it).first << "\t"

           << "значение: " << ("it).second << "\n";

  cout << "\n\n";

  // второе упрощение: жестко заданный текст

  string textarray[14]={ "nah", "I", "sez", "tanx",

              "cuz", "I", "wuz", "pos", "to", "not",

              "cuz", "I", "wuz", "gratz" };

  vector< string > text( textarray, textarray+14 );

  vector< string >::iterator iter;

  // напечатаем текст

  cout << "Исходный вектор строк:\n\n";

  int cnt = 1;

  for ( iter = text-begin(); iter != text.end();

                                      ++iter,++cnt )

      cout <<  *iter << ( cnt % 8 ? " " : "\n" );

  cout << "\n\n\n";

  // map для сбора статистики

  map< string,int > stats;

  typedef map< string,int >::value_type statsValType;

  // здесь происходит реальная работа

  for ( iter=text.begin(); iter != text.end(); ++iter )

  if (( it = trans_map.find( *iter ))

             != trans_map.end() )

  {

      if ( stats.count( *iter ))

          stats [ *iter ] += 1;

      else stats.insert( statsVa1Type( *iter, 1 ));

      *iter = (*it).second;

  }

  // напечатаем преобразованный текст

  cout << "Преобразованный вектор строк:\n\n";

  cnt = 1;

  for ( iter = text.begin(); iter != text.end();

                                      ++iter, ++cnt )

      cout << *iter << ( cnt % 8 ? " " : "\n" );

  cout << "\n\n\n";

  // напечатаем статистику

  cout << "И напоследок статистика:\n\n";

  map<string,int,less<string>,allocator>::iterator siter;

  for (siter=stats.begin(); siter!=stats.end(); ++siter)

    cout << (*siter).first     << " "

         << "было заменено "

         << (*siter).second

         << (" раз(а)\n" );

}

Вот результат работы программы:

Наш словарь подстановок:

key: 'em      value: them

key: cuz      value: because

key: gratz    value: grateful

key: nah      value: no

key: pos      value: suppose

key: sez      value: says

key: tanx     value: thanks

key: wuz      value: was

Исходный вектор строк:

nah I sez tanx cuz I wuz pos

to not cuz I wuz gratz

Преобразованный вектор строк:

no I says thanks because I was suppose

to not because I was grateful

И напоследок статистика:

cuz было заменено 2 раз(а)

gratz было заменено 1 раз(а)

nah было заменено 1 раз(а)

pos было заменено 1 раз(а)

sez было заменено 1 раз(а)

tanx было заменено 1 раз(а)

wuz было заменено 2 раз(а)



Соберем все вместе


Функция main() для нашего приложения текстового поиска выглядит следующим образом:

#include "TextQuery.h"

int main()

{

   TextQuery tq;

   tq.build_up_text();

   tq.query_text();

}

Функция-член build_text_map() – это не что иное, как переименованная функция doit() из раздела 6.14:

inline void

TextQuery::

build_text_map()

{

   retrieve_text();

   separate_words();

   filter_text();

   suffix_text();

   strip_caps();

   build_word_map();

}

Функция-член query_text()

заменяет одноименную функцию из раздела 6.14. В первоначальной реализации в ее обязанности входили прием запроса от пользователя и вывод ответа. Мы решили сохранить за query_text() эти задачи, но реализовать ее по-другому[19]:

void

TextQuery::query_text()

{

    /* локальные объекты:

     *

     * text: содержит все слова запроса

     * query_text: вектор для хранения пользовательского запроса

     * caps: фильтр для поддержки преобразования

     * прописных букв в строчные

     *

     * user_query: объект UserQuery, в котором инкапсулировано

     *             собственно вычисление ответа на запрос

     */

           string text;

           string caps( "ABCDEFGHIJKLMNOPQRSTUVWXYZ" );

           vector<string, allocator> query_text;

           UserQuery user_query;

     // инициализировать статические члены UserQuery

           NotQuery::all_locs( text_locations->second );

           AndQuery::max_col( &line_cnt );

           UserQuery::word_map( word_map );

           do {

           // удалить предыдущий запрос, если он был

                  query_text.clear();

           cout << "Введите запрос. Пожалуйста, разделяйте все его "

                        << "элементы пробелами.\n"

                << "Запрос (или весь сеанс) завершается точкой ( . ).\n\n"

                         << "==> ";

          

          /*

           * прочитать запрос из стандартного ввода,

           * преобразовать все заглавные буквы, после чего

           * упаковать его в query_text ...

           *

           * примечание: здесь производятся все действия по

           * обработке запроса, связанные собственно с текстом ...

           */

                  while( cin  >> text )

                  {

                       if ( text == "." )

                            break;

                       string::size_type pos = 0;

                while (( pos = text.find_first_of( caps, pos ))

                                     != string::npos )

                          text[pos] = tolower( text[pos] );

                       query_text.push_back( text );

                  }

           // теперь у нас есть внутреннее представление запроса

           // обработаем его ...

                  if ( ! query_text.empty() )

                  {

                 // передать запрос объекту UserQuery

                         user_query.query( &query_text );

                 // вычислить ответ на запрос

                 // вернуть иерархию Query*

                 // подробности см. в разделе 17.7

                 // query - это член класса TextQuery типа Query*

                         query = user_query.eval_query();

                 // вычислить иерархию Query,

                 // реализация описана в разделе 17.7

                         query->eval();

                 // вывести ответ с помощью

                 // функции-члена класса TextQuery

                         display_solution();

                 // вывести на терминал пользователя дополнительную

                 // пустую строку

                         cout << endl;

                  }

        }

        while ( ! query_text.empty() );

        cout << "До свидания!\n";

<
}

Тестируя программу, мы применили ее к нескольким текстам. Первым стал короткий рассказ Германа Мелвилла “Bartleby”. Здесь иллюстрируется составной запрос AndQuery, для которого подходящие слова расположены в соседних строках. (Отметим, что слова, заключенные между символами косой черты, предполагаются набранными курсивом.)

Введите запрос. Пожалуйста, разделяйте все его элементы пробелами.

Запрос (или весь сеанс) завершается точкой ( . ).

==> John && Jacob && Astor

         john ( 3 ) lines match

         jacob ( 3 ) lines match

         john && jacob ( 3 ) lines match

         astor ( 3 ) lines match

         john && jacob && astor ( 5 ) lines match

Requested query: john && jacob && astor

( 34 ) All who know me consider me an eminently /safe/ man. The late

John Jacob

( 35 ) Astor, a personage little given to poethic enthusiasm, had no

hesitation in

( 38 ) my profession by the late John Jacob Astor, a name which, I admit

I love to

( 40 ) bullion. I will freely add that I was not insensible to the late

John Jacob

( 41 ) Astor's good opinion.

Следующий запрос, в котором тестируются скобки и составные операторы, обращен к тексту новеллы “Heart of Darkness” Джозефа Конрада:

==> horror || ( absurd && mystery ) || ( North && Pole )

         horror ( 5 ) lines match

         absurd ( 8 ) lines match

         mystery ( 12 ) lines match

         ( absurd && mystery ) ( 1 ) lines match

         horror || ( absurd && mystery ) ( 6 ) lines match

         north ( 2 ) lines match

         pole ( 7 ) lines match

         ( north && pole ) ( 1 ) lines match

         horror || ( absurd && mystery ) || ( north && pole )

                ( 7 ) lines match

Requested query: horror || ( absurd && mystery ) || ( north && pole )

( 257 ) up I will go there.' The North Pole was one of these

( 952 ) horros. The heavy pole had skinned his poor nose



( 3055 ) some lightless region of subtle horrors, where pure,

( 3673 ) " 'The horror! The horror!'

( 3913 ) the whispered cry, 'The horror! The horror! '

( 3957 ) absurd mysteries not fit for a human being to behold.

( 4088 ) wind. 'The horror! The horror!'

Последний запрос был обращен к отрывку из романа Генри Джеймса “Portrait of a Lady”. В нем иллюстрируется составной запрос в применении к большому текстовому файлу:

==> clever && trick || devious

         clever ( 46 ) lines match

         trick ( 12 ) lines match

         clever && trick ( 2 ) lines match

         devious ( 1 ) lines match

         clever && trick || devious ( 3 ) lines match

Requested query: clever && trick || devious

( 13914 ) clever trick she had guessed. Isabel, as she herself grew older

( 13935 ) lost the desire to know this lady's clever trick. If she had

( 14974 ) desultory, so devious, so much the reverse of processional.

There were

Упражнение 17.23

Реализованная нами обработка запроса пользователя обладает одним недостатком: она не применяет к каждому слову те же предварительные фильтры, что и программа, строящая вектор позиций (см. разделы 6.9 и 6.10). Например, пользователь, который хочет найти слово “maps”, обнаружит, что в нашем представлении текста распознается только “map”, поскольку существительные во множественном числе приводятся к форме в единственном числе. Модифицируйте функцию query_text() так, чтобы она применяла эквивалентные фильтры к словам запроса.

Упражнение 17.24

Поисковую систему можно было бы усовершенствовать, добавив еще одну разновидность запроса “И”, которую мы назовем InclusiveAndQuery и будем обозначать символом &. Строка текста удовлетворяет условиям запроса, если в ней находятся оба указанных слова, пусть даже не рядом. Например, строка

We were her pride of ten, she named us

удовлетворяет запросу:

pride & ten

но не:

pride && ten



Поддержите запрос InclusiveAndQuery.

Упражнение 17.25

Представленная ниже реализация функции display_solution()

может выводить только в стандартный вывод. Более правильно было бы позволить пользователю самому задавать поток ostream, в который надо направить вывод. Модифицируйте display_solution()

так, чтобы ostream

можно было задавать. Какие еще изменения необходимо внести в определение класса UserQuery?



void TextQuery::

display_solution()

{

           cout << "\n"

                << "Requested query: "

                << *query << "\n\n";

           const set<short,less<short>,allocator> *solution = query->solution();

          

           if ( ! solution->size() ) {

                cout << "\n\t"

                    << "Sorry, no matching lines were found in text.\n"

                    << endl;

           }

           set<short>::const_iterator

                  it = solution->begin(),

                  end_it = solution->end();

           for ( ; it != end_it; ++it ) {

                  int line = *it;

                  // пронумеруем строки с 1 ...

                  cout << "( " << line+1 << " ) "

                       << (*lines_of_text)[line] << '\n';

           }

           cout << endl;
}

Упражнение 17.26

Нашему классу TextQuery не хватает возможности принимать аргументы, заданные пользователем в командной строке.

(a)    Предложите синтаксис командной строки для нашей поисковой системы.

(b)   Добавьте в класс необходимые данные и функции-члены.

(c)    Предложите средства для работы с командной строкой (см. пример в разделе 7.8).

Упражнение 17.27

В качестве темы для рабочего проекта рассмотрите следующие усовершенствования нашей поисковой системы:

(a)    Реализуйте поддержку, необходимую для представления запроса AndQuery в виде одной строки, например “Motion Picture Screen Cartoonists”.



(b)   Реализуйте поддержку для ответа на запрос на основе вхождения слов не в строку, а в предложение.

(c)    Реализуйте подсистему хранения истории, с помощью которой пользователь мог бы ссылаться на предыдущий запрос по номеру, возможно, комбинируя его с новым запросом.

(d)   Вместо того чтобы показывать счетчик найденных и все найденные строки, реализуйте возможность задать диапазон выводимых строк для промежуточных вычислений и для окончательного ответа:

==> John && Jacob && Astor

(1)    john ( 3 ) lines match

(2)    jacob ( 3 ) lines match

(3)    john && jacob ( 3 ) lines match

(4)    astor ( 3 ) lines match

(5)    john && jacob && astor ( 5 ) lines match

// Новая возможность: пусть пользователь укажет, какой запрос выводить

// пользователь вводит число

==> вывести? 3

// Затем система спрашивает, сколько строк выводить

// при нажатии клавиши Enter выводятся все строки,

// но пользователь может также ввести номер одной строки или диапазон

ð                                сколько (Enter выводит все, иначе введите номер строки или диапазон) 1-3

18


Соображения эффективности A


В общем случае объект класса эффективнее передавать функции по указателю или по ссылке, нежели по значению. Например, если дана функция с сигнатурой:

bool sufficient_funds( Account acct, double );

то при каждом ее вызове требуется выполнить почленную инициализацию формального параметра acct

значением фактического аргумента-объекта класса Account. Если же функция имеет любую из таких сигнатур:

bool sufficient_funds( Account *pacct, double );

bool sufficient_funds( Account &acct, double );

то достаточно скопировать адрес объекта Account. В этом случае никакой инициализации класса не происходит (см. обсуждение взаимосвязи между ссылочными и указательными параметрами в разделе 7.3).

Хотя возвращать указатель или ссылку на объект класса также более эффективно, чем сам объект, но корректно запрограммировать это достаточно сложно. Рассмотрим такой оператор сложения:

// задача решается, но для больших матриц эффективность может

// оказаться неприемлемо низкой

Matrix

operator+( const Matrix& m1, const Matrix& m2 )

{

   Matrix result;

   // выполнить арифметические операции ...

   return result;

}

Этот перегруженный оператор позволяет пользователю писать

Matrix a, b;

// ...

// в обоих случаях вызывается operator+()

Matrix c = a + b;

a = b + c;

Однако возврат результата по значению может потребовать слишком больших затрат времени и памяти, если Matrix

представляет собой большой и сложный класс. Если эта операция выполняется часто, то она, вероятно, резко снизит производительность.

Следующая пересмотренная реализация намного увеличивает скорость:

// более эффективно, но после возврата адрес оказывается недействительным

// это может привести к краху программы

Matrix&

operator+( const Matrix& m1, const Matrix& m2 )

{

   Matrix result;

   // выполнить сложение ...

   return result;

}

но при этом происходят частые сбои программы. Дело в том, что значение переменной result не определено после выхода из функции, в которой она объявлена. (Мы возвращаем ссылку на локальный объект, который после возврата не существует.)


Значение возвращаемого адреса должно оставаться действительным после выхода из функции. В приведенной реализации возвращаемый адрес не затирается:



// нет возможности гарантировать отсутствие утечки памяти

// поскольку матрица может быть большой, утечки будут весьма заметными

Matrix&

operator+( const Matrix& m1, const Matrix& m2 )

{

   Matrix *result = new Matrix;

   // выполнить сложение ...

   return *result;
}

Однако это неприемлемо: происходит большая утечка памяти, так как ни одна из частей программы не отвечает за применение оператора delete к объекту по окончании его использования.

Вместо оператора сложения лучше применять именованную функцию, которой в качестве третьего параметра передается ссылка, где следует сохранить результат:



// это обеспечивает нужную эффективность,

// но не является интуитивно понятным для пользователя

void

mat_add( Matrix &result,

         const Matrix& m1, const Matrix& m3 )

{

   // вычислить результат
}

Таким образом, проблема производительности решается, но для класса уже нельзя использовать операторный синтаксис, так что теряется возможность инициализировать объекты



// более не поддерживается
Matrix c = a + b;

и использовать их в выражениях:



// тоже не поддерживается
if ( a + b > c ) ...

Неэффективный возврат объекта класса – слабое место С++. В качестве одного из решений предлагалось расширить язык, введя имя возвращаемого функцией объекта:



Matrix&

operator+( const Matrix& m1, const Matrix& m2 )

name result

{

   Matrix result;

   // ...

   return result;
}

Тогда компилятор мог бы самостоятельно переписать функцию, добавив к ней третий параметр-ссылку:



// переписанная компилятором функция

// в случае принятия предлагавшегося расширения языка

void

operator+( Matrix &result, const Matrix& m1, const Matrix& m2 )

name result

{

   // вычислить результат
}

и преобразовать все вызовы этой функции, разместив результат непосредственно в области, на которую ссылается первый параметр. Например:



Matrix c = a + b;

было бы трансформировано в



Matrix c;
operator+(c, a, b);

Это расширение так и не стало частью языка, но предложенная оптимизация прижилась. Компилятор в состоянии распознать, что возвращается объект класса и выполнить трансформацию его значения и без явного расширения языка. Если дана функция общего вида:



classType

functionName( paramList )

{

   classType namedResult;

   // выполнить какие-то действия ...

   return namedResult;
}

то компилятор самостоятельно трансформирует как саму функцию, так и все обращения к ней:



void

functionName( classType &namedResult, paramList )

{

   // вычислить результат и разместить его по адресу namedResult
}

что позволяет уйти от необходимости возвращать значение объекта и вызывать копирующий конструктор. Чтобы такая оптимизация была применена, в каждой точке возврата из функции должен возвращаться один и тот же именованный объект класса.

И последнее замечание об эффективности работы с объектами в C++. Инициализация объекта класса вида

Matrix c = a + b;

всегда эффективнее присваивания. Например, результат следующих двух инструкций такой же, как и в предыдущем случае:



Matrix c;
c = a + b;

но объем требуемых вычислений значительно больше. Аналогично эффективнее писать:



for ( int ix = 0; ix < size-2; ++ix ) {

     Matrix matSum = mat[ix] + mat[ix+1];

     // ...
}

чем



Matrix matSum;

for ( int ix = 0; ix < size-2; ++ix ) {

     matSum = mat[ix] + mat[ix+1];

     // ...
}

Причина, по которой присваивание всегда менее эффективно, состоит в том, что возвращенный локальный объект нельзя подставить вместо объекта в левой части оператора присваивания. Иными словами, в то время как инструкцию

Point3d p3 = operator+( p1, p2 );

можно безопасно трансформировать:



// Псевдокод на C++

Point3d p3;
operator+( p3, p1, p2 );

преобразование



Point3d p3;
p3 = operator+( p1, p2 );

в



// Псевдокод на C++

// небезопасно в случае присваивания
operator+( p3, p1, p2 );



небезопасно.

Преобразованная функция требует, чтобы переданный ей объект представлял собой неформатированную область памяти. Почему? Потому что к объекту сразу применяется конструктор, который уже был применен к именованному локальному объекту. Если переданный объект уже был сконструирован, то делать это еще раз с семантической точки зрения неверно.

Что касается инициализируемого объекта, то отведенная под него память еще не подвергалась обработке. Если же объекту присваивается значение и в классе объявлены конструкторы (а именно этот случай мы и рассматриваем), можно утверждать, что эта память уже форматировалась одним из них, так что непосредственно передавать объект функции небезопасно.

Вместо этого компилятор должен создать неформатированную область памяти в виде временного объекта класса, передать его функции, а затем почленно присвоить возвращенный временный объект объекту, стоящему в левой части оператора присваивания. Наконец, если у класса есть деструктор, то он применяется к временному объекту. Например, следующий фрагмент



Point3d p3;
p3 = operator+( p1, p2 );

трансформируется в такой:



// Псевдокод на C++

Point3d temp;

operator+( temp, p1, p2 );

p3.Point3d::operator=( temp );
temp.Point3d::~Point3d();

Майкл Тиманн (Michael Tiemann), автор компилятора GNU C++, предложил назвать это расширение языка именованным возвращаемым значением

(return value language extension). Его точка зрения изложена в работе [LIPPMAN96b]. В нашей книге “Inside the C++ Object Model” ([LIPPMAN96a]) приводится детальное обсуждение затронутых в этой главе тем.

15


Сопоставление объявлений в разных файлах


Одна из проблем, вытекающих из возможности объявлять объект или функцию в разных файлах, – вероятность несоответствия объявлений или их расхождения в связи с модификацией программы. В С++ имеются средства, помогающие обнаружить такие различия.

Предположим, что в файле token.C

функция addToken()

определена как имеющая один параметр типа unsigned char. В файле lex.C, где эта функция вызывается, в ее определении указан параметр типа char.

// ---- в файле token.C ----

int addToken( unsigned char tok ) { /* ... */ }

// ---- в файле lex.C ----

extern int addToken( char );

Вызов addToken() в файле lex.C

вызывает ошибку во время связывания программы. Если бы такое связывание прошло успешно, можно представить дальнейшее развитие событий: скомпилированная программа была протестирована на рабочей станции Sun Sparc, а затем перенесена на IBM 390. Первый же запуск потерпел неудачу: даже самые простые тесты не проходили. Что случилось?

Вот часть объявлений набора лексем:

const unsigned char INLINE = 128;

const unsigned char VIRTUAL = 129;

Вызов addToken()

выглядит так:

curTok = INLINE;

// ...

addToken( curTok );

Тип char

реализован как знаковый в одном случае и как беззнаковый в другом. Неверное объявление addToken()

приводит к переполнению на той машине, где тип char

является знаковым, всякий раз, когда используется лексема со значением больше 127. Если бы такой программный код компилировался и связывался без ошибки, во время выполнения могли обнаружиться серьезные последствия.

В С++ информация о количестве и типах параметров функций помещается в имя функции – это называется безопасным связыванием (type-safe linkage). Оно помогает обнаружить расхождения в объявлениях функций в разных файлах. Поскольку типы параметров unsigned char и char

различны, в соответствии с принципом безопасного связывания функция addToken(), объявленная в файле lex.C, будет считаться неизвестной. Согласно стандарту определение в файле token.C

задает другую функцию.


Подобный механизм обеспечивает некоторую степень проверки типов при вызове функций из разных файлов. Безопасное связывание также необходимо для поддержки перегруженных функций. (Мы продолжим рассмотрение этой проблемы в главе 9.)

Прочие типы несоответствия объявлений одного и того же объекта или функции в разных файлах не обнаруживаются во время компиляции или связывания. Поскольку компилятор обрабатывает отдельно каждый файл, он не способен сравнить типы в разных файлах. Несоответствия могут быть источником серьезных ошибок, проявляющихся, подобно приведенным ниже, только во время выполнения программы (к примеру, путем возбуждения исключения или из-за вывода неправильной информации).



// в token. C

unsigned char lastTok = 0;

unsigned char peekTok() { /* ... */ }

// в lex.C

extern char lastTok;
extern char peekTok();

Избежать подобных неточностей поможет прежде всего правильное использование заголовочных файлов. Мы поговорим об этом в следующем подразделе.


Состояние формата


Каждый объект класса из библиотеки iostream

поддерживает состояние формата, которое управляет выполнением операций форматирования, например основание системы счисления для целых значений или точность для значений с плавающей точкой. Для модификации состояния формата объекта в распоряжении программиста имеется предопределенный набор манипуляторов[O.A.6].1

Манипулятор применяется к потоковому объекту так же, как к данным. Однако вместо чтения или записи данных манипулятор модифицирует внутреннее состояние потока. Например, по умолчанию объект типа bool, имеющий значение true (а также литеральная константа true), выводится как целая ‘1’:

#include <iostream.h>

int main()

{

    bool illustrate = true;

    cout << "объект illustrate типа bool установлен в true: "

         << illustrate << '\n';

}

Чтобы поток cout

выводил переменную illustrate в виде слова true, мы применяем манипулятор boolalpha:

#include <iostream.h>

int main()

{

    bool illustrate = true;

    cout << "объект illustrate типа bool установлен в true: ";

    // изменяет состояние cout так, что булевские значения

    // печатаются в виде строк true и false

    cout << boolalpha;

    cout << illustrate << '\n';

}

Поскольку манипулятор возвращает потоковый объект, к которому он применялся, то допустимо прицеплять его к выводимым данным и другим манипуляторам. Вот как можно перемежать данные и манипуляторы в нашей программе:

#include <iostream.h>

int main()

{

    bool illustrate = true;

    cout << "объект illustrate типа bool: "

         << illustrate

         << "\nс использованием boolalpha: "

         << boolalpha << illustrate << '\n';

    // ...

}

Вывод данных и манипуляторов вперемежку может сбить пользователя с толку. Применение манипулятора изменяет не только представление следующего за ним объекта, но и внутреннее состояние потока. В нашем примере все значения типа bool в оставшейся части программы также будут выводиться в виде строк.


Чтобы отменить сделанную модификацию потока cout, необходимо использовать манипулятор noboolalpha:



cout << boolalpha   // устанавливает внутреннее состояние cout

     << illustrate
     << noboolalpha  // сбрасывает внутреннее состояние cout

Как мы покажем, для многих манипуляторов имеются парные.

По умолчанию значения арифметических типов читаются и записываются в десятичной системе счисления. Программист может изменить ее на восьмеричную или шестнадцатеричную, а затем вернуться к десятичной (это распространяется только на целые типы, но не на типы с плавающей точкой), пользуясь манипуляторами hex, oct и dec:



#include <iostream>

int main()

{

           int ival = 16;

           double dval = 16.0;

          

           cout << "ival: " << ival

          << " установлен oct: " << oct << ival << "\n";

           cout << "dval: " << dval

          << " установлен hex: " << hex << dval << "\n";

           cout << "ival: " << ival

          << " установлен dec: " << dec << ival << "\n";
}

Эта программа печатает следующее:

ival: 16 установлен oct: 20

dval: 16 установлен hex: 16

ival: 10 установлен dec: 16

Но, глядя на значение, мы не можем понять, в какой системе счисления оно записано. Например, 20 – это действительно 20 или восьмеричное представление 16? Манипулятор showbase

выводит основание системы счисления вместе со значением с помощью следующих соглашений:

·                  0x в начале обозначает шестнадцатеричную систему (если мы хотим, чтобы вместо строчной буквы 'x'

печаталась заглавная, то можем применить манипулятор uppercase, а для отмены – манипулятор nouppercase);

·                  0 в начале обозначает восьмеричную систему;



·                  отсутствие того и другого обозначает десятичную систему.

Вот та же программа, но и с использованием showbase:



#include <iostream>

int main()

{

           int ival = 16;

           double dval = 16.0;

     cout << showbase;

          

           cout << "ival: " << ival

          << " установлен oct: " << oct << ival << "\n";

           cout << "dval: " << dval

          << " установлен hex: " << hex << dval << "\n";

           cout << "ival: " << ival

          << " установлен dec: " << dec << ival << "\n";

     cout << noshowbase;
}

Результат:

ival: 16 установлен oct: 020

dval: 16 установлен hex: 16

ival: 0x10 установлен dec: 16

Манипулятор noshowbase

восстанавливает состояние cout, при котором основание системы счисления не выводится.

По умолчанию значения с плавающей точкой выводятся с точностью 6. Эту величину можно модифицировать с помощью функции-члена precision(int) или манипулятора setprecision(); для использования последнего необходимо включить заголовочный файл iomanip. precision()

возвращает текущее значение точности. Например:



#include <iostream>

#include <iomanip>

#include <math.h>

int main()

{

           cout << "Точность: "

          << cout.precision() << endl

          << sqrt(2.0) << endl;

     cout.precision(12);

           cout << "\nТочность: "

                << cout.precision() << endl

          << sqrt(2.0) << endl;

           cout << "\nТочность: "  << setprecision(3)

                << cout.precision() << endl

                << sqrt(2.0) << endl;

           return 0;
<


}

После компиляции и запуска программа печатает следующее:

Точность: 6

1.41421

Точность: 12

1.41421356237

Точность: 3

1.41

Манипуляторы, принимающие аргумент, такие, как setprecision() и setw(), требуют включения заголовочного файла iomanip:

#include <iomanip>

Кроме описанных аспектов,  setprecision()

имеет еще два: на целые значения он не оказывает никакого влияния; значения с плавающей точкой округляются, а не обрезаются. Таким образом, при точности 4 значение 3.14159

печатается как 3.142, а при точности 3 – как 3.14.

По умолчанию десятичная точка не печатается, если дробная часть значения равна 0. Например:

cout << 10.00

выводит

10

Чтобы точка выводилась, воспользуйтесь манипулятором showpoint:



cout << showpoint

     << 10.0
     << noshowpoint << '\n';

Манипулятор noshowpoint

восстанавливает поведение по умолчанию.

По умолчанию значения с плавающей точкой выводятся в нотации с фиксированной точкой. Для перехода на научную нотацию используется идентификатор scientific, а для возврата к прежней нотации – модификатор fixed:



cout << "научная: " << scientific

     << 10.0

     << "с фиксированной точкой: " << fixed
     << 10.0 << '\n';

В результате печатается:

научная: 1.0e+01

с фиксированной точкой: 10

Если бы мы захотели вместо буквы 'e'

выводить 'E', то следовало бы употребить манипулятор uppercase, а для возврата к 'e' – nouppercase. (Манипулятор uppercase не приводит к переводу букв в верхний регистр при печати.)

По умолчанию перегруженные операторы ввода пропускают пустые символы (пробелы, знаки табуляции, новой строки и возврата каретки). Если дана последовательность:

a bc

d

то цикл



char ch;

while ( cin >> ch )
     // ...

читает все буквы от 'a' до 'd' за четыре итерации, а пробельные разделители оператором ввода игнорируются. Манипулятор noskipws

отменяет такой пропуск пробельных символов:





char ch;

cin >> noskipws;

while ( cin >> ch )

     // ...
cin >> skipws;

Теперь цикл while

будет выполняться семь раз. Чтобы восстановить поведение по умолчанию, к потоку cin

применяется манипулятор skipws.

Когда мы пишем:

cout << "пожалуйста, введите значение: ";

то в буфере потока cout

сохраняется литеральная строка. Есть ряд условий, при которых буфер сбрасывается (т.е. опустошается), – в нашем случае в стандартный вывод:

·                  буфер может заполниться. Тогда перед чтением следующего значения его необходимо сбросить;

·                  буфер можно сбросить явно с помощью любого из манипуляторов flush, ends или endl:



// сбрасывает буфер

cout << "hi!" << flush;

// вставляет нулевой символ, затем сбрасывает буфер

char ch[2]; ch[0] = 'a'; ch[1] = 'b';

cout << ch << ends;

// вставляет символ новой строки, затем сбрасывает буфер
cout << "hi!" << endl;

·                  при установлении внутренней переменной состояния потока unitbuf буфер сбрасывается после каждой операции вывода;

·                  объект ostream

может быть связан (tied) с объектом istream. Тогда буфер ostream

сбрасывается каждый раз, когда istream читает из входного потока. cout

всегда связан с cin:

cin.tie( &cout );

Инструкция

cin >> ival;

приводит к сбросу буфера cout.

В любой момент времени объект ostream

разрешено связывать только с одним объектом istream. Чтобы разорвать существующую связь, мы передаем функции-члену tie() значение 0:



istream is;

ostream new_os;

// ...

// tie() возвращает существующую связь

ostream *old_tie = is.tie();

is.tie( 0 );   // разорвать существующую связь

is.tie( &new_os );  // установить новую связь

// ...

is.tie( 0 );   // разорвать существующую связь
<


is.tie( old_tie );  // восстановить прежнюю связь

Мы можем управлять шириной поля, отведенного для печати числового или строкового значения, с помощью манипулятора setw(). Например, программа

#include <iostream>

#include <iomanip>

int main()

{

           int ival = 16;

           double dval = 3.14159;

          

           cout << "ival: " << setw(12) << ival << '\n'

                << "dval: " << setw(12) << dval << '\n';
}

печатает:

ival:           16

dval:      3.14159

Второй модификатор setw()

необходим потому, что, в отличие от других манипуляторов, setw() не изменяет состояние формата объекта ostream.

Чтобы выровнять значение по левой границе, мы применяем манипулятор left

(соответственно манипулятор right восстанавливает выравнивание по правой границе). Если мы хотим получить такой результат:

    16

-    3

то пользуемся манипулятором internal, который выравнивает знак по левой границе, а значение – по правой, заполняя пустое пространство пробелами. Если же нужен другой символ, то можно применить манипулятор setfill(). Так

cout << setw(6) << setfill('%') << 100 << endl;

печатает:

%%%100

В табл. 20.1 приведен полный перечень предопределенных манипуляторов.

Таблица 20.1. Манипуляторы

Манипулятор

Назначение

 boolalpha

Представлять true и false в виде строк

*noboolalpha

Представлять true и false

как 1 и 0

Showbase

Печатать префикс, обозначающий систему счисления

*noshowbase

Не печатать префикс системы счисления

showpoint

Всегда печатать десятичную точку

*noshowpoint

Печатать десятичную точку только в том случае, если дробная часть ненулевая

showpos

Печатать +

для неотрицательных чисел

*noshowpos

Не печатать +

для неотрицательных чисел

Манипулятор

Назначение

*skipws

Пропускать пробельные символы в операторах ввода

noskipws

Не пропускать пробельные символы в операторах ввода

uppercase

Печатать 0X

при выводе в шестнадцатеричной системе счисления; E – при выводе в научной нотации

*nouppercase

Печатать 0x

при выводе в шестнадцатеричной системе счисления; e – при выводе в научной нотации

*dec

Печатать в десятичной системе

hex

Печатать в шестнадцатеричной системе

oct

Печатать в восьмеричной системе

left

Добавлять символ заполнения справа от значения

right

Добавлять символ заполнения слева от значения

internal

Добавлять символ заполнения между знаком и значением

*fixed

Отображать число с плавающей точкой в десятичной нотации

scientific

Отображать число с плавающей точкой в научной нотации

flush

Сбросить буфер ostream

ends

Вставить нулевой символ, затем сбросить буфер ostream

endl

Вставить символ новой строки, затем сбросить буфер ostream

ws

Пропускать пробельные символы

// для этих манипуляторов требуется #include <ionamip>

setfill( ch)

Заполнять пустое место символом ch

Setprecision( n )

Установить точность вывода числа с плавающей точкой равной n

setw( w )

Установить ширину поля ввода или вывода равной w

setbase( b )

Выводить целые числа по основанию b

*

обозначает состояние потока по умолчанию


Состояния потока


Пользователей библиотеки iostream, разумеется, интересует, находится ли поток в ошибочном состоянии. Например, если мы пишем

int ival;

cin >> ival;

и вводим слово "Borges", то cin

переводится в состояние ошибки после неудачной попытки присвоить строковый литерал целому числу. Если бы мы ввели число 1024, то чтение прошло бы успешно и поток остался бы в нормальном состоянии.

Чтобы выяснить, в каком состоянии находится поток, достаточно проверить его значение на истину:

if ( !cin )

   // операция чтения не прошла или встретился конец файла

Для чтения заранее неизвестного количества элементов мы обычно пишем цикл while:

while ( cin >> word )

      // операция чтения завершилась успешно ...

Условие в цикле while

будет равно false, если достигнут конец файла или произошла ошибка при чтении. В большинстве случаев такой проверки потокового объекта достаточно. Однако при реализации оператора ввода для класса WordCount из раздела 20.5 нам понадобился более точный анализ состояния.

У любого потока есть набор флагов, с помощью которых можно следить за состоянием потока. Имеются четыре предикатные функции-члена:

·

eof()

возвращает true, если достигнут конец файла:

if ( inOut.eof() )

   // отлично: все прочитано ...

·         bad()

возвращает true при попытке выполнения некорректной операции, например при установке позиции за концом файла. Обычно это свидетельствует о том, что поток находится в состоянии ошибки;

·         fail()

возвращает true, если операция завершилась неудачно, например не удалось открыть файл или передан некорректный формат ввода:

ifstream iFile( filename, ios_base::in );

if ( iFile.fail() )    // не удалось открыть

   error_message( ... );

·         good()

возвращает true, если все вышеперечисленные условия ложны:

if ( inOut.good() )

Существует два способа явно изменить состояние потока iostream. С помощью функции-члена clear() ему явно присваивается указанное значение. Функция setstate() не сбрасывает состояние, а устанавливает один из флагов, не меняя значения остальных. Например, в коде оператора ввода для класса WordCount при обнаружении неверного формата мы используем setstate() для установки флага fail в состоянии объекта istream:




if ((ch = is.get()) != '<' )

{

   is.setstate( ios_base::failbit );

   return is;
}

Имеются следующие значения флагов состояния:



ios_base::badbit

ios_base::eofbit

ios_base::failbit
ios_base::goodbit

Для установки сразу нескольких флагов используется побитовый оператор ИЛИ:

is.setstate( ios_base::badbit | ios_base::failbit );

При тестировании оператора ввода в классе WordCount (см. раздел 20.5) мы писали:



if ( !cin ) {

   cerr << "Ошибка ввода WordCount" << endl;

   return -1;
}

Возможно, вместо этого мы предпочли бы продолжить выполнение программы, предупредив пользователя об ошибке и попросив повторить ввод. Но перед чтением нового значения из потока cin

необходимо перевести его в нормальное состояние. Это можно сделать с помощью функции-члена clear():

cin.clear(); // сброс ошибок

В более общем случае clear()

используется для сброса текущего состояния и установки одного или нескольких флагов нового. Например:

cin.clear( ios_base::goodbit );

восстанавливает нормальное состояние потока. (Оба вызова эквивалентны, поскольку goodbit

является для clear()

аргументом по умолчанию.)

Функция-член rdstate()

позволяет получить текущее состояние объекта:



ios_base::iostate old_state = cin.rdstate();

cin.clear();

process_input();

// перевести поток cin в прежнее состояние
cin.clear( old_state );

Упражнение 20.15

Измените один (или оба) оператор ввода для класса Date из упражнения 20.7 и/или класса CheckoutRecord из упражнения 20.8 (см. раздел 20.4) так, чтобы они устанавливали состояние объекта istream. Модифицируйте программы, которыми вы пользовались для тестирования этих операторов, для проверки явно установленного состояния, вывода его на печать и сброса в нормальное. Протестируйте программы, подав на вход правильные и неправильные данные.


Специализации шаблонов классов A


Прежде чем приступать к рассмотрению специализаций шаблонов классов и причин, по которым в них может возникнуть надобность, добавим в шаблон Queue

функции-члены min() и max(). Они будут обходить все элементы очереди и искать среди них соответственно минимальное и максимальное значения (правильнее, конечно, использовать для этой цели обобщенные алгоритмы min() и max(), представленные в главе 12, но мы определим эти функции как члены шаблона Queue, чтобы познакомиться со специализациями.)

template <class Type>

class Queue {

   // ...

public:

   Type min();

   Type max();

   // ...

};

// найти минимальное значение в очереди Queue

template <class Type>

   Type Queue<Type>::min()

{

   assert( ! is_empty() );

   Type min_val = front->item;

   for ( QueueItem *pq = front->next; pq != 0; pq = pq->next )

      if ( pq->item < min_val )

         min_val = pq->item;

   return min_val;

}

// найти максимальное значение в очереди Queue

template <class Type>

   Type Queue<Type>::max()

{

   assert( ! is_empty() );

   Type max_val = front->item;

   for ( QueueItem *pq = front->next; pq != 0; pq = pq->next )

      if ( pq->item > max_val )

         max_val = pq->item;

   return max_val;

}

Следующая инструкция в функции-члене min()

сравнивает два элемента очереди Queue:

pq->item < min_val

Здесь неявно присутствует требование к типам, которыми может конкретизироваться шаблон класса Queue: такой тип должен либо иметь возможность пользоваться предопределенным оператором “меньше” для встроенных типов, либо быть классом, в котором определен оператор operator<(). Если же этого оператора нет, то попытка применить min() к очереди приведет к ошибке компиляции в том месте, где вызывается несуществующий оператор сравнения. (Аналогичная проблема существует и в max(), только касается оператора operator>()).

Предположим, что шаблон класса Queue

нужно конкретизировать таким типом:




class LongSouble {

public:

   LongDouble( double dbval ) : value( dval ) { }

   bool compareLess( const LongDouble & );

private:

   double value;
};

Но в этом классе нет оператора operator<(), позволяющего сравнивать два значения типа LongDouble, поэтому использовать для очереди типа Queue<LongDouble>

функции-члены min() и max()

нельзя. Одним из решений этой проблемы может стать определение глобальных operator<() и operator>(), в которых для сравнения значений типа Queue<LongDouble>

используется функция-член compareLess. Эти глобальные операторы вызывались бы из min() и max()

автоматически при сравнении объектов из очереди.

Однако мы рассмотрим другое решение, связанное со специализацией шаблонов класса: вместо общих определений функций-членов min() и max() при конкретизации шаблона Queue

типом LongDouble мы определим специальные экземпляры Queue<LongDouble>::min() и Queue<LongDouble>::max(), основанные на функции-члене compareLess() класса LongDouble.

Это можно сделать, если воспользоваться явным определением специализации, где после ключевого слова template

идет пара угловых скобок <>, а за ней – определение специализации члена класса. В приведенном примере для функций-членов min() и max()

класса Queue<LongDouble>, конкретизированного из шаблона, определены явные специализации:



// определения явных специализаций

template<> LongDouble Queue<LongDouble>::min()

{

   assert( ! is_empty() );

   LongDouble min_val = front->item;

   for ( QueueItem *pq = front->next; pq != 0; pq = pq->next )

      if ( pq->item.compareLess( min_val ) )

         min_val = pq->item;

   return min_val;

}

template<> LongDouble Queue<LongDouble>::max()

{

   assert( ! is_empty() );

   LongDouble max_val = front->item;

   for ( QueueItem *pq = front->next; pq != 0; pq = pq->next )

      if ( max_val.compareLess( pq->item ) )

         max_val = pq->item;

   return max_val;
<


}

Хотя тип класса Queue<LongDouble>

конкретизируется по шаблону, в каждом объекте этого типа используются специализированные функции-члены min() и max() – не те, что конкретизируются по обобщенным определениям этих функций в шаблоне класса Queue.

Поскольку определения явных специализаций min() и max() – это определения невстроенных функций, помещать их в заголовочный файл нельзя: они обязаны находится в файле с текстом программы. Однако явную специализацию функции можно объявить, не определяя. Например:



// объявления явных специализаций функций-членов

template <> LongDouble Queue<LongDouble>::min();
template <> LongDouble Queue<LongDouble>::max();

Поместив эти объявления в заголовочный файл, а соответствующие определения – в исходный, мы можем организовать код так же, как и для определений функций-членов обычного класса.

Иногда определение всего шаблона оказывается непригодным для конкретизации некоторым типом. В таком случае программист может специализировать шаблон класса целиком. Напишем полное определение класса Queue<LongDouble>:



// QueueLD.h: определяет специализацию класса Queue<LongDouble>

#include "Queue.h"

template<> Queue<LongDouble> {

   Queue<LongDouble>();

   ~Queue<LongDouble>();

   LongDouble& remove();

   void add( const LongDouble & );

   bool is_empty() const;

   LongDouble min();

   LongDouble max();

private:

   // Некоторая реализация
};

Явную специализацию шаблона класса можно определять только после того, как общий шаблон уже был объявлен (хотя и не обязательно определен). Иными словами, должно быть известно, что специализируемое имя обозначает шаблон класса. Если в приведенном примере не включить заголовочный файл Queue.h перед определением явной специализации шаблона, компилятор выдаст сообщение об ошибке, указывая, что Queue – это не имя шаблона.

Если мы определяем специализацию всего шаблона класса, то должны определить также все без исключения функции-члены и статические данные-члены. Определения членов из общего шаблона никогда не используются для создания определений членов явной специализации: множества членов этих шаблонов могут различаться. Чтобы предоставить определение явной специализации для типа класса Queue<LongDouble>, придется определить не только функции-члены min() и max(), но и все остальные.



Если класс специализируется целиком, лексемы template<>

помещаются только перед определением явной специализации всего шаблона:



#include "QueueLD.h"

// определяет функцию-член min()

// из специализированного шаблона класса
LongDouble Queue<LongDouble>::min() { }

Класс не может в одних файлах конкретизироваться из общего определения шаблона, а в других – из специализированного, если задано одно и то же множество аргументов. Например, специализацию шаблона QueueItem<LongDouble>

необходимо объявлять в каждом файле, где она используется:



// ---- File1.C ----

#include "Queue.h"

void ReadIn( Queue<LongDouble> *pq ) {

   // использование pq->add()

   // приводит к конкретизации QueueItem<LongDouble>

}


// ---- File2.C ----

#include "QueueLD.h"

void ReadIn( Queue<LongDouble> * );

int main() {

   // используется определение специализации для Queue<LongDouble>

   Queue<LongDouble> *qld = new Queue<LongDouble>;

   ReadIn( qld );

   // ...

}

Эта программа некорректна, хотя большинство компиляторов ошибку не обнаружат: заголовочный файл QueueLD.h

следует включать во все файлы, где используется Queue<LongDouble>, причем до первого использования.


Специальная семантика инициализации


Наследование, в котором присутствует один или несколько виртуальных базовых классов, требует специальной семантики инициализации. Взгляните еще раз на реализации Bear и Raccoon в предыдущем разделе. Видите ли вы, какая проблема связана с порождением класса Panda?

class Panda : public Bear,

              public Raccoon, public Endangered {

public:

      Panda( string name, bool onExhibit=true );

      virtual ostream& print( ostream& ) const;

      bool sleeping() const { return _sleeping; }

      void sleeping( bool newval ) { _sleeping = newval; }

      // ...

protected:

      bool _sleeping;

      // ...

};

Проблема в том, что конструкторы базовых классов Bear и Raccoon вызывают конструктор ZooAnimal с неявным набором аргументов. Хуже того, в нашем примере значения по умолчанию для аргумента fam_name

(название семейства) не только отличаются, они еще и неверны для Panda.

В случае невиртуального наследования производный класс способен явно инициализировать только свои непосредственные базовые классы (см. раздел 17.4). Так, классу Panda, наследующему от ZooAnimal, не разрешается напрямую вызвать конструктор ZooAnimal в своем списке инициализации членов. Однако при виртуальном наследовании только Panda может напрямую вызывать конструктор своего виртуального базового класса ZooAnimal.

Ответственность за инициализацию виртуального базового возлагается на ближайший производный класс. Например, когда объявляется объект класса Bear:

Bear winnie( "pooh" );

то Bear

является ближайшим производным классом для объекта winnie, поэтому выполняется вызов конструктора ZooAnimal, определенный в классе Bear. Когда мы пишем:

cout << winnie.family_name();

будет выведена строка:

The family name for pooh is Bear

(Название семейства для pooh – это Bear)

Аналогично для объявления

Raccoon meeko( "meeko" );

Raccoon – это ближайший производный класс для объекта meeko, поэтому выполняется вызов конструктора ZooAnimal, определенный в классе Raccoon. Когда мы пишем:


cout << meeko.family_name();

печатается строка:

The family name for meeko is Raccoon

(Название семейства для meeko - это Raccoon)

Если же объявить объект типа Panda:

Panda yolo( "yolo" );

то ближайшим производным классом для объекта yolo будет Panda, поэтому он и отвечает за инициализацию ZooAnimal.

Когда инициализируется объект Panda, то явные вызовы конструктора ZooAnimal в конструкторах классов Raccoon и Bear не выполняются, а вызывается он с теми аргументами, которые указаны в списке инициализации членов объекта Panda. Вот так выглядит реализация:



Panda::Panda( string name, bool onExhibit=true )

          : ZooAnimal( name, onExhibit, "Panda" ),

            Bear( name, onExhibit ),

            Raccoon( name, onExhibit ),

            Endangered( Endangered::environment,

                        Endangered::critical ),

            sleeping( false )
{}

Если в конструкторе Panda

аргументы для конструктора ZooAnimal не указаны явно, то вызывается конструктор ZooAnimal по умолчанию либо, если такового нет, выдается ошибка при компиляции определения конструктора Panda.

Когда мы пишем:

cout << yolo.family_name();

печатается строка:

The family name for yolo is Panda

(Название семейства для yolo - это Panda)

Внутри определения Panda

классы Raccoon и Bear

являются промежуточными, а не ближайшими производными. В промежуточном производном классе все прямые вызовы конструкторов виртуальных базовых классов автоматически подавляются. Если бы от Panda был в дальнейшем произведен еще один класс, то сам класс Panda

стал бы промежуточным и вызов из него конструктора ZooAnimal

также был бы подавлен.

Обратите внимание, что оба аргумента, передаваемые конструкторам Bear и Raccoon, излишни в том случае, когда они выступают в роли промежуточных производных классов. Чтобы избежать передачи ненужных аргументов, мы можем предоставить явный конструктор, вызываемый, когда класс оказывается промежуточным производным. Изменим наш конструктор Bear:





class Bear : public virtual ZooAnimal {

public:

   // если выступает в роли ближайшего производного класса

   Bear( string name, bool onExhibit=true )

       : ZooAnimal( name, onExhibit, "Bear" ),

         _dance( two_left_feet )

   {}

   // ... остальное без изменения

protected:

   // если выступает в роли промежуточного производного класса

   Bear() : _dance( two_left_feet ) {}

   // ... остальное без изменения
};

Мы сделали этот конструктор защищенным, поскольку он вызывается только из производных классов. Если аналогичный конструктор по умолчанию обеспечен и для класса Raccoon, можно следующим образом модифицировать конструктор Panda:



Panda::Panda( string

name, bool onExhibit=true )

          : ZooAnimal( name, onExhibit, "Panda" ),

            Endangered( Endangered::environment,

                        Endangered::critical ),

            sleeping( false )
{}


Специальные функции-члены


Существует специальная категория функций-членов, отвечающих за такие действия с объектами, как инициализация, присваивание, управление памятью, преобразование типов и уничтожение. Такие функции называются конструкторами. Они вызываются компилятором неявно каждый раз, когда объект класса определяется или создается оператором new. В объявлении конструктора его имя совпадает с именем класса. Вот, например, объявление конструктора класса Screen, в котором заданы значения по умолчанию для параметров hi, wid и bkground:

class Screen {

public:

   Screen( int hi = 8, int wid = 40, char bkground = '#');

   // объявления других функций-членов не изменяются

};

Определение конструктора класса Screen

выглядит так:

Screen::Screen( int hi, int

wid, char bk ) :

   _height( hi ),   // инициализировать _height значением hi

   _width( wid ),   // инициализировать _width значением wid

   _cursor ( 0 ),   // инициализировать _cursor нулем

   _screen( hi * wid, bk )  // размер экрана равен hi * wid

                            // все позиции инициализируются

                            // символом '#'

{ // вся работа проделана в списке инициализации членов

  // этот список обсуждается в разделе 14.5

}

Каждый объявленный объект класса Screen

автоматически инициализируется конструктором:

Screen s1;                     // Screen(8,40,'#')

Screen *ps = new Screen( 20 ); // Screen(20,40,'#')

int main() {

   Screen s(24,80,'*');        // Screen(24,80,'*')

   // ...

}

(В главе 14 конструкторы, деструкторы и операторы присваивания рассматриваются более подробно. В главе 15 обсуждаются конвертеры и функции управления памятью.)



Спецификации исключений


По объявлениям функций-членов pop() и push() класса iStack

невозможно определить, что они возбуждают исключения. Можно, конечно, включить в объявление подходящий комментарий. Тогда описание интерфейса класса в заголовочном файле будет содержать документацию возбуждаемых исключений:

class iStack {

public:

   // ...

   void pop( int &value );   // возбуждает popOnEmpty

   void push( int value );   // возбуждает pushOnFull

private:

   // ...

};

Но такое решение несовершенно. Неизвестно, будет ли обновлена документация при выпуске следующих версий iStack. Кроме того, комментарий не дает компилятору достоверной информации о том, что никаких других исключений функция не возбуждает. Спецификация исключений позволяет перечислить в объявлении функции все исключения, которые она может возбуждать. При этом гарантируется, что другие исключения функция возбуждать не будет.

Такая спецификация следует за списком формальных параметров функции. Она состоит из ключевого слова throw, за которым идет список типов исключений, заключенный в скобки. Например, объявления функций-членов класса iStack можно модифицировать, добавив спецификации исключений:

class iStack {

public:

   // ...

   void pop( int &value ) throw(popOnEmpty);

   void push( int value ) throw(pushOnFull);

private:

   // ...

};

Гарантируется, что при обращении к pop() не будет возбуждено никаких исключений, кроме popOnEmpty, а при обращении к push()–только pushOnFull.

Объявление исключения – это часть интерфейса функции, оно должно быть задано при ее объявлении в заголовочном файле. Спецификация исключений – это своего рода “контракт” между функцией и остальной частью программы, гарантия того, что функция не будет возбуждать никаких исключений, кроме перечисленных.

Если в объявлении функции присутствует спецификация исключений, то при повторном объявлении этой же функции должны быть перечислены точно те же типы. Спецификации исключений в разных объявлениях одной и той же функции не суммируются:




// два объявления одной и той же функции

extern int foo( int = 0 ) throw(string);

// ошибка: опущена спецификация исключений
extern int foo( int parm ) { }

Что произойдет, если функция возбудит исключение, не перечисленное в ее спецификации? Исключения возбуждаются только при обнаружении определенных аномалий в поведении программы, и во время компиляции неизвестно, встретится ли то или иное исключение во время выполнения. Поэтому нарушения спецификации исключений функции могут быть обнаружены только во время выполнения. Если функция возбуждает исключение, не указанное в спецификации, то вызывается unexpected() из стандартной библиотеки C++, а та по умолчанию вызывает terminate(). (В некоторых случаях необходимо переопределить действия, выполняемые функцией unexpected(). Стандартная библиотека предоставляет механизм для этого. Подробнее см. [STRAUSTRUP97].)

Необходимо уточнить, что unexpected() не вызывается только потому, что функция возбудила исключение, не указанное в ее спецификации. Все нормально, если она обработает это исключение самостоятельно, внутри функции. Например:



void recoup( int op1, int op2 ) throw(ExceptionType)

{

   try {

      // ...

      throw string("we're in control");

   }

   // обрабатывается возбужденное исключение

   catch ( string ) {

      // сделать все необходимое

   }
}  // все хорошо, unexpected() не вызывается

Функция recoup()

возбуждает исключение типа string, несмотря на его отсутствие в спецификации. Поскольку это исключение обработано в теле функции, unexpected() не вызывается.

Нарушения спецификации исключений функции обнаруживаются только во время выполнения. Компилятор не сообщает об ошибке, если в выражении throw возбуждается исключение неуказанного типа. Если такое выражение никогда не выполнится или не возбудит исключения, нарушающего спецификацию, то программа будет работать, как и ожидалось, и нарушение никак не проявится:



extern void doit( int, int ) throw(string, exceptionType);

void action ( int op1, int op2 ) throw(string) {

   doit( op1, op2 );   // ошибки компиляции не будет

   // ...
<


}

doit()

может возбудить исключение типа exceptionType, которое не разрешено спецификацией action(). Однако функция компилируется успешно. Компилятор при этом генерирует код, гарантирующий, что при возбуждении исключения, нарушающего спецификацию, будет вызвана библиотечная функция unexpected().

Пустая спецификация  показывает, что функция не возбуждает никаких исключений:

extern void no_problem () throw();

Если же в объявлении функции спецификация исключений отсутствует, то может быть возбуждено исключение любого типа.

Между типом возбужденного исключения и типом исключения, указанного в спецификации, не разрешается проводить никаких преобразований:



int convert( int parm ) throw(string)

{

   //...

   if ( somethingRather )

      // ошибка программы:

      // convert() не допускает исключения типа const char*

      throw "help!";
}

Выражение throw в функции convert()

возбуждает исключение типа строки символов в стиле языка C. Созданный объект-исключение имеет тип const char*. Обычно выражение типа const char*

можно привести к типу string. Однако спецификация не допускает преобразования типов, поэтому если convert()

возбуждает такое исключение, то вызывается unexpected(). Для исправления ошибки выражение throw

можно модифицировать так, чтобы оно явно преобразовывало значение выражения в тип string:

throw string( "help!" );


Спецификации исключений и указатели на функции


Спецификацию исключений можно задавать и при объявлении указателя на функцию. Например:

void (*pf)( int ) throw(string);

В этом объявлении говорится, что pf

указывает на функцию, которая способна возбуждать только исключения типа string. Как и для объявлений функций, спецификации исключений в разных объявлениях одного и того же указателя не суммируются, они должны быть одинаковыми:

extern void (*pf) ( int ) throw(string);

// ошибка: отсутствует спецификация исключения

void (*pf)( int );

При работе с указателем на функцию со спецификацией исключений есть ограничения на тип указателя, используемого в качестве инициализатора или стоящего в правой части присваивания. Спецификации исключений обоих указателей не обязаны быть идентичными. Однако на указатель-инициализатор она должна накладывать столь же или более строгие ограничения, чем на инициализируемый указатель (или тот, которому присваивается значение). Например:

void recoup( int, int ) throw(exceptionType);

void no_problem() throw();

void doit( int, int ) throw(string, exceptionType);

// правильно: ограничения, накладываемые на спецификации

// исключений recoup() и pf1, одинаковы

void (*pf1)( int, int ) throw(exceptionType) = &recoup;

// правильно: ограничения, накладываемые на спецификацию исключений no_problem(), более строгие,

// чем для pf2

void (*pf2)( ) throw(string) = &no_problem;

// ошибка: ограничения, накладываемые на спецификацию

// исключений doit(), менее строгие, чем для pf3

//

void (*pf3)( int, int ) throw(string) = &doit;

Третья инициализация не имеет смысла. Объявление указателя гарантирует, что pf3

адресует функцию, которая может возбуждать только исключения типа string. Но doit() возбуждает также исключения типа exceptionType. Поскольку  она не подходит под ограничения, накладываемые спецификацией исключений pf3, то не может служить корректным инициализатором для pf3, так что компилятор выдает ошибку.

Упражнение 11.9

В коде, разработанном для упражнения 11.8, измените объявление оператора operator[]() в классе IntArray, добавив спецификацию возбуждаемых им исключений. Модифицируйте программу так, чтобы operator[]()

возбуждал исключение, не указанное в спецификации. Что при этом происходит?

Упражнение 11.10

Какие исключения может возбуждать функция, если ее спецификация исключений имеет вид throw()? А если у нее нет такой спецификации?

Упражнение 11.11

Какое из следующих присваиваний ошибочно? Почему?

void example() throw(string);

(a) void (*pf1)() = example;

(b) void (*pf2) throw() = example;



Спецификатор const


Возьмем следующий пример кода:

for ( int index = 0; index < 512; ++index )

      ... ;

С использованием литерала 512 связаны две проблемы. Первая состоит в легкости восприятия текста программы. Почему верхняя граница переменной цикла должна быть равна именно 512? Что скрывается за этой величиной? Она  кажется случайной...

Вторая проблема касается простоты модификации и сопровождения кода. Предположим, программа состоит из 10 000 строк, и литерал 512 встречается в 4% из них. Допустим, в 80% случаев число 512 должно быть изменено на 1024. Способны ли вы представить трудоемкость такой работы и количество ошибок, которые можно сделать, исправив не то значение?

Обе эти проблемы решаются одновременно: нужно создать объект со значением 512. Присвоив ему осмысленное имя, например bufSize, мы сделаем программу гораздо более понятной:  ясно, с чем именно сравнивается переменная цикла.

index < bufSize

В этом случае изменение размера bufSize не требует просмотра 400 строк кода для модификации 320 из них. Насколько уменьшается вероятность ошибок ценой добавления всего одного объекта! Теперь значение 512 локализовано.

int bufSize = 512; // размер буфера ввода

// ...

for ( int index = 0; index < bufSize; ++index )

      // ...

Остается одна маленькая проблема: переменная bufSize здесь является l-значением, которое можно случайно изменить в программе, что приведет к трудно отлавливаемой ошибке. Вот одна из распространенных ошибок – использование операции присваивания (=) вместо сравнения (==):

// случайное изменение значения bufSize

if ( bufSize = 1 )

     // ...

В результате выполнения этого кода значение bufSize станет равным 1, что может привести к совершенно непредсказуемому поведению программы. Ошибки такого рода обычно очень тяжело обнаружить, поскольку они попросту не видны.

Использование спецификатора const

решает данную проблему. Объявив объект как

const int bufSize = 512; // размер буфера ввода

мы превращаем переменную в константу со значением 512, значение которой не может быть изменено: такие попытки пресекаются компилятором: неверное использование оператора присваивания вместо сравнения, как в приведенном примере, вызовет ошибку компиляции.




// ошибка: попытка присваивания значения константе
if ( bufSize = 0 ) ...

Раз константе нельзя присвоить значение, она должна быть инициализирована в месте своего определения. Определение константы без ее инициализации также вызывает ошибку компиляции:

const double pi; // ошибка: неинициализированная константа

Давайте рассуждать дальше. Явная трансформация значения константы пресекается компилятором. Но как быть с косвенной адресацией? Можно ли присвоить адрес константы некоторому указателю?



const double minWage = 9.60;

// правильно? ошибка?
double *ptr = &minWage;

Должен ли компилятор разрешить подобное присваивание? Поскольку minWage – константа, ей нельзя присвоить значение. С другой стороны, ничто не запрещает нам написать:

*ptr += 1.40; // изменение объекта minWage!

Как правило, компилятор не в состоянии уберечь от использования указателей и не сможет сигнализировать об ошибке в случае подобного их употребления. Для этого требуется слишком глубокий анализ логики программы. Поэтому компилятор просто запрещает присваивание адресов констант обычным указателям.

Что же, мы лишены возможности использовать указатели на константы? Нет. Для этого существуют указатели, объявленные со спецификатором const:

const double *cptr;

где cptr – указатель на объект типа const double. Тонкость заключается в том, что сам указатель – не константа, а значит, мы можем изменять его значение. Например:



const double *pc = 0;

const double minWage = 9.60;

// правильно: не можем изменять minWage с помощью pc

pc = &minWage;

double dval = 3.14;

// правильно: не можем изменять minWage с помощью pc

// хотя dval и не константа

pc = &dval; // правильно

dval = 3.14159; //правильно
*pc = 3.14159; // ошибка

Адрес константного объекта присваивается только указателю на константу. Вместе с тем, такому указателю может быть присвоен и адрес обычной переменной:

pc = &dval;

Константный указатель не позволяет изменять адресуемый им объект с помощью косвенной адресации. Хотя dval в примере выше и не является константой, компилятор не допустит изменения переменной dval



через pc. (Опять- таки потому, что он не в состоянии определить, адрес какого объекта может содержать указатель в произвольный момент выполнения программы.)

В реальных программах указатели на константы чаще всего употребляются как формальные параметры функций. Их использование дает гарантию, что объект, переданный в функцию в качестве фактического аргумента, не будет изменен этой функцией. Например:



// В реальных программах указатели на константы чаще всего

// употребляются как формальные параметры функций
int strcmp( const char *str1, const char *str2 );

(Мы еще поговорим об указателях на константы в главе 7, когда речь пойдет о функциях.)

Существуют и константные указатели. (Обратите внимание на разницу между константным указателем и указателем на константу!). Константный указатель может адресовать как константу, так и переменную. Например:

int errNumb = 0;

int *const currErr = &errNumb;

Здесь curErr – константный указатель на неконстантный объект. Это значит, что мы не можем присвоить ему адрес другого объекта, хотя сам объект допускает модификацию. Вот как мог бы быть использован указатель curErr:



do_something();

if ( *curErr ) {



      errorHandler();

      *curErr = 0; // правильно: обнулим значение errNumb

}

Попытка присвоить значение константному указателю вызовет ошибку компиляции:

curErr = &myErNumb; // ошибка

Константный указатель на константу является объединением двух рассмотренных случаев.



const double pi = 3.14159;
const double *const pi_ptr = &pi;

Ни значение объекта, на который указывает pi_ptr, ни значение самого указателя не может быть изменено в программе.

Упражнение 3.16

Объясните значение следующих пяти определений. Есть ли среди них ошибочные?



(a) int i;          (d) int *const cpi;

(b) const int ic;   (e) const int *const cpic;
(c) const int *pic;

Упражнение 3.17

Какие из приведенных определений правильны? Почему?



(a) int i = -1;

(b) const int ic = i;

(c) const int *pic = &ic;

(d) int *const cpi = &ic;
(e) const int *const cpic = &ic;

Упражнение

3.18

Используя определения из предыдущего упражнения, укажите правильные операторы присваивания. Объясните.



(a) i = ic;     (d) pic = cpic;

(b) pic = &ic;  (i) cpic = &ic;
(c) cpi = pic;  (f) ic = *cpic;


Спецификатор volatile


Объект объявляется как volatile

(неустойчивый, асинхронно изменяемый), если его значение может быть изменено незаметно для компилятора, например переменная, обновляемая значением системных часов. Этот спецификатор сообщает компилятору, что не нужно производить оптимизацию кода для работы с данным объектом.

Спецификатор volatile

используется подобно спецификатору const:

volatile int disp1ay_register;

volatile Task *curr_task;

volatile int ixa[ max_size ];

volatile Screen bitmap_buf;

display_register – неустойчивый объект типа int. curr_task – указатель на неустойчивый объект класса Task. ixa – неустойчивый массив целых, причем каждый элемент такого массива считается неустойчивым. bitmap_buf – неустойчивый объект класса Screen, каждый его член данных также считается неустойчивым.

Единственная цель использования спецификатора volatile – сообщить компилятору, что тот не может определить, кто и как может изменить значение данного объекта. Поэтому компилятор не должен выполнять оптимизацию кода, использующего данный объект.



Список инициализации членов


Модифицируем наш класс Account, объявив член _name

типа string:

#include <string>

class Account {

public:

   // ...

private:

   unsigned int _acct_nmbr;

   double       _balance;

   string       _name;

};

Придется заодно изменить и конструкторы. Возникает две проблемы: поддержание совместимости с первоначальным интерфейсом и инициализация объекта класса с помощью подходящего набора конструкторов.

Исходный конструктор Account с двумя параметрами

Account( const char*, double = 0.0 );

не может инициализировать член типа string. Например:

string new_client( "Steve Hall" );

Account new_acct( new_client, 25000 );

не будет компилироваться, так как не существует неявного преобразования из типа string в тип char*. Инструкция

Account new_acct( new_client.c_str(), 25000 );

правильна, но вызовет у пользователей класса недоумение. Одно из решений– добавить новый конструктор вида:

Account( string, double = 0.0 );

Если написать:

Account new_acct( new_client, 25000 );

вызывается именно этот конструктор, тогда как старый код

Account *open_new_account( const char *nm )

{

   Account *pact = new Account( nm );

   // ...

   return pacct;

}

по-прежнему будет приводить к вызову исходного конструктора с двумя параметрами.

Так как в классе string

определено преобразование из типа char* в тип string

(преобразования классов обсуждаются в этой главе ниже), то можно заменить исходный конструктор на новый, которому в качестве первого параметра передается тип string. В таком случае, когда встречается инструкция:

Account myAcct( "Tinkerbell" );

"Tinkerbell"

преобразуется во временный объект типа string. Затем этот объект передается новому конструктору с двумя параметрами.

При проектировании приходится идти на компромисс между увеличением числа конструкторов класса Account и несколько менее эффективной обработкой аргументов типа char*

из-за необходимости создавать временный объект. Мы предоставили две версии конструктора с двумя параметрами. Тогда модифицированный набор конструкторов Account


будет таким:



#include <string>

class Account {

public:

   Account();

   Account( const char*, double=0.0 );

   Account( const string&, double=0.0 );

   Account( const Account& );

   // ...

private:

   // ...
};

Как правильно инициализировать член, являющийся объектом некоторого класса с собственным набором конструкторов? Этот вопрос можно разделить на три:

1.      где вызывается конструктор по умолчанию? Внутри конструктора по умолчанию класса Account;

2.      где вызывается копирующий конструктор? Внутри копирующего конструктора класса Account и внутри конструктора с двумя параметрами, принимающего в качестве первого тип string;

3.      как передать аргументы конструктору класса, являющегося членом другого класса? Это необходимо делать внутри конструктора Account с двумя параметрами, принимающего в качестве первого тип char*.

Решение заключается в использовании списка инициализации членов (мы упоминали о нем в разделе 14.2). Члены, являющиеся классами, можно явно инициализировать с помощью списка, состоящего из разделенных запятыми пар “имя члена/значение”. Наш конструктор с двумя параметрами теперь выглядит так (напомним, что _name – это член, являющийся объектом класса string):



inline Account::

Account( const char* name, double opening_bal )

       : _name( name ), _balance( opening_bal )

{

       _acct_nmbr = het_unique_acct_nmbr();
}

Список инициализации членов следует за сигнатурой конструктора и отделяется от нее двоеточием. В нем указывается имя члена, а в скобках – начальные значения, что аналогично синтаксису вызова функции. Если член является объектом класса, то эти значения становятся аргументами, передаваемыми подходящему конструктору, который затем и используется. В нашем примере значение name передается конструктору string, который применяется к члену _name. Член _balance

инициализируется значением opening_bal.



Аналогично выглядит второй конструктор с двумя параметрами:



inline Account::

Account( const string& name, double opening_bal )

       : _name( name ), _balance( opening_bal )

{

       _acct_nmbr = het_unique_acct_nmbr();
}

В этом случае вызывается копирующий конструктор string, инициализирующий член _name

значением параметра name

типа string.

Часто у новичков возникает вопрос: в чем разница между использованием списка инициализации и присваиванием значений членам в теле конструктора? Например, в чем разница между



inline Account::

Account( const char* name, double opening_bal )

       : _name( name ), _balance( opening_bal )

{

       _acct_nmbr = het_unique_acct_nmbr();
}

и



Account( const char* name, double opening_bal )

{

       _name = name;

       _balance = opening_bal;

       _acct_nmbr = het_unique_acct_nmbr();
}

В конце работы обоих конструкторов все три члена будут иметь одинаковые значения. Разница в том, что только список обеспечивает инициализацию тех членов, которые являются объектами класса. В теле конструктора установка значения члена – это не инициализация, а присваивание. Важно это различие или нет, зависит от природы члена.

С концептуальной точки зрения выполнение конструктора состоит из двух фаз: фаза явной или неявной инициализации и фаза вычислений, включающая все инструкции в теле конструктора. Любая установка значений членов во второй фазе рассматривается как присваивание, а не инициализация. Непонимание этого различия приводит к ошибкам и неэффективным программам.

Первая фаза может быть явной или неявной в зависимости от того, имеется ли список инициализации членов. При неявной инициализации сначала вызываются конструкторы по умолчанию всех базовых классов в порядке их объявления, а затем конструкторы по умолчанию всех членов, являющихся объектами классов. (Базовые классы мы будем рассматривать в главе 17 при обсуждении объектно-ориентированного программирования.) Например, если написать:



inline Account::

Account()

{

   _name = "";

   _balance = 0.0;

   _acct_nmbr = 0;
<


}

то фаза инициализации будет неявной. Еще до выполнения тела конструктора вызывается конструктор по умолчанию класса string, ассоциированный с членом _name. Это означает, что присваивание _name пустой строки излишне.

Для объектов классов различие между инициализацией и присваиванием существенно. Член, являющийся объектом класса, всегда следует инициализировать с помощью списка, а не присваивать ему значение в теле конструктора. Более правильной является следующая реализация конструктора по умолчанию класса Account:



inline Account::

Account() : _name( string() )

{

   _balance = 0.0;

   _acct_nmbr = 0;
}

Мы удалили ненужное присваивание _name из тела конструктора. Явный же вызов конструктора по умолчанию string

излишен. Ниже приведена эквивалентная, но более компактная версия:



inline Account::

Account()

{

   _balance = 0.0;

   _acct_nmbr = 0;
}

Однако мы еще не ответили на вопрос об инициализации двух членов встроенных типов. Например, так ли существенно, где происходит инициализация _balance: в списке инициализации или в теле конструктора? Инициализация и присваивание членам, не являющимся объектами классов, эквивалентны как с точки зрения результата, так и с точки зрения производительности (за двумя исключениями). Мы предпочитаем использовать список:



// предпочтительный стиль инициализации

inline Account::

Account() : _balance( 0.0 ), _acct_nmbr( 0 )
{}

Два вышеупомянутых исключения – это константные члены и члены-ссылки независимо от типа. Для них всегда нужно использовать список инициализации, в противном случае компилятор выдаст ошибку:



class ConstRef {

public:

   ConstRef(int ii );

private:

   int i;

   const int ci;

   int &ri;

};

ConstRef::

ConstRef( int ii )

{  // присваивание

   i = ii;        // правильно

   ci = ii;       // ошибка: нельзя присваивать константному члену

   ri = i;        // ошибка: ri не инициализирована
}

К началу выполнения тела конструктора инициализация всех константных членов и членов-ссылок должна быть завершена. Для этого нужно указать их в списке инициализации. Правильная реализация предыдущего примера такова:





// правильно: инициализируются константные члены и ссылки

ConstRef::

ConstRef( int ii )

        : ci( ii ), ri ( i )
{ i = ii; }

Каждый член должен встречаться в списке инициализации не более одного раза. Порядок инициализации определяется не порядком следования имен в списке, а порядком объявления членов. Если дано следующее объявление членов класса Account:



class Account {

public:

   // ...

private:

   unsigned int _acct_nmbr;

   double       _balance;

   string       _name;
};

то порядок инициализации для такой реализации конструктора по умолчанию



inline Account::

Account() : _name( string() ), _balance( 0.0 ), _acct_nmbr( 0 )
{}

будет следующим: _acct_nmbr, _balance, _name. Однако члены, указанные в списке (или в неявно инициализируемом члене-объекте класса), всегда инициализируются раньше, чем производится присваивание членам в теле конструктора. Например, в следующем конструкторе:



inline Account::

Account( const char* name, double bal )

       : _name( name ), _balance( bal )

{

       _acct_nmbr = get_unique_acct_nmbr();
}

порядок инициализации такой: _balance, _name, _acct_nmbr.

Расхождение между порядком инициализации и порядком следования членов в соответствующем списке может приводить к трудным для обнаружения ошибкам, когда один член класса используется для инициализации другого:



class X {

   int i;

   int j;

public:

   // видите проблему?

   X( int val )

      : j( val ), i( j )

      {}

   // ...
};

кажется, что перед использованием для инициализации i член j уже инициализирован значением val, но на самом деле i

инициализируется первым, для чего применяется еще неинициализированный член j. Мы рекомендуем помещать инициализацию одного члена другим (если вы считаете это необходимым) в тело конструктора:



// предпочтительная идиома
X::X( int val ) : i( val ) { j = i; }

Упражнение 14.12

Что неверно в следующих определениях конструкторов? Как бы вы исправили обнаруженные ошибки?



(a) Word::Word( char *ps, int count = 1 )

         : _ps( new char[strlen(ps)+1] ),

           _count( count )

    {

         if ( ps )

            strcpy( _ps, ps );

         else {

            _ps = 0;

            _count = 0;

         }
<


    }



(b) class CL1 {

    public:

       CL1() { c.real(0.0); c.imag(0.0); s = "not set"; }

       // ...

    private:

       complex<double> c;

       string s;

    }



 (c) class CL2 {

    public:

       CL2( map<string,location> *pmap, string key )

            : _text( key ), _loc( (*pmap)[key] ) {}

       // ...

    private:

       location _loc;

       string   _text;

};


Список параметров функции


Список параметров не может быть опущен. Функция, которая не требует параметров, должна иметь пустой список либо список, состоящий из одного ключевого слова void. Например, следующие объявления эквивалентны:

int fork();

int fork( void );

Такой список состоит из названий типов, разделенных запятыми. После имени типа может находиться имя параметра, хотя это и необязательно. В списке параметров не разрешается использовать сокращенную запись, соотнося одно имя типа с несколькими параметрами:

int manip( int vl, v2 );     // ошибка

int manip( int vl, int v2 ); // правильно

Имена параметров не могут повторяться. Имена, фигурирующие в определении функции, можно и даже нужно использовать в ее теле. В объявлении же функции они не обязательны и служат средством документирования ее интерфейса. Например:

void print( int *array, int size );

Имена параметров в объявлении и в определении одной и той же функции не обязаны совпадать. Однако употребление разных имен может запутать пользователя.

С++ допускает сосуществование двух или более функций, имеющих одно и то же имя, но разные списки параметров. Такие функции называются перегруженными. О списке параметров в этом случае говорят как о сигнатуре функции, поскольку именно он используется различения разных версий одноименных функций. Имя и сигнатура однозначно идентифицируют версию. (Перегруженные функции подробно обсуждаются в главе 9.)



Сравнительные объекты-функции


Сравнительные объекты-функции поддерживают операции равенства, неравенства, больше, больше или равно, меньше, меньше или равно.

·

Равенство: equal_to<Type>

equal_to<string> stringEqual;

sres = stringEqual( sval1, sval2 );

ires = count_if( svec.begin(), svec.end(),

                 equal_to<string>(), sval1 );

·         Неравенство: not_equal_to<Type>

not_equal_to<complex> complexNotEqual;

cres = complexNotEqual( cval1, cval2 );

ires = count_if( svec.begin(), svec.end(),

                 not_equal_to<string>(), sval1 );

·         Больше: greater<Type>

greater<int> intGreater;

ires = intGreater( ival1, ival2 );

ires = count_if( svec.begin(), svec.end(),

                 greater<string>(), sval1 );

·         Больше или равно: greater_equal<Type>

greater_equal<double> doubleGreaterEqual;

dres = doubleGreaterEqual( dval1, dval2 );

ires = count_if( svec.begin(), svec.end(),

                 greater_equal <string>(), sval1 );

·         Меньше: less<Type>

less<Int> IntLess;

Ires = IntLess( Ival1, Ival2 );

ires = count_if( svec.begin(), svec.end(),

                 less<string>(), sval1 );

·         Меньше или равно: less_equal<Type>

less_equal<int> intLessEqual;

ires = intLessEqual( ival1, ival2 );

ires = count_if( svec.begin(), svec.end(),

                 less_equal<string>(), sval1 );



Ссылки


Фактический аргумент или формальный параметр функции могут быть ссылками. Как это влияет на правила преобразования типов?

Рассмотрим, что происходит, когда ссылкой является фактический аргумент. Его тип никогда не бывает ссылочным. Аргумент-ссылка трактуется как l-значение, тип которого совпадает с типом соответствующего объекта:

int i;

int& ri = i;

void print( int );

int main() {

   print( i );   // аргумент - это lvalue типа int

   print( ri );  // то же самое

   return 0;

}

Фактический аргумент в обоих вызовах имеет тип int. Использование ссылки для его передачи во втором вызове не влияет на сам тип аргумента.

Стандартные преобразования и расширения типов, рассматриваемые компилятором, одинаковы для случаев, когда фактический аргумент является ссылкой на тип T и когда он сам имеет такой тип. Например:

int i;

int& ri = i;

void calc( double );

int main() {

   calc( i );   // стандартное преобразование между целым типом

                // и типом с плавающей точкой

   calc( ri );  // то же самое

   return 0;

}

А как влияет на преобразования, применяемые к фактическому аргументу, формальный параметр-ссылка? Сопоставление дает следующие результаты:

·                  фактический аргумент подходит в качестве инициализатора параметра-ссылки. В таком случае мы говорим, что между ними есть точное соответствие:

void swap( int &, int & );

void manip( int i1, int i2 ) {

   // ...

   swap( i1, i2 );    // правильно: вызывается swap( int &, int & )

   // ...

   return 0;

}

·                  фактический аргумент не может инициализировать параметр-ссылку. В такой ситуации точного соответствия нет, и аргумент нельзя использовать для вызова функции. Например:

int obj;

void frd( double & );

int main() {

   frd( obj );   // ошибка: параметр должен иметь иметь тип const double &

   return 0;

<
}

Вызов функции frd()

является ошибкой. Фактический аргумент имеет тип int и должен быть преобразован в тип double, чтобы соответствовать формальному параметру-ссылке. Результатом такой трансформации является временная переменная. Поскольку ссылка не имеет спецификатора const, то для ее инициализации такие переменные использовать нельзя.

Вот еще один пример, в котором между формальным параметром-ссылкой и фактическим аргументом нет соответствия:



class B;

void takeB( B& );

B giveB();

int main() {

   takeB( giveB() );   // ошибка: параметр должен быть типа const B &

   return 0;
}

Вызов функции takeB() –

ошибка. Фактический аргумент – это возвращаемое значение, т.е. временная переменная, которая не может быть использована для инициализации ссылки без спецификатора const.

В обоих случаях мы видим, что если формальный параметр-ссылка имеет спецификатор const, то между ним и фактическим аргументом может быть установлено точное соответствие.

Следует отметить, что и преобразование l-значения в r-значение, и инициализация ссылки считаются точными соответствиями. В данном примере первый вызов функции приводит к ошибке:



void print( int );

void print( int& );

int iobj;

int &ri = iobj;

int main() {

   print( iobj );   // ошибка: неоднозначность

   print( ri );     // ошибка: неоднозначность

   print( 86 );     // правильно: вызывается print( int )

   return 0;
}

Объект iobj – это аргумент, для которого может быть установлено соответствие с обеими функциями print(), то есть вызов неоднозначен. То же относится и к следующей строке, где ссылка ri

обозначает объект, соответствующий обеим функциям print(). С третьим вызовом, однако, все в порядке. Для него print(int&) не является устоявшей. Целая константа – это r-значение, так что она не может инициализировать параметр-ссылку. Единственной устоявшей функцией для вызова print(86)

является print(int), поэтому она и выбирается при разрешении перегрузки.



Короче говоря, если формальный параметр представляет собой ссылку, то для фактического аргумента точное соответствие устанавливается, если он может инициализировать ссылку, и не устанавливается в противном случае.

Упражнение 9.6

Назовите два тривиальных преобразования, допустимых при установлении точного соответствия.

Упражнение 9.7

Каков ранг каждого из преобразований аргументов в следующих вызовах функций:



(a) void print( int *, int );

    int arr[6];

    print( arr, 6 );   // вызов функции

(b) void manip( int, int );

    manip( 'a', 'z' ); // вызов функции

(c) int calc( int, int );

    double dobj;

    double = calc( 55.4, dobj ) // вызов функции

(d) void set( const int * );

    int *pi;
    set( pi ); // вызов функции

Упражнение 9.8

Какие из данных вызовов ошибочны из-за того, что не существует преобразования между типом фактического аргумента и формального параметра:



(a) enum Stat { Fail, Pass };

    void test( Stat );

    text( 0 ); // вызов функции

(b) void reset( void *);

    reset( 0 ); // вызов функции

(c) void set( void * );

    int *pi;

    set( pi ); // вызов функции

(d) #include <list>

    list<int> oper();

    void print( oper() ); // вызов функции

(e) void print( const int );

    int iobj;
    print( iobj ); // вызов функции


Ссылочный тип


Ссылочный тип, иногда называемый псевдонимом, служит для задания объекту дополнительного имени. Ссылка позволяет косвенно манипулировать объектом, точно так же, как это делается с помощью указателя. Однако эта косвенная манипуляция не требует специального синтаксиса, необходимого для указателей. Обычно ссылки употребляются как формальные параметры функций. В этом разделе мы рассмотрим самостоятельное использование объектов ссылочного типа.

Ссылочный тип обозначается указанием оператора взятия адреса (&) перед именем переменной. Ссылка должна быть инициализирована. Например:

int ival = 1024;

// правильно: refVal - ссылка на ival

int &refVal = ival;

// ошибка: ссылка должна быть инициализирована

int &refVal2;

Хотя, как мы говорили, ссылка очень похожа на указатель, она должна быть инициализирована не адресом объекта, а его значением. Таким объектом может быть и указатель:

int ival = 1024;

// ошибка: refVal имеет тип int, а не int*

int &refVal = &ival;

int *pi = &ival;

// правильно: ptrVal - ссылка на указатель

int *&ptrVal2 = pi;

Определив ссылку, вы уже не сможете изменить ее так, чтобы работать с другим объектом (именно поэтому ссылка должна быть инициализирована в месте своего определения). В следующем примере оператор присваивания не меняет значения refVal, новое значение присваивается переменной ival – ту,  которую адресует refVal.

int min_val = 0;

// ival получает значение min_val,

// а не refVal меняет значение на min_val

refVal = min_val;

Все операции со ссылками реально воздействуют на адресуемые ими объекты. В том числе и операция взятия адреса. Например:

refVal += 2;

прибавляет 2 к ival – переменной, на которую ссылается refVal. Аналогично

int ii = refVal;

присваивает ii

текущее значение ival,

int *pi = &refVal;

инициализирует pi

адресом ival.

Если мы определяем ссылки в одной инструкции через запятую, перед каждым объектом типа ссылки должен стоять амперсанд (&) – оператор взятия адреса (точно так же, как и для указателей). Например:




// определено два объекта типа int

int ival = 1024, ival2 = 2048;

// определена одна ссылка и один объект

int &rval = ival, rval2 = ival2;

// определен один объект, один указатель и одна ссылка

int inal3 = 1024, *pi = ival3, &ri = ival3;

// определены две ссылки
int &rval3 = ival3, &rval4 = ival2;

Константная ссылка может быть инициализирована объектом другого типа (если, конечно, существует возможность преобразования одного типа в другой), а также безадресной величиной – такой, как литеральная константа. Например:



double dval = 3.14159;

// верно только для константных ссылок

const int &ir = 1024;

const int &ir2 = dval;
const double &dr = dval + 1.0;

Если бы мы не указали спецификатор const, все три определения ссылок вызвали бы ошибку компиляции. Однако, причина, по которой компилятор не пропускает таких определений, неясна. Попробуем разобраться.

Для литералов это более или менее понятно: у нас не должно быть возможности косвенно поменять значение литерала, используя указатели или ссылки. Что касается объектов другого типа, то компилятор преобразует исходный объект в некоторый вспомогательный. Например, если мы пишем:



double dval = 1024;
const int &ri = dval;

то компилятор преобразует это примерно так:



int temp = dval;
const int &ri = temp;

Если бы мы могли присвоить новое значение ссылке ri, мы бы реально изменили не dval, а temp. Значение dval

осталось бы тем же, что совершенно неочевидно для программиста. Поэтому компилятор запрещает такие действия, и единственная возможность проинициализировать ссылку объектом другого типа – объявить ее как const.

Вот еще один пример ссылки, который трудно понять с первого раза. Мы хотим определить ссылку на адрес константного объекта, но наш первый вариант вызывает ошибку компиляции:



const int ival = 1024;

// ошибка: нужна константная ссылка
int *&pi_ref = &ival;

Попытка исправить дело добавлением спецификатора const тоже не проходит:





const int ival = 1024;

// все равно ошибка
const int *&pi_ref = &ival;

В чем причина? Внимательно прочитав определение, мы увидим, что pi_ref

является ссылкой на константный указатель на объект типа int. А нам нужен неконстантный указатель на константный объект, поэтому правильной будет следующая запись:



const int ival = 1024;

// правильно
int *const &pi­ref = &ival;

Между ссылкой и указателем существуют два основных отличия. Во-первых, ссылка обязательно должна быть инициализирована в месте своего определения. Во-вторых, всякое изменение ссылки преобразует не ее, а тот объект, на который она ссылается. Рассмотрим на примерах. Если мы пишем:

int *pi = 0;

мы инициализируем указатель pi

нулевым значением, а это значит, что pi не указывает ни на какой объект. В то же время запись

const int &ri = 0;

означает примерно следующее:



int temp = 0;
const int &ri = temp;

Что касается операции присваивания, то в следующем примере:



int ival = 1024, ival2 = 2048;

int *pi = &ival, *pi2 = &ival2;

pi = pi2;

переменная ival, на которую указывает pi, остается неизменной, а pi получает значение адреса переменной ival2. И pi, и pi2 и теперь указывают на один и тот же объект ival2.

Если же мы работаем со ссылками:



int &ri = ival, &ri2 = ival2;

ri = ri2;

то само значение ival

меняется, но ссылка ri

по-прежнему адресует ival.

В реальных С++ программах ссылки редко используются как самостоятельные объекты, обычно они употребляются в качестве формальных параметров функций. Например:



// пример использования ссылок

// Значение возвращается в параметре next_value

bool get_next_value( int &next_value );

// перегруженный оператор
Matrix operator+( const Matrix&, const Matrix& );

Как соотносятся самостоятельные объекты-ссылки и ссылки-параметры? Если мы пишем:



int ival;
while (get_next_value( ival )) ...

это равносильно следующему определению ссылки внутри функции:



int &next_value = ival;

(Подробнее использование ссылок в качестве формальных параметров функций рассматривается в главе 7.)

Упражнение 3.19

Есть ли ошибки в данных определениях? Поясните. Как бы вы их исправили?



(a) int ival = 1.01;       (b) int &rval1 = 1.01;

(c) int &rval2 = ival;     (d) int &rval3 = &ival;

(e) int *pi = &ival;       (f) int &rval4 = pi;

(g) int &rval5 = pi*;      (h) int &*prval1 = pi;
(i) const int &ival2 = 1;  (j) const int &*prval2 = &ival;

Упражнение 3.20

Если ли среди нижеследующих операций присваивания ошибочные (используются определения из предыдущего упражнения)?



(a) rval1 = 3.14159;

(b) prval1 = prval2;

(c) prval2 = rval1;
(d) *prval2 = ival2;

Упражнение 3.21

Найдите ошибки в приведенных инструкциях:



(a) int ival = 0;

    const int *pi = 0;

    const int &ri = 0;

(b) pi = &ival;

    ri = &ival;
    pi = &rval;


Стандартное пространство имен std


Все компоненты стандартной библиотеки С++ находятся в пространстве имен std. Каждая функция, объект и шаблон класса, объявленные в стандартном заголовочном файле, таком, как <vector> или <iostream>, принадлежат к этому пространству.

Если все компоненты библиотеки объявлены в std, то какая ошибка допущена в данном примере:

#include <vector>

#include <string>

#include <iterator>

int main()

{

    // привязка istream_iterator к стандартному вводу

    istream_iterator<string> infile( cin );

    // istream_iterator, отмечающий end-of-stream

    istream_iterator<string> eos;

    // инициализация svec элементами, считываемыми из cin

    vector<string> svec( infile, eos );

    // ...

}

Правильно, этот фрагмент кода не компилируется, потому что члены пространства имен std

должны использоваться с указанием их специфицированных имен. Для того чтобы исправить положение, мы можем выбрать один из следующих способов:

·                  заменить имена членов пространства std в этом примере соответствующими специфицированными именами;

·                  применить using-объявления, чтобы сделать видимыми используемые члены пространства std;

·                  употребить using-директиву, сделав видимыми все члены пространства std.

Членами пространства имен std в этом примере являются: шаблон класса istream_iterator, стандартный входной поток cin, класс string и шаблон класса vector.

Простейшее решение – добавить using-директиву после директивы препроцессора #include:

using namespace std;

В данном примере using-директива делает все члены пространства std

видимыми. Однако не все они нам нужны. Предпочтительнее пользоваться using-объявлениями, чтобы уменьшить вероятность коллизии имен при последующем добавлении в программу глобальных объявлений.


Using-объявления, необходимые для компиляции этого примера, таковы:



using std::istream_iterator;

using std::string;

using std::cin;
using std::vector;

Но куда их поместить? Если программа состоит из большого количества файлов, можно для удобства создать заголовочный файл, содержащий все эти using-объявления, и включать его в исходные файлы вслед за заголовочными файлами стандартной библиотеки.

В нашей книге мы не употребляли using-объявлений. Это сделано, во-первых, для того, чтобы сократить размер кода, а во-вторых, потому, что большинство примеров компилировались в реализации С++, не поддерживающей пространства имен. Подразумевается, что using-объявления указаны для всех членов пространства имен std, используемых в примерах.

Упражнение 8.14

Поясните разницу между using-объявлениями и using-директивами.

Упражнение 8.15

Напишите все необходимые using-объявления для примера из раздела 6.14.

Упражнение 8.16

Возьмем следующий фрагмент кода:



namespace Exercise {

    int ivar = 0;

    double dvar = 0;

    const int limit = 1000;

}

int ivar = 0;

//1

void manip() {

    //2

    double dvar = 3.1416;

    int iobj = limit + 1;

    ++ivar;

    ++::ivar;
}

Каковы будут значения объявлений и выражений, если поместить using-объявления для всех членов пространства имен Exercise в точку //1? В точку //2? А если вместо using-объявлений использовать using-директиву?

9


Стандартный массив – это вектор


Хотя встроенный массив формально и обеспечивает механизм контейнера, он, как мы видели выше, не поддерживает семантику абстракции контейнера. До принятия стандарта C++ для программирования на таком уровне мы должны были либо приобрести нужный класс, либо реализовать его самостоятельно. Теперь же класс массива является частью стандартной библиотеки C++. Только называется он не массив, а вектор.

Разумеется, вектор реализован в виде шаблона класса. Так, мы можем написать

vector<int> ivec(10);

vector<string> svec(10);

Есть два существенных отличия нашей реализации шаблона класса Array от реализации шаблона класса vector. Первое отличие состоит в том, что вектор поддерживает как присваивание значений существующим элементам, так и вставку дополнительных элементов, то есть динамически растет во время выполнения, если программист решил воспользоваться этой его возможностью. Второе отличие более радикально и отражает существенное изменение парадигмы проектирования. Вместо того чтобы поддержать большой набор операций-членов, применимых к вектору, таких, как sort(), min(), max(), find()и так далее, класс vector

предоставляет минимальный набор: операции сравнения на равенство и на меньше, size() и empty(). Более общие операции, перечисленные выше, определены как независимые обобщенные алгоритмы.

Для использования класса vector мы должны включить соответствующий заголовочный файл.

#include <vector>

// разные способы создания объектов типа vector

vector<int> vec0; // пустой вектор

const int size = 8;

const int value = 1024;

// вектор размером 8

// каждый элемент инициализируется 0

vector<int> vec1(size);

// вектор размером 8

// каждый элемент инициализируется числом 1024

vector<int> vec2(size,value);

// вектор размером 4

// инициализируется числами из массива ia

int ia[4] = { 0, 1, 1, 2 };

vector<int> vec3(ia,ia+4);

// vec4 - копия vec2

vector<int> vec4(vec2);


Так же, как наш класс Array, класс vector

поддерживает операцию доступа по индексу. Вот пример перебора всех элементов вектора:



#include <vector>
extern int getSize();

void mumble()

{

  int size = getSize();

  vector<int> vec(size);

  for (int ix=0; ix<size; ++ix)

    vec[ix] = ix;

  // ...

}

Для такого перебора можно также использовать итераторную пару. Итератор – это объект класса, поддерживающего абстракцию указательного типа. В шаблоне класса vector определены две функции-члена – begin() и end(), устанавливающие итератор соответственно на первый элемент вектора и на элемент, который следует за последним. Вместе эти две функции задают диапазон элементов вектора. Используя итератор, предыдущий пример можно переписать таким образом:



#include <vector>
extern int getSize();

void mumble()

{

  int size = getSize();

  vector<int> vec(size);

  vector<int>::iterator iter = vec.begin();

  for (int ix=0; iter!=vec.end(); ++iter, ++ix)

    *iter = ix;

  // ...

}

Определение переменной iter

vector<int>::iterator iter = vec.begin();

инициализирует ее адресом первого элемента вектора vec. iterator определен с помощью typedef в шаблоне класса vector, содержащего элементы типа int. Операция инкремента

++iter

перемещает итератор на следующий элемент вектора. Чтобы получить сам элемент, нужно применить операцию разыменования:

*iter

В стандартной библиотеке С++ имеется поразительно много функций, работающих с классом vector, но определенных не как функции-члены класса, а как набор обобщенных алгоритмов. Вот их неполный перечень:

алгоритмы поиска: find(), find_if(), search(), binary_search(), count(), count_if();

алгоритмы сортировки и упорядочения: sort(), partial_sort(), merge(), partition(), rotate(), reverse(), random_shuffle();

алгоритмы удаления: unique(), remove();

численные алгоритмы: accumulate(), partial_sum(), inner_product(), adjacent_difference();



алгоритмы генерации и изменения последовательности: generate(), fill(), transform(), copy(), for_each();

алгоритмы сравнения: equal(), min(), max().

В число параметров этих обобщенных алгоритмов входит итераторная пара, задающая диапазон элементов вектора, к которым применяется алгоритм. Скажем, чтобы упорядочить все элементы некоторого вектора ivec, достаточно написать следующее:

sort ( ivec.begin(), ivec.end() );

Чтобы применить алгоритм sort()

только к первой половине вектора, мы напишем:

sort ( ivec.begin(), ivec.begin() + ivec.size()/2 );

Роль итераторной пары может играть и пара указателей на элементы встроенного массива. Пусть, например, нам дан массив:

int ia[7] = { 10, 7, 9, 5, 3, 7, 1 };

Упорядочить весь массив можно вызовом алгоритма sort():

sort ( ia, ia+7 );

Так можно упорядочить первые четыре элемента:

sort ( ia, ia+4 );

Для использования алгоритмов в программу необходимо включить заголовочный файл

#include <algorithm>

Ниже приведен пример программы, использующей разнообразные алгоритмы в применении к объекту типа vector:



#include <vector>

#include <algorithm>

#include <iostream>

int ia[ 10 ] = {

    51, 23, 7, 88, 41, 98, 12, 103, 37, 6

};

int main()

{

    vector< int > vec( ia, ia+10 );

    vector<int>::iterator    it = vec.begin(),    end_it = vec.end();

    cout << "Начальный массив: ";

    for ( ; it != end_it; ++ it ) cout << *it << ' ';

    cout << "\n";

    // сортировка массива

    sort( vec.begin(), vec.end() );

    cout << "упорядоченный массив:   ";

    it = vec.begin(); end_it = vec.end();

    for ( ; it != end_it; ++ it ) cout << *it << ' ';

        cout << "\n\n";

    int search_value;

    cout << "Введите значение для поиска: ";

    cin >> search_value;

    // поиск элемента

    vector<int>::iterator found;

    found = find( vec.begin(), vec.end(), search_value );

    if ( found != vec.end() )

         cout << "значение найдено!\n\n";

    else cout << "значение найдено!\n\n";

    // инвертирование массива

    reverse( vec.begin(), vec.end() );

    cout << "инвертированный массив: ";

    it = vec.begin(); end_it = vec.end();

    for ( ; it != end_it; ++ it ) cout << *it << ' ';

      cout << endl;

<


}

Стандартная библиотека С++ поддерживает и ассоциативные массивы. Ассоциативный массив – это массив, элементы которого можно индексировать не только целыми числами, но и значениями любого типа. В терминологии стандартной библиотеки ассоциативный массив называется отображением (map). Например, телефонный справочник может быть представлен в виде ассоциативного массива, где индексами служат фамилии абонентов, а значениями элементов – телефонные номера:



#include <map>
#include <string>

#include "TelephoneNumber.h"

map<string, telephoneNum> telephone_directory;

(Классы векторов, отображений и других контейнеров в подробностях описываются в главе 6. Мы попробуем реализовать систему текстового поиска, используя эти классы. В главе 12 рассмотрены обобщенные алгоритмы, а в Приложении приводятся примеры их использования.)

В данной главе были очень бегло рассмотрены основные аспекты программирования на С++, основы объектно-ориентированного подхода применительно к данному языку и использование стандартной библиотеки. В последующих главах мы разберем эти вопросы более подробно и систематично.

Упражнение 2.22

Поясните результаты каждого из следующих определений вектора:



string pals[] = {

  "pooh", "tiger", "piglet", "eeyore", "kanga" };

(a) vector<string> svec1(pals,pals+5);

(b) vector<int>    ivec1(10);

(c) vector<int>    ivec2(10,10);

(d) vector<string> svec2(svec1);

(e) vector<double> dvec;
Упражнение 2.23

Напишите две реализации функции min(), объявление которой приведено ниже. Функция должна возвращать минимальный элемент массива. Используйте цикл for и перебор элементов с помощью

индекса

итератора



template <class elemType>
elemType min (const vector<elemType> &vec);

Часть II

Основы языка

Код программы и данные, которыми программа манипулирует, записываются в память компьютера в виде последовательности битов. Бит – это мельчайший элемент компьютерной памяти, способная хранить либо 0, либо 1. На физическом уровне это соответствует электрическому напряжению, которое, как известно, либо есть , либо нет. Посмотрев на содержимое памяти компьютера, мы увидим что-нибудь вроде:



00011011011100010110010000111011 ...

Очень трудно придать такой последовательности смысл, но иногда нам приходится манипулировать и подобными неструктурированными данными (обычно нужда в этом возникает при программировании драйверов аппаратных устройств). С++ предоставляет набор операций для работы с битовыми данными. (Мы поговорим об этом в главе 4.)

Как правило, на последовательность битов накладывают какую-либо структуру, группируя биты в байты и слова. Байт содержит 8 бит, а слово – 4 байта, или 32 бита. Однако определение слова может быть разным в разных операционных системах. Сейчас начинается переход к 64-битным системам, а еще недавно были распространены системы с 16-битными словами. Хотя в подавляющем большинстве систем размер байта одинаков, мы все равно будем называть эти величины машинно-зависимыми.

Так выглядит наша последовательность битов, организованная в байты.

Рис 1.

Адресуемая машинная память

Теперь мы можем говорить, например, о байте с адресом 1040 или о слове с адресом 1024 и утверждать, что байт с адресом 1032 не равен байту с адресом 1040.

Однако мы не знаем, что же представляет собой какой-либо байт, какое-либо машинное слово. Как понять смысл тех или иных 8 бит? Для того чтобы однозначно интерпретировать значение этого байта (или слова, или другого набора битов), мы должны знать тип данных, представляемых данным байтом.

С++ предоставляет набор встроенных типов данных: символьный, целый, вещественный – и набор составных и расширенных типов: строки, массивы, комплексные числа. Кроме того, для действий с этими данными имеется базовый набор операций: сравнение, арифметические и другие операции. Есть также операторы переходов, циклов,

условные операторы. Эти элементы языка С++ составляют тот набор кирпичиков, из которых можно построить систему любой сложности. Первым шагом в освоении С++ станет изучение перечисленных базовых элементов, чему и посвящена часть II данной книги.

Глава 3 содержит обзор встроенных и расширенных типов, а также механизмов, с помощью которых можно создавать новые типы. В основном это, конечно, механизм классов, представленный в разделе 2.3. В главе 4 рассматриваются выражения, встроенные операции и их приоритеты, преобразования типов. В главе 5 рассказывается об инструкциях языка. И наконец глава 6 представляет стандартную библиотеку С++ и контейнерные типы – вектор и ассоциативный массив.


Статические члены класса


Иногда нужно, чтобы все объекты некоторого класса имели доступ к единственному глобальному объекту. Допустим, необходимо подсчитать, сколько их было создано; глобальным может быть указатель на процедуру обработки ошибок для класса или, скажем, указатель на свободную память для его объектов. В подобных случаях более эффективно иметь один глобальный объект, используемый всеми объектами класса, чем отдельные члены в каждом объекте. Хотя такой объект является глобальным, он существует лишь для поддержки реализации абстракции класса.

В этой ситуации приемлемым решением является статический член класса, который ведет себя как глобальный объект, принадлежащий своему классу. В отличие от других членов, которые присутствуют в каждом объекте как отдельные элементы данных, статический член существует в единственном экземпляре и связан с самим типом, а не с конкретным его объектом. Это разделяемая сущность, доступная всем объектам одного класса.

По сравнению с глобальным объектом у статического члена есть следующие преимущества:

·

статический член не находится в глобальном пространстве имен программы, следовательно, уменьшается вероятность случайного конфликта имен с другими глобальными объектами;

·                  остается возможность сокрытия информации, так как статический член может быть закрытым, а глобальный объект – никогда.

Чтобы сделать член статическим, надо поместить в начале его объявления в теле класса ключевое слово static. К ним применимы все правила доступа к открытым, закрытым и защищенным членам. Например, для определенного ниже класса Account член _interestRate

объявлен как закрытый и статический типа double:

class Account {                  // расчетный счет

   Account( double amount, const string &owner );

   string owner() { return _owner; }

private:

   static double _interestRate;  // процентная ставка

   double        _amount;        // сумма на счету

   string        _owner;         // владелец

<
};

Почему _interestRate

сделан статическим, а _amount и _owner

нет? Потому что у всех счетов разные владельцы и суммы, но процентная ставка одинакова. Следовательно, объявление члена _interestRate статическим уменьшает объем памяти, необходимый для хранения объекта Account.

Хотя текущее значение _interestRate для всех счетов одинаково, но со временем оно может изменяться. Поэтому мы решили не объявлять этот член как const. Достаточно модифицировать его лишь один раз, и с этого момента все объекты Account будут видеть новое значение. Если бы у каждого объекта была собственная копия, то пришлось бы обновить их все, что неэффективно и является потенциальным источником ошибок.

В общем случае статический член инициализируется вне определения класса. Его имя во внешнем определении должно быть специфицировано именем класса. Вот так можно инициализировать _interestRate:



// явная инициализация статического члена класса

#include "account.h"
double Account::_interestRate = 0.0589;

В программе может быть только одно определение статического члена. Это означает, что инициализацию таких членов следует помещать не в заголовочные файлы, а туда, где находятся определения невстроенных функций-членов класса.

В объявлении статического члена можно указать любой тип. Это могут быть константные объекты, массивы, объекты классов и т.д. Например:



#include <string>

class Account {

   // ...

private:

   static const string name;

};

const string Account::name( "Savings Account" );

Константный статический член целого типа инициализируется константой внутри тела класса: это особый случай. Если бы для хранения названия счета мы решили использовать массив символов вместо строки, то его размер можно было бы задать с помощью константного члена типа int:



// заголовочный файл

class Account {

   //...

private:

   static const int nameSize = 16;

   static const string name[nameSize];

};

// исходный файл

const string Account::nameSize;   // необходимо определение члена
<


const string Account::name[nameSize] = "Savings Account";

Отметим, что константный статический член целого типа, инициализированный константой, – это константное выражение. Проектировщик может объявить такой статический член, если внутри тела класса возникает необходимость в именованной константе. Например, поскольку константный статический член nameSize является константным выражением, проектировщик использует его для задания размера члена-массива с именем name.

Даже если такой член инициализируется в теле класса, его все равно необходимо задать вне определения класса. Однако поскольку начальное значение уже задано в объявлении, то при определении оно не указывается.

Так как name – это массив (и не целого типа), его нельзя инициализировать в теле класса. Попытка поступить таким образом приведет к ошибке компиляции:



class Account {

   //...

private:

   static const int nameSize = 16;   // правильно: целый тип

   static const string name[nameSize] = "Savings Account";  // ошибка
};

Член name

должен быть инициализирован вне определения класса.

Обратите внимание, что член nameSize

задает размер массива name в определении, находящемся вне тела класса:

const string Account::name[nameSize] = "Savings Account";

nameSize не квалифицирован именем класса Account. И хотя это закрытый член, определение name не приводит к ошибке. Как такое может быть? Определение статического члена аналогично определению функции-члена класса, которое может ссылаться на закрытые члены. Определение статического члена name

находится в области видимости класса и может ссылаться на закрытые члены, после того как распознано квалифицированное имя Account::name. (Подробнее об области видимости класса мы поговорим в разделе 13.9.)

Статический член класса доступен функции-члену того же класса и без использования соответствующих операторов:



inline double Account::dailyReturn()

{

   return( _interestRate / 365 * _amount );
}

Что же касается функций, не являющихся членами класса, то они могут обращаться к статическому члену двумя способами. Во-первых, посредством операторов доступа:





class Account {

   // ...

private:

   friend int compareRevenue( Account&, Account* );

   // остальное без изменения

};

// мы используем ссылочный и указательный параметры,

// чтобы проиллюстрировать оба оператора доступа

int compareRevenue( Account &ac1, Account *ac2 );

{

   double ret1, ret2;

   ret1 = ac1._interestRate * ac1._amount;

   ret2 = ac2->_interestRate * ac2->_amount;

   // ...
}

Как ac1._interestRate, так и ac2->_interestRate

относятся к статическому члену Account::_interestRate.

Поскольку есть лишь одна копия статического члена класса, до нее необязательно добираться через объект или указатель. Другой способ заключается в том, чтобы обратиться к статическому члену напрямую, квалифицировав его имя именем класса:



// доступ к статическому члену с указанием квалифицированного имени
if ( Account::_interestRate < 0.05 )

Если обращение к статическому члену производится без помощи оператора доступа, то его имя следует квалифицировать именем класса, за которым следует оператор разрешения области видимости:

Account::

Это необходимо, поскольку такой член не является глобальным объектом, а значит, в глобальной области видимости отсутствует. Следующее определение дружественной функции compareRevenue

эквивалентно приведенному выше:



int compareRevenue( Account &ac1, Account *ac2 );

{

   double ret1, ret2;

   ret1 = Account::_interestRate * ac1._amount;

   ret2 = Account::_interestRate * ac2->_amount;

   // ...
}

Уникальная особенность статического члена – то, что он существует независимо от объектов класса, – позволяет использовать его такими способами, которые для нестатических членов недопустимы.

·                  статический член может принадлежать к типу того же класса, членом которого он является. Нестатические объявляются лишь как указатели или ссылки на объект своего класса:



class Bar {

public:

   // ...

private:

   static Bar mem1;   // правильно

   Bar *mem2;         // правильно

   Bar mem3;          // ошибка
<


};

·                  статический член может выступать в роли аргумента по умолчанию для функции-члена класса, а для нестатического это запрещено:



extern int var;

class Foo {

private:

   int var;

   static int stcvar;

public:

   // ошибка: трактуется как Foo::var,

   // но ассоциированного объекта класса не существует

   int mem1( int = var );

   // правильно: трактуется как static Foo::stcvar,

   // ассоциированный объект и не нужен

   int mem2( int = stcvar );

   // правильно: трактуется как глобальная переменная var

   int mem3( int = :: var );
};


Статические члены шаблонов класса


В шаблоне класса могут быть объявлены статические данные-члены. Каждый конкретизированный экземпляр имеет собственный набор таких членов. Рассмотрим операторы new() и delete() для шаблона QueueItem. В класс QueueItem

нужно добавить два статических члена:

static QueueItem<Type> *free_list;

static const unsigned QueueItem_chunk;

Модифицированное определение шаблона QueueItem

выглядит так:

#include <cstddef>

template <class Type>

class QueueItem {

   // ...

private:

   void *operator new( size_t );

   void operator delete( void *, size_t );

   // ...

   static QueueItem *free_list;

   static const unsigned QueueItem_chunk;

   // ...

};

Операторы new() и delete()

объявлены закрытыми, чтобы предотвратить создание объектов типа QueueItem

вызывающей программой: это разрешается только членам и друзьям QueueItem (к примеру, шаблону Queue).

Оператор new()

можно реализовать таким образом:

template <class Type> void*

   QueueItem<Type>::operator new( size_t size )

{

   QueueItem<Type> *p;

   if ( ! free_list )

   {

      size_t chunk = QueueItem_chunk * size;

      free_list = p =

      reinterpret_cast< QueueItem<Type>* >

                      ( new char[chunk] );

      for ( ; p != &free_list[ QueueItem_chunk - 1 ]; ++p )

         p->next = p + 1;

      p->next = 0;

   }

   p = free_list;

   free_list = free_list->next;

   return p;

}

А реализация оператора delete()

выглядит так:

template <class Type>

void QueueItem<Type>::

   operator delete( void *p, size_t )

{

   static_cast< QueueItem<Type>* >( p )->next = free_list;

   free_list = static_cast< QueueItem<Type>* > ( p );

}

Теперь остается инициализировать статические члены free_list и QueueItem_chunk. Вот шаблон для определения статических данных-членов:

/* для каждой конкретизации QueueItem сгенерировать

 * соответствующий free_list и инициализировать его нулем

 */

template <class T>

   QueueItem<T> *QueueItem<T>::free_list = 0;

/* для каждой конкретизации QueueItem сгенерировать

 * соответствующий QueueItem_chunk и инициализировать его значением 24

 */

template <class T>

   const unsigned int

<
   QueueItem<T>::QueueItem_chunk = 24;

Определение шаблона статического члена должно быть вынесено за пределы определения самого шаблона класса, которое начинается с ключевого слово template с последующим списком параметров <class T>. Имени статического члена предшествует префикс QueueItem<T>::,

показывающий, что этот член принадлежит именно шаблону QueueItem. Определения таких членов помещаются в заголовочный файл Queue.h и должны включаться во все файлы, где производится их конкретизация. (В разделе 16.8 мы объясним, почему решили делать именно так, и затронем другие вопросы, касающиеся модели компиляции шаблонов.)

Статический член конкретизируется по шаблону только в том случае, когда реально используется в программе. Сам такой член тоже является шаблоном. Определение шаблона для него не приводит к выделению памяти: она выделяется только для конкретизированного экземпляра статического члена. Каждая подобная конкретизация соответствует конкретизации шаблона класса. Таким образом, обращение к экземпляру статического члена всегда производится через некоторый конкретизированный экземпляр класса:



// ошибка: QueueItem - это не реальный конкретизированный экземпляр

int ival0 = QueueItem::QueueItem_chunk;

int ival1 = QueueItem<string>::QueueItem_chunk;  // правильно
int ival2 = QueueItem<int>::QueueItem_chunk;     // правильно

Упражнение 16.7

Реализуйте определенные в разделе 15.8 операторы new() и delete() и относящиеся к ним статические члены screenChunk и freeStore для шаблона класса Screen, построенного в упражнении 16.6.


Статические функции-члены


Функции-члены raiseInterest() и interest()

обращаются к глобальному статическому члену _interestRate:

class Account {

public:

   void raiseInterest( double incr );

   double interest() { return _interestRate; }

private:

   static double  _interestRate;

};

inline void Account::raiseInterest( double incr )

{

  _interestRate += incr;

}

Проблема в том, что любая функция-член должна вызываться с помощью оператора доступа к конкретному объекту класса. Поскольку приведенные выше функции обращаются только к статическому _interestRate, то совершенно безразлично, для какого объекта они вызываются. Нестатические члены при вызове этих функций не читаются и не модифицируются.

Поэтому лучше объявить такие функции-члены как статические. Это можно сделать следующим образом:

class Account {

public:

   static void raiseInterest( double incr );

   static double interest() { return _interestRate; }

private:

   static double  _interestRate;

};

inline void Account::raiseInterest( double incr )

{

  _interestRate += incr;

}

Объявление статической функции-члена почти такое же, как и нестатической: в теле класса ему предшествует ключевое слово static, а спецификаторы const или volatile

запрещены. В ее определении, находящемся вне тела класса, слова static

быть не должно.

Такой функции-члену указатель this не передается, поэтому явное или неявное обращение к нему внутри ее тела вызывает ошибку компиляции. В частности, попытка обращения к нестатическому члену класса неявно требует наличия указателя this и, следовательно, запрещена. Например, представленную ранее функцию-член dailyReturn() нельзя объявить статической, поскольку она обращается к нестатическому члену _amount.

Статическую функцию-член можно вызвать для объекта класса, пользуясь одним из операторов доступа. Ее также можно вызвать непосредственно, квалифицировав ее имя, даже если никаких объектов класса не объявлено. Вот небольшая программа, иллюстрирующая их применение:

#include <iostream>

#include "account.h"

bool limitTest( double limit )

{

   // пока еще ни одного объекта класса Account не объявлено

   // правильно: вызов статической функции-члена

   return limit <= Account::interest() ;

}

int main() {

   double limit = 0.05;

   if ( limitTest( limit ) )

   {

      // указатель на статическую функцию-член

      // объявлен как обычный указатель

      void (*psf)(double) = &Account::raiseInterest;

      psf( 0.0025 );

   }

   Account ac1( 5000, "Asterix" );

   Account ac2( 10000, "Obelix" );

   if ( compareRevenue( ac1, &ac2 ) > 0 )

      cout << ac1.owner()

           << " is richer than "

           << ac2.owner() << "\n";

   else

      cout << ac1.owner()

           << " is poorer than "

           << ac2.owner() << "\n";

   return 0;

<
}

Упражнение 13.8

Пусть дан класс Y с двумя статическими данными-членами и двумя статическими функциями-членами:



class X {

public:

   X( int i ) { _val = i; }

   int val() { return _val; }

private:

   int _val;

};

class Y {

public:

   Y( int i );

   static X xval();

   static int callsXval();

private:

   static X _xval;

   static int _callsXval;
};

Инициализируйте _xval

значением 20, а _callsXval

значением 0.

Упражнение 13.9

Используя классы из упражнения 13.8, реализуйте обе статические функции-члена для класса Y. callsXval() должна подсчитывать, сколько раз вызывалась xval().

Упражнение 13.10

Какие из следующих объявлений и определений статических членов ошибочны? Почему?



// example.h

class Example {

public:

   static double rate = 6.5;

   static const int vecSize = 20;

   static vector<double> vec(vecSize);

};

// example.c

#include "example.h"

double Example::rate;
vector<double> Example::vec;


Статические локальные объекты


Внутри функции или составной инструкции можно объявить объект с локальной областью видимости, который, однако, будет существовать в течение всего времени выполнения программы. Если значение локального объекта должно сохраняться между вызовами функции, то обычный автоматический объект не подойдет: ведь его значение теряется каждый раз после выхода.

В таком случае локальный объект необходимо объявить как static (со статической продолжительностью хранения). Хотя значение такого объекта сохраняется между вызовами функции, в которой он определен, видимость его имени ограничена локальной областью. Статический локальный объект инициализируется во время первого выполнения инструкции, где он объявлен. Вот, например, версия функции gcd(),устанавливающая глубину рекурсии с его помощью:

#include <iostream>

int traceGcd( int vl, int v2 )

{

    static int depth = 1;

    cout << "глубина #" << depth++ << endl;

    if ( v2 == 0 ) {

        depth = 1;

        return vl;

    }

    return traceGcd( v2, vl%v2 );

}

Значение, ассоциированное со статическим локальным объектом depth, сохраняется между вызовами traceGcd(). Его инициализация выполняется только один раз – когда к этой функции обращаются впервые. В следующей программе используется traceGcd():

#include <iostream>

extern int traceGcd(int, int);

int main() {

    int rslt = traceCcd( 15, 123 );

    cout << "НОД (15,123): " << rslt << endl;

    return 0;

}

Результат работы программы:

глубина #1

глубина #2

глубина #3

глубина #4

НОД (15,123): 3

Неинициализированные статические локальные объекты получают значение 0. А автоматические объекты в подобной ситуации получают случайные значения. Следующая программа иллюстрирует разницу инициализации по умолчанию для автоматических и статических объектов и опасность, подстерегающую программиста в случае ее отсутствия для автоматических объектов.

#include <iostream>

const int iterations = 2;

void func() {

    int value1, value2; // не инициализированы

    static int depth;   // неявно инициализирован нулем

    if ( depth < iterations )

        { ++depth; func(); }

    else depth = 0;

    cout << "\nvaluel:\t" << value1;

    cout << "\tvalue2:\t" << value2;

    cout << "\tsum:\t" << value1 + value2;

}

int main() {

    for ( int ix = 0; ix < iterations; ++ix ) func();

    return 0;

}

Вот результат работы программы:

valuel:  0           value2:  74924      sum:  74924

valuel:  0           value2:  68748      sum:  68748

valuel:  0           value2:  68756      sum:  68756

valuel:  148620      value2:  2350       sum:  150970

valuel:  2147479844  value2:  671088640  sum:  -1476398812

valuel:  0           value2:  68756      sum:  68756

value1 и

value2 – неинициализированные автоматические объекты. Их начальные значения, как можно видеть из приведенной распечатки, оказываются случайными, и потому результаты сложения непредсказуемы. Объект depth, несмотря на отсутствие явной инициализации, гарантированно получает значение 0, и функция func()

рекурсивно вызывает сама себя только дважды.



Статический вызов виртуальной функции


Вызывая виртуальную функцию с помощью оператора разрешения области видимости класса, мы отменяем механизм виртуализации и разрешаем вызов статически, на этапе компиляции. Предположим, что мы определили виртуальную функцию isA() в базовом и каждом из производных классов иерархии Query:

Query *pquery = new NameQuery( "dumbo" );

// isA() вызывается динамически с помощью механизма виртуализации

// реально будет вызвана NameQuery::isA()

pquery->isA();

// isA вызывается статически во время компиляции

// реально будет вызвана Query::isA

pquery->Query::isA();

Тогда явный вызов Query::isA()

разрешается на этапе компиляции в пользу реализации isA() в базовом классе Query, хотя pquery

адресует объект NameQuery.

Зачем нужно отменять механизм виртуализации? Как правило, ради эффективности. В теле виртуальной функции производного класса часто необходимо вызвать реализацию из базового, чтобы завершить операцию, расщепленную между базовым и производным классами. К примеру, вполне вероятно, что виртуальная функция display() из Camera

выводит некоторую информацию, общую для всех камер, а реализация display() в классе PerspectiveCamera

сообщает информацию, специфичную только для перспективных камер. Вместо того чтобы дублировать в ней действия, общие для всех камер, можно вызвать реализацию из класса Camera. Мы точно знаем, какая именно реализация нам нужна, поэтому нет нужды прибегать к механизму виртуализации. Более того, реализация в Camera

объявлена встроенной, так что разрешение во время компиляции приводит к подстановке по месту вызова.

Приведем еще один пример, когда отмена механизма виртуализации может оказаться полезной, а заодно познакомимся с неким аспектом чисто виртуальных функций, который начинающим программистам кажется противоречащим интуиции.

Реализации функции print() в классах AndQuery и OrQuery

совпадают во всем, кроме литеральной строки, представляющей название оператора. Реализуем только одну функцию, которую можно вызывать из данных классов. Для этого мы снова определим абстрактный базовый BinaryQuery (его наследники – AndQuery и OrQuery). В нем определены два операнда и еще один член типа string для хранения значения оператора. Поскольку это абстрактный класс, объявим print()


чисто виртуальной функцией:



class BinaryQuery : public Query {

public:

   BinaryQuery( Query *lop, Query *rop, string oper )

              : _lop(lop), _rop(rop), _oper(oper) {}

   ~BinaryQuery() { delete _lop; delete _rop; }

   ostream &print( ostream&=cout, ) const = 0;

protected:

   Query *_lop;

   Query *_rop;

   string _oper;
};

Вот как реализована в BinaryQuery

функция print(), которая будет вызываться из производных классов AndQuery и OrQuery:



inline ostream&

BinaryQuery::

print( ostream &os ) const

{

   if ( _lparen )

            print_lparen( _lparen, os );

   _lop->print( os );

   os << ' ' << _oper << ' ';

   _rop->print( os );

   if ( _rparen )

            print_rparen( _rparen, os );

   return os;
}

Похоже, мы попали в парадоксальную ситуацию. С одной стороны, необходимо объявить этот экземпляр print() как чисто виртуальную функцию, чтобы компилятор воспринимал BinaryQuery как абстрактный базовый класс. Тогда в приложении определить независимые объекты BinaryQuery

будет невозможно.

С другой стороны, нужно определить в классе BinaryQuery виртуальную функцию print() и уметь вызывать ее через объекты AndQuery и OrQuery.

Но как часто бывает с кажущимися парадоксами, мы не учли одного обстоятельства: чисто виртуальную функцию нельзя вызывать с помощью механизма виртуализации, но можно вызывать статически:



inline ostream&

AndQuery::

print( ostream &os ) const

{

   // правильно: подавить механизм виртуализации

   // вызвать BinaryQuery::print статически

   BinaryQuery::print( os );
}


Стек


В разделе 4.5 операции инкремента и декремента были проиллюстрированы на примере реализации абстракции стека. В общем случае стек является очень полезным механизмом для сохранения текущего состояния, если в разные моменты выполнения программы одновременно существует несколько состояний, вложенных друг в друга.

Поскольку стек– это важная абстракция данных, в стандартной библиотеке С++ предусмотрен класс stack, для использования которого нужно включить заголовочный файл:

#include <stack>

В стандартной библиотеке стек реализован несколько иначе, чем у нас. Разница состоит в том, что доступ к элементу с вершины стека и удаление его осуществляются двумя функциями – top() и pop(). Полный набор операций со стеком приведен в таблице 6.5.

Таблица 6.5. Операции со стеком

Операция

Действие

empty()

Возвращает true, если стек пуст, и false в противном случае

size()

Возвращает количество элементов в стеке

pop()

Удаляет элемент с вершины стека, но не возвращает его значения

top()

Возвращает значение элемента с вершины стека, но не удаляет его

push(item)

Помещает новый элемент в стек

В нашей программе приводятся примеры использования этих операций:

#include <stack>

#include <iostream>

int main()

{

const int ia_size = 10;

int ia[ia_size ]={0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

// заполним стек

int ix = 0;

stack< int > intStack;

for ( ; ix < ia_size; ++ix )

    intStack.push( ia[ ix ] );

int error_cnt = 0;

if ( intStack.size() != ia_size ) {

    cerr << "Ошибка! неверный размер IntStack: "

         << intStack.size()

         << "\t ожидается: " << ia_size << endl,

    ++error_cnt;

}

int value;

while ( intStack.empty() == false )

{

    // считаем элемент с вершины

    value = intStack.top();

    if ( value != --ix ) {

        cerr << "Ошибка! ожидается " << ix

             << " получено " << value << endl;

        ++error_cnt;

    }

    // удалим элемент

    intStack.pop();

}

cout << "В результате запуска программы получено "

<< error_cnt << " ошибок" << endl;

<
}

Объявление

stack< int > intStack;

определяет intStack как пустой стек, предназначенный для хранения элементов типа int. Стек является надстройкой над некоторым контейнерным типом, поскольку реализуется с помощью того или иного контейнера. По умолчанию это deque, поскольку именно эта структура обеспечивает эффективную вставку и удаление первого элемента, а vector эти операции не поддерживает. Однако мы можем явно указать другой тип контейнера, задав его как второй параметр:

stack< int, list<int> > intStack;

Элементы, добавляемые в стек, копируются в реализующий его контейнер. Это может приводить к потере эффективности для больших или сложных объектов, особенно если мы только читаем элементы. В таком случае удобнее определить стек указателей на объекты. Например:



#include <stack>

class NurbSurface { /* mumble */ };
stack< NurbSurface* > surf_Stack;

К двум стекам одного типа можно применять операции сравнения: равенство, неравенство, меньше, больше, меньше или равно, больше или равно, если они определены над элементами стека. Элементы сопоставляются попарно. Первая пара несовпадающих элементов определяет результат операции сравнения в целом.

Стек будет использован в нашей программе текстового поиска в разделе 17.7 для поддержки сложных запросов типа

Civil && ( War || Rights )


Строим отображение позиций слов


В этом разделе мы построим отображение (map), позволяющее для каждого уникального слова текста сохранить номера строк и колонок, в которых оно встречается. (В следующем разделе мы изучим ассоциативный контейнер set.) В общем случае контейнер set полезен, если мы хотим знать, содержится ли определенный элемент в некотором множестве, а map

позволяет связать с каждым из них какую-либо величину.

В map хранятся пары ключ/значение. Ключ играет роль индекса для доступа к ассоциированному с ним значению. В нашей программе каждое уникальное слово текста будет служить ключом, а значением станет вектор, содержащий пары (номер строки, номер колонки). Для доступа применяется оператор взятия индекса. Например:

string query( "pickle" );

vector< location > *locat;

// возвращается location<vector>*, ассоциированный с "pickle"

locat = text_map[ query ];

Ключом здесь является строка, а значение имеет тип location<vector>*.

Для использования отображения необходимо включить соответствующий заголовочный файл:

#include <map>

Какие основные действия производятся над ассоциативными контейнерами? Их заполняют элементами или проверяют на наличие определенного элемента. В следующем подразделе мы покажем, как определить пару ключ/значение и как поместить такие пары в контейнер. Далее мы расскажем, как сформулировать запрос на поиск элемента и извлечь значение, если элемент существует.



Строковые потоки


Библиотека iostream

поддерживает операции над строковыми объектами в памяти. Класс ostringstream

вставляет символы в строку, istringstream читает символы из строкового объекта, а stringstream

может использоваться как для чтения, так и для записи. Чтобы работать со строковым потоком, в программу необходимо включить заголовочный файл

#include <sstream>

Например, следующая функция читает весь файл alice_emma в объект buf

класса ostringstream. Размер buf

увеличивается по мере необходимости, чтобы вместить все символы:

#include <string>

#include <fstream>

#include <sstream>

string read_file_into_string()

{

           ifstream ifile( "alice_emma" );

           ostringstream buf;

           char ch;

           while ( buf && ifile.get( ch ))

                   buf.put( ch );

           return buf.str();

}

Функция-член str()

возвращает строку – объект класса string, ассоциированный со строковым потоком ostringstream. Этой строкой можно манипулировать так же, как и “обычным” объектом класса string. Например, в следующей программе text почленно инициализируется строкой, ассоциированной с buf:

int main()

{

           string text = read_file_into_string();

           // запомнить позиции каждого символа новой строки

           vector< string::size_type > lines_of_text;

           string::size_type pos = 0;

           while ( pos != string::npos )

           {

                  pos = text.find( '\n' pos );

                  lines_of_text.push_back( pos );

           }

           // ...

}

Объект класса ostringstream

можно использовать для автоматического форматирования составной строки, т.е. строки, составленной из данных разных типов. Так, следующий оператор вывода автоматически преобразует любой арифметический тип в соответствующее строковое представление, поэтому заботиться о выделении нужного количества памяти нет необходимости:

#include <iostream>

#include <sstream>

int main()

{

           int    ival  = 1024;     int   *pival = &ival;

           double  dval  = 3.14159; double *pdval = &dval;

           ostringstream format_message;

           // преобразование значений в строковое представление

           format_message << "ival: "  << ival

                          << " адрес ival: " << pival << 'n'

                          << "dval: " << dval

                          << " адрес dval: " << pdval << endl;

           string msg = format_message.str();

           cout << " размер строки сообщения: " << msg.size()

                << " сообщение: " << msg << endl;

<
}

Иногда лучше собрать все диагностические сообщения об ошибках, а не выводить их по мере возникновения. Это легко сделать с помощью перегруженного множества функций форматирования:



string

format( string msg, int expected, int received )

{

   ostringstream message;

   message << msg << " ожидалось: " << expected

           << " принято: " << received << "\n";

   return message.str();

}

string format( string msg, vector<int> *values );
// ... и так далее

Приложение может сохранить такие строки для последующего отображения и даже рассортировать их по серьезности. Обобщить эту идею помогают классы Notify

(извещение), Log

(протокол) и Error

(ошибка).

Поток istringstream

читает из объекта класса string, с помощью которого был сконструирован. В частности, он применяется для преобразования строкового представления числа в его арифметическое значение:



#include <iostream>

#include <sstream>

#include <string>

int main()

{

           int    ival = 1024;    int *pival = &ival;

           double dval = 3.14159; double *pdval = &dval;

           // создает строку, в которой значения разделены пробелами

           ostringstream format_string;

           format_string << ival << " " << pival << " "

                   << dval << " " << pdval << endl;

           // извлекает сохраненные значения в коде ASCII

           // и помещает их в четыре разных объекта

           istringstream input_istring( format_string.str() );

           input_istring >> ival >> pival

                   >> dval >> pdval;
}

Упражнение 20.16

В языке Си форматирование выходного сообщения производится с помощью функций семейства printf(). Например, следующий фрагмент



int    ival = 1024;

double dval = 3.14159;

char   cval = 'a';

char  *sval = "the end";

printf( "ival: %d\tdval% %g\tcval: %c\tsval: %s",
<


         ival, dval, cval, sval );

печатает:

ival: 1024   dval: 3.14159  cval: a   sval: the end

Первым аргументом printf()

является форматная строка. Каждый символ % показывает, что вместо него должно быть подставлено значение аргумента, а следующий за ним символ определяет тип этого аргумента. Вот некоторые из поддерживаемых типов (полное описание см. в [KERNIGHAN88]):



%d                целое число

%g                число с плавающей точкой

%c                char
%s                C-строка

Дополнительные аргументы printf() на позиционной основе сопоставляются со спецификаторами формата, начинающимися со знака %. Все остальные символы в форматной строке рассматриваются как литералы и выводятся буквально.

Основные недостатки семейства функций printf()

таковы: во-первых, форматная строка не обобщается на определенные пользователем типы, и, во-вторых, если типы или число аргументов не соответствуют форматной строке, компилятор не заметит ошибки, а вывод будет отформатирован неверно. Однако у функций printf()

есть и достоинство – компактность записи.

1.      Получите так же отформатированный результат с помощью объекта класса ostringstream.

2.      Сформулируйте достоинства и недостатки обоих подходов.


Строковые типы


В С++ поддерживаются два типа строк– встроенный тип, доставшийся от С, и класс string из стандартной библиотеки С++. Класс string предоставляет гораздо больше возможностей и поэтому удобней в применении, однако на практике нередки ситуации, когда необходимо пользоваться встроенным типом либо хорошо понимать, как он устроен. (Одним из примеров может являться разбор параметров командной строки, передаваемых в функцию main(). Мы рассмотрим это в главе 7.)



Строковый ввод


Считывание можно производить как в C-строки, так и в объекты класса string. Мы рекомендуем пользоваться последними. Их главное преимущество – автоматическое управление памятью для хранения символов. Чтобы прочитать данные в C-строку, т.е. массив символов, необходимо сначала задать его размер, достаточный для хранения строки. Обычно мы читаем символы в буфер, затем выделяем из хипа ровно столько памяти, сколько нужно для хранения прочитанной строки, и копируем данные из буфера в эту память:

#include <iostream>

#include <string.h>

char inBuf[ 1024 ];

try

{

   while ( cin >> inBuf ) {

           char *str = new char[ strlen( inBuf ) + 1 ];

           strcpy( str, inBuf );

           // ... сделать что-то с массивом символов str

           delete [] str;

   }

}

catch( ... ) { delete [] str; throw; }

Работать с типом string

значительно проще:

#include <iostream>

#include <string.h>

string str;

while ( cin >> str )

        // ... сделать что-то со строкой

Рассмотрим операторы ввода в C-строки и в объекты класса string. В качестве входного текста по-прежнему будет использоваться рассказ об Алисе Эмме:

Alice Emma has long flowing red hair. Her Daddy says

when the wind blows through her hair, it looks almost

alive, like a fiery bird in flight. A beautiful fiery

bird, he tells her, magical but untamed. "Daddy, shush,

there is no such creature," she tells him, at the same time

wanting him to tell her more. Shyly, she asks, "I mean,

Daddy, is there?"

Поместим этот текст в файл alice_emma, а затем перенаправим на него стандартный вход программы. Позже, когда мы познакомимся с файловым вводом, мы откроем и прочтем этот файл непосредственно. Следующая программа помещает прочитанные со стандартного ввода слова в C-строку и находит самое длинное слово:

#include <iostream.h>

#include <string.h>

int main()

{

           const int bufSize = 24;

           char buf[ bufSize ], largest[ bufSize ];

           // для хранения статистики

           int curLen, max = -1, cnt = 0;

           while ( cin >> buf )

           {

                  curLen = strlen( buf );

                  ++cnt;

                  // новое самое длинное слово? сохраним его

                  if ( curLen > max ) {

              max = curLen;

              strcpy( largest, buf );

                  }

           }

           cout << "Число прочитанных слов "

                << cnt << endl;

     cout << "Длина самого длинного слова "

                << max << endl;

           cout << "Самое длинное слово "

                << largest << endl;

<
}         

После компиляции и запуска программа выводит следующие сведения:

Число прочитанных слов 65

Длина самого длинного слова 10

Самое длинное слово creature,"

На самом деле этот результат неправилен: самое длинное слово beautiful, в нем девять букв. Однако выбрано creature, потому что программа сочла его частью запятую и кавычку. Следовательно, необходимо отфильтровать небуквенные символы.

Но прежде чем заняться этим, рассмотрим программу внимательнее. В ней каждое слово помещается в массив buf, длина которого равна 24. Если бы в тексте попалось слово длиной 24 символа (или более), то буфер переполнился бы и программа, вероятно, закончилась бы крахом. Чтобы предотвратить переполнение входного массива, можно воспользоваться манипулятором setw(). Модифицируем предыдущую программу:

while ( cin >> setw( bufSize ) >> buf )

Здесь bufSize – размер массива символов buf. setw() разбивает строку длиной bufSize или больше на несколько строк, каждая из которых не длиннее, чем bufSize - 1.

Завершается такая частичная строка двоичным нулем. Для использования setw() в программу необходимо включить заголовочный файл iomanip:

#include <iomanip>

Если в объявлении массива buf

размер явно не указан:

char buf[] = "Нереалистичный пример";

то программист может применить оператор sizeof, но при условии, что идентификатор является именем массива и находится в области видимости выражения:

while ( cin >> setw(sizeof( buf )) >> buf )

Применение оператора sizeof в следующем примере дает неожиданный результат:



#include <iostream>

#include <iomanip>

int main()

{

           const int bufSize = 24;

           char buf[ bufSize ];

           char *pbuf = buf;

           // если строка длиннее, чем sizeof(char*),

           // она разбивается на несколько строк

           while ( cin >> setw( sizeof( pbuf )) >> pbuf )

             cout << pbuf << endl;
<


}

Программа печатает:

$ a.out

The winter of our discontent

The

win

ter

of

our

dis

con

ten

t

Функции setw()

вместо размера массива передается размер указателя, длина которого на нашей машине равна четырем байтам, поэтому вывод разбит на строки по три символа.

Попытка исправить ошибку приводит к еще более серьезной проблеме:

while ( cin >> setw(sizeof( *pbuf )) >> pbuf )

Мы хотели передать setw() размер массива, адресуемого pbuf. Но выражение

*pbuf

дает только один символ, т.е. объект типа char. Поэтому setw()

передается значение 1. На каждой итерации цикла while в массив, на который указывает pbuf, помещается только нулевой символ. До чтения из стандартного ввода дело так и не доходит, программа зацикливается.

При использовании класса string все проблемы управления памятью исчезают, об этом заботится сам string. Вот как выглядит наша программа в данном случае:



#include <iostream.h>

#include <string>

int main()

{

           string buf, largest;

           // для хранения статистики

           int curLen,   // длина текущего слова

               max = -1, // максимальная длина слова

               cnt = 0;  // счетчик прочитанных слов

           while ( cin >> buf )

           {

                  curLen = buf.size();

                  ++cnt;

                  // новое самое длинное слово? сохраним его

                  if ( curLen > max )

                  {

              max = curLen;

              largest = buf;

                  }

           }

           cout << "Число прочитанных слов " << cnt << endl;

     cout << "Длина самого длинного слова " << max << endl;

           cout << "Самое длинное слово " << largest << endl;
}

Однако запятая и кавычка по-прежнему считаются частью слова. Напишем функцию для удаления этих символов из слова:



#include <string>

void filter_string( string &str )

{

     // элементы, подлежащие фильтрации

     string filt_elems( "\",?." );

     string::size_type pos = 0;

     while (( pos = str.find_first_of( filt_elems, pos ))

                  != string::npos )

                  str.erase( pos, 1 );
<


}

Эта функция работает правильно, но множество символов, которые мы собираемся отбрасывать, “зашито” в код. Лучше дать пользователю возможность самому передать строку, содержащую такие символы. Если он согласен на множество по умолчанию, то может передать пустую строку.



#include <string>

void filter_string( string &str,

                    string filt_elems = string("\",."))

{

     string::size_type pos = 0;

     while (( pos = str.find_first_of( filt_elems, pos ))

                  != string::npos )

                  str.erase( pos, 1 );
}

Более общая версия filter_string()

принимает пару итераторов, обозначающих диапазон, где производится фильтрация:



template <class InputIterator>

void filter_string( InputIterator first, InputIterator last,

                    string filt_elems = string("\",."))

{

           for ( ; first != last; first++ )

           {

                  string::size_type pos = 0;

                  while (( pos = (*first).find_first_of( filt_elems, pos ))

                       != string::npos )

                       (*first).erase( pos, 1 );

           }
}

С использованием этой функции программа будет выглядеть так:



#include <string>

#include <algorithm>

#include <iterator>

#include <vector>

#include <iostream>

bool length_less( string s1, string s2 )

           { return s1.size() < s2.size(); }

int main()

{

     istream_iterator< string > input( cin ), eos;

           vector< string > text;

     // copy - это обобщенный алгоритм

           copy( input, eos, back_inserter( text ));

           string filt_elems( "\",.;:");

           filter_string( text.begin(), text.end(), filt_elems );

           int cnt = text.size();

     // max_element - это обобщенный алгоритм

           string *max = max_element( text.begin(), text.end(),

                                length_less );

           int len = max->size();

          

           cout << "Число прочитанных слов "

                << cnt << endl;

     cout << "Длина самого длинного слова "

                << len << endl;

           cout << "Самое длинное слово "

                << *max << endl;
<


}         

Когда мы применили в алгоритме max_element()

стандартный оператор “меньше”, определенный в классе string, то были удивлены полученным результатом:

Число прочитанных слов 65

Длина самого длинного слова 4

Самое длинное слово wind

Очевидно, что wind – это не самое длинное слово. Оказывается, оператор “меньше” в классе string

сравнивает строки не по длине, а в лексикографическом порядке. И в этом смысле wind – действительно максимальный элемент. Для того чтобы найти слово максимальной длины, мы должны заменить оператор “меньше” предикатом length_less(). Тогда результат будет таким:

Число прочитанных слов 65

Длина самого длинного слова 9

Самое длинное слово beautiful

Упражнение 20.2

Прочитайте из стандартного ввода последовательность данных таких типов: string, double, string, int, string. Каждый раз проверяйте, не было ли ошибки чтения.

Упражнение 20.3

Прочитайте из стандартного ввода заранее неизвестное число строк. Поместите их в список. Найдите самую длинную и самую короткую строку.


Тип bool


Объект типа bool

может принимать одно из двух значений: true и false. Например:

// инициализация строки

string search_word = get_word();

// инициализация переменной found

bool found = false;

string next_word;

while ( cin >> next_word )

   if ( next_word == search_word )

        found = true;

// ...

// сокращенная запись: if ( found == true )

if ( found )

      cout << "ok, мы нашли слово\n";

 else cout << "нет, наше слово не встретилось.\n";

Хотя bool

относится к одному из целых типов, он не может быть объявлен как signed, unsigned, short или long, поэтому  приведенное определение ошибочно:

// ошибка

short bool found = false;

Объекты типа bool

неявно преобразуются в тип int. Значение true

превращается в 1, а false – в 0. Например:

bool found = false;

int occurrence_count = 0;

while ( /* mumble */ )

{

   found = look_for( /* something */ );

   // значение found преобразуется в 0 или 1

   occurrence_count += found;

}

Таким же образом значения целых типов и указателей могут быть преобразованы в значения типа bool. При этом 0 интерпретируется как false, а все остальное как true:

// возвращает количество вхождений

extern int find( const string& );

bool found = false;

if ( found = find( "rosebud" ))

     // правильно: found == true

// возвращает указатель на элемент

extern int* find( int value );

if ( found = find( 1024 ))

     // правильно: found == true



Тип члена класса


Указателю на функцию нельзя присвоить адрес функции-члена, даже если типы возвращаемых значений и списки параметров полностью совпадают. Например, переменная pfi – это указатель на функцию без параметров, которая возвращает значение типа int:

int (*pfi)();

Если имеются глобальные функции HeightIs() и WidthIs()

вида:

int HeightIs();

int WidthIs();

то допустимо присваивание pfi

адреса любой из этих переменных:

pfi = HeightIs;

pfi = WidthIs;

В классе Screen

также определены две функции доступа, height() и width(), не имеющие параметров и возвращающие значение типа int:

inline int Screen::height() { return _height; }

inline int Screen::width() { return _width; }

Однако попытка присвоить их переменной pfi является нарушением типизации и влечет ошибку компиляции:

// неверное присваивание: нарушение типизации

pfi = &Screen::height;

В чем нарушение? У функций-членов есть дополнительный атрибут типа, отсутствующий у функций, не являющихся членами, – класс. Указатель на функцию-член должен соответствовать типу присваиваемой ему функции не в двух, а в трех отношениях: по типу и количеству формальных параметров; типу возвращаемого значения; типу класса, членом которого является функция.

Несоответствие типов между двумя указателями – на функцию-член и на обычную функцию – обусловлено их разницей в представлении. В указателе на обычную функцию хранится ее адрес, который можно использовать для непосредственного вызова. (Указатели на функции рассматривались в разделе 7.9.) Указатель же на функцию-член должен быть сначала привязан к объекту или указателю на объект, чтобы получить this, и только после этого он применяется для вызова функции-члена. (В следующем подразделе мы покажем, как осуществить такую привязку.) Хотя для указателя на обычную функцию и для указателя на функцию-член используется один и тот же термин, их природа различна.

Синтаксис объявления указателя на функцию-член должен принимать во внимание тип класса. То же верно и в отношении указателей на данные-члены. Рассмотрим член _height


класса Screen. Его полный тип таков: член класса Screen типа short. Следовательно, полный тип указателя на _height – это указатель на член класса Screen

типа short:

short Screen::*

Определение указателя на член класса Screen типа short

выглядит следующим образом:

short Screen::*ps_Screen;

Переменную ps_Screen

можно инициализировать адресом _height:

short Screen::*ps_Screen = &Screen::_height;

или присвоить ей адрес _width:

short Screen::*ps_Screen = &Screen::_width;

Переменной ps_Screen

разрешается присваивать указатель на _width или _height, так как они являются членами класса Screen типа short.

Несоответствие типов указателя на данные-члены и обычного указателя также связано с различием в их представлении. Обычный указатель содержит всю информацию, необходимую для обращения к объекту. Указатель на данные-члены следует сначала привязать к объекту или указателю на него, а лишь затем использовать для доступа к члену этого объекта. (В книге “Inside the C++ Object Model” ([LIPPMAN96a]) также описывается представление указателей на члены.)

Указатель на функцию-член определяется путем задания типа возвращаемого функцией значения, списка ее параметров и класса. Например, следующий указатель, с помощью которого можно вызвать функции height() и width(), имеет тип указателя на функцию-член класса Screen без параметров, которая возвращает значение типа int:

int (Screen::*)()

Указатели на функции-члены можно объявлять, инициализировать и присваивать:



// всем указателям на функции-члены класса можно присвоить значение 0

int (Screen::*pmf1)() = 0;

int (Screen::*pmf2)() = &Screen::height;

pmf1 = pmf2;
pmf2 = &Screen::width;

Использование typedef

может облегчить чтение объявлений указателей на члены. Например, для типа “указатель на функцию-член класса Screen без параметров, которая возвращает ссылку на объект Screen”, т.е.

Screen& (Screen::*)()

Следующий typedef

определяет Action как альтернативное имя:



typedef Screen& (Screen::*Action)();

Action default = &Screen::home;
<


Action next = &Screen::forward;

Тип “указатель на функцию-член” можно использовать для объявления формальных параметров и типа возвращаемого значения функции. Для параметра того же типа можно также указать значение аргумента по умолчанию:

Screen& action( Screen&, Action)();

action()

объявлена как принимающая два параметра: ссылку на объект класса Screen и указатель на функцию-член Screen без параметров, которая возвращает ссылку на его объект. Вызвать action() можно любым из следующих способов:



Screen meScreen;

typedef Screen& (Screen::*Action)();

Action default = &Screen::home;

extern Screen& action( Screen&, Sction = &Screen::display );

void ff()

{

   action( myScreen );

   action( myScreen, default );

   action( myScreen, &Screen::end );
}

В следующем подразделе обсуждается вызов функции-члена посредством указателя.


Тип “массив”


Мы уже касались массивов в разделе 2.1. Массив – это набор элементов одного типа, доступ к которым производится по индексу – порядковому номеру элемента в массиве. Например:

int ival;

определяет ival как переменную типа int, а инструкция

int ia[ 10 ];

задает массив из десяти объектов типа int. К каждому из этих объектов, или элементов массива, можно обратиться с помощью операции взятия индекса:

ival = ia[ 2 ];

присваивает переменной ival

значение элемента массива ia с индексом 2. Аналогично

ia[ 7 ] = ival;

присваивает элементу с индексом 7 значение ival.

Определение массива состоит из спецификатора типа, имени массива и размера. Размер задает количество элементов массива (не менее 1) и заключается в квадратные скобки. Размер массива нужно знать уже на этапе компиляции, а следовательно, он должен быть константным выражением, хотя не обязательно задается литералом. Вот примеры правильных и неправильных определений массивов:

extern int get_size();

// buf_size и max_files константы

const int buf_size = 512, max_files = 20;

int staff_size = 27;

// правильно: константа

char input_buffer[ buf_size ];

// правильно: константное выражение: 20 - 3

char *fileTable[ max_files-3 ];

// ошибка: не константа

double salaries[ staff_size ];

// ошибка: не константное выражение

int test_scores[ get_size() ];

Объекты buf_size и max_files

являются константами, поэтому определения массивов input_buffer и fileTable

правильны. А вот staff_size – переменная (хотя и инициализированная константой 27), значит, salaries[staff_size] недопустимо. (Компилятор не в состоянии найти значение переменной staff_size в момент определения массива salaries.)

Выражение max_files-3

может быть вычислено на этапе компиляции, следовательно, определение массива fileTable[max_files-3]

синтаксически правильно.

Нумерация элементов начинается с 0, поэтому для массива из 10 элементов правильным диапазоном индексов является не 1 – 10, а 0 – 9. Вот пример перебора всех элементов массива:




int main()

{

           const int array_size = 10;

           int ia[ array_size ];

           for ( int ix = 0; ix < array_size; ++ ix )

                  ia[ ix ] = ix;
}

При определении массив можно явно инициализировать, перечислив значения его элементов в фигурных скобках, через запятую:



const int array_size = 3;
int ia[ array_size ] = { 0, 1, 2 };

Если мы явно указываем список значений, то можем не указывать размер массива: компилятор сам подсчитает количество элементов:



// массив размера 3
int ia[] = { 0, 1, 2 };

Когда явно указаны и размер, и список значений, возможны три варианта. При совпадении размера и количества значений все очевидно. Если список значений короче, чем заданный размер, оставшиеся элементы массива инициализируются нулями. Если же в списке больше значений, компилятор выводит сообщение об ошибке:



// ia ==> { 0, 1, 2, 0, 0 }

const int array_size = 5;
int ia[ array_size ] = { 0, 1, 2 };

Символьный массив может быть инициализирован не только списком символьных значений в фигурных скобках, но и строковым литералом. Однако между этими способами есть некоторая разница. Допустим,



const char cal[] = {'C', '+', '+' };
const char cal2[] = "C++";

Размерность массива ca1

равна 3, массива ca2 – 4 (в строковых литералах учитывается завершающий нулевой символ). Следующее определение вызовет ошибку компиляции:



// ошибка: строка "Daniel" состоит из 7 элементов
const char ch3[ 6 ] = "Daniel";

Массиву не может быть присвоено значение другого массива, недопустима и инициализация одного массива другим. Кроме того, не разрешается использовать массив ссылок. Вот примеры правильного и неправильного употребления массивов:



const int array_size = 3;

int ix, jx, kx;

// правильно: массив указателей типа int*

int *iar [] = { &ix, &jx, &kx };

// error: массивы ссылок недопустимы

int &iar[] = { ix, jx, kx };

int main()

{

           int ia3{ array_size ]; // правильно

          

           // ошибка: встроенные массивы нельзя копировать

ia3 = ia;

return 0;
<


}

Чтобы скопировать один массив в другой, придется проделать это для каждого элемента по отдельности:



const int array_size = 7;

int ia1[] = { 0, 1, 2, 3, 4, 5, 6 };

int main()

{

           int ia3[ array_size ];

           for ( int ix = 0; ix < array_size; ++ix )

                  ia2[ ix ] = ia1[ ix ];

           return 0;
}

В качестве индекса массива может выступать любое выражение, дающее результат целого типа. Например:



int someVal, get_index();
ia2[ get_index() ] = someVal;

Подчеркнем, что язык С++ не обеспечивает контроля индексов массива – ни на этапе компиляции, ни на этапе выполнения. Программист сам должен следить за тем, чтобы индекс не вышел за границы массива. Ошибки при работе с индексом достаточно распространены. К сожалению, не так уж трудно встретить примеры программ, которые компилируются и даже работают, но тем не менее содержат фатальные ошибки, рано или поздно приводящие к краху.

Упражнение 3.22

Какие из приведенных определений массивов содержат ошибки? Поясните.



(a) int ia[ buf_size ];     (d) int ia[ 2 * 7 - 14 ]

(b) int ia[ get_size() ];   (e) char st[ 11 ] = "fundamental";
(c) int ia[ 4 * 7 - 14 ];

Упражнение 3.23

Следующий фрагмент кода должен инициализировать каждый элемент массива значением индекса. Найдите допущенные ошибки:



int main() {

    const int array_size = 10;

    int ia[ array_size ];

    for ( int ix = 1; ix <= array_size; ++ix )

                ia[ ia ] = ix;

    // ...
}


Тип указателя на функцию


Как объявить указатель на функцию? Как выглядит формальный параметр, когда фактическим аргументом является такой указатель? Вот определение функции lexicoCompare(), которая сравнивает две строки лексикографически:

#include <string>

int lexicoCompare( const string &sl, const string &s2 ) {

    return sl.compare(s2);

}

Если все символы строк s1 и s2

равны, lexicoCompare()

вернет 0, в противном случае– отрицательное число, если s1

меньше чем s2, и положительное, если s1

больше s2.

Имя функции не входит в ее сигнатуру – она определяется только типом возвращаемого значения и списком параметров. Указатель на lexicoCompare()

должен адресовать функцию с той же сигнатурой. Попробуем написать так:

int *pf( const string &, const string & ) ;

// нет, не совсем так

Эта инструкция почти правильна. Проблема в том, что компилятор интерпретирует ее как объявление функции с именем pf, которая возвращает указатель типа int*. Список параметров правилен, но тип возвращаемого значения не тот. Оператор разыменования (*) ассоциируется с данным типом (int в нашем случае), а не с pf. Чтобы исправить положение, нужно использовать скобки:

int (*pf)( const string &, const string & ) ;

// правильно

pf объявлен как указатель на функцию с двумя параметрами, возвращающую значение типа int, т.е.  такую, как lexicoCompare().

pf способен адресовать и приведенную ниже функцию, поскольку ее сигнатура совпадает с типом lexicoCompare():

int sizeCompare( const string &sl, const string &s2 );

Функции calc() и gcd()другого типа, поэтому pf не может указывать на них:

int calc( int , int );

int gcd( int , int );

Указатель, который адресует эти две функции, определяется так:

int (*pfi)( int, int );

Многоточие является частью сигнатуры функции. Если у двух функций списки параметров отличаются только тем, что в конце одного из них стоит многоточие, то считается, что функции различны. Таковы же и типы указателей.

int printf( const char*, ... );

int strlen( const char* );

int (*pfce)( const char*, ... ); // может указывать на printf()

int (*pfc)( const char* );       // может указывать на strlen()

Типов функций столько, сколько комбинаций типов возвращаемых значений и списков параметров.



Тип возвращаемого функцией значения


Тип возвращаемого функцией значения бывает встроенным, как int или double, составным, как int& или double*, или определенным пользователем– перечислением или классом. Можно также использовать специальное ключевое слово void, которое говорит о том, что функция не возвращает никакого значения:

#include <string>

#include <vector> class Date { /* определение */ };

bool look_up( int *, int );

double calc( double );

int count( const string &, char );

Date& calendar( const char );

void sum( vector<int>&, int );

Однако функция или встроенный массив не могут быть типом возвращаемого значения. Следующий пример ошибочен:

// массив не может быть типом возвращаемого значения

int[10] foo_bar();

Но можно вернуть указатель на первый элемент массива:

// правильно: указатель на первый элемент массива

int *foo_bar();

(Размер массива должен быть известен вызывающей программе.)

Функция может возвращать типы классов, в частности контейнеры. Например:

// правильно: возвращается список символов

list<char> foo_bar();

(Этот подход не очень эффективен. Обсуждение типа возвращаемого значения см. в разделе 7.4.)

Тип возвращаемого функцией значения должен быть явно указан. Приведенный ниже код вызывает ошибку компиляции:

// ошибка: пропущен тип возвращаемого значения

const is_equa1( vector<int> vl, vector<int> v2 );

В предыдущих версиях С++ в подобных случаях считалось, что функция возвращает значение типа int. Стандарт С++ отменил это соглашение. Правильное объявление is_equal()

выглядит так:

// правильно: тип возвращаемого значения указан

const bool is_equa1( vector<int> vl, vector<int> v2 );



Типы данных С++


В этой главе приводится обзор встроенных, или элементарных, типов данных языка С++. Она начинается с определения литералов, таких, как 3.14159 или pi, а затем вводится понятие переменной, или объекта, который должен принадлежать к одному из типов данных. Оставшаяся часть главы посвящена подробному описанию каждого встроенного типа. Кроме того, приводятся производные типы данных для строк и массивов, предоставляемые стандартной библиотекой С++. Хотя эти типы не являются элементарными, они очень важны для написания настоящих программ на С++, и нам хочется познакомить с ними читателя как можно раньше. Мы будем называть такие типы данных расширением базовых типов С++.