НА ГЛАВНУЮ
Меню сайта
Категория
Ghost++ [1]
С++ [55]
Развлечение
ON - LINE
Опрос
Какой артефакт рулит?
Всего ответов: 460
Оbserver Ward

Онлайн всего: 1
Гостей: 1
Пользователей: 0


Друзья сайта
Заведи себе Бота
Hаша кнопка
Для обмена банерами , наша кнопка для размещения у вас на сайте

Клансайт USSR


Главная » Статьи » Программирование » С++

3. Типы данных С++ (2)
3.6. Ссылочный тип

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

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 &piref = &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;

3.7. Тип 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

3.8. Перечисления

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

const int input = 1;
const int output = 2;
const int append = 3;

и пользоваться этими константами:

bool open_file( string file_name, int open_mode);
// ...
open_file( "Phoenix_and_the_Crane", append );

Подобное решение допустимо, но не вполне приемлемо, поскольку мы не можем гарантировать, что аргумент, передаваемый в функцию open_file() равен только 1, 2 или 3.
Использование перечислимого типа решает данную проблему. Когда мы пишем:

enum open_modes{ input = 1, output, append };

мы определяем новый тип open_modes. Допустимые значения для объекта этого типа ограничены набором 1, 2 и 3, причем каждое из указанных значений имеет мнемоническое имя. Мы можем использовать имя этого нового типа для определения как объекта данного типа, так и типа формальных параметров функции:

void open_file( string file_name, open_modes om );

input, output и append являются элементами перечисления. Набор элементов перечисления задает допустимое множество значений для объекта данного типа. Переменная типа open_modes (в нашем примере) инициализируется одним из этих значений, ей также может быть присвоено любое из них. Например:

open_file( "Phoenix and the Crane", append );

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

// ошибка: 1 не является элементом перечисления open_modes
open_file( "Jonah", 1 );


Есть способ определить переменную типа open_modes, присвоить ей значение одного из элементов перечисления и передать параметром в функцию:

open_modes om = input;
  // ...
  om = append;
  open_file( "TailTell", om );

Однако получить имена таких элементов невозможно. Если мы напишем оператор вывода:

cout << input << " " << om << endl;

то все равно получим:

1 3

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

cout << open_modes_table[ input ] << " "
   << open_modes_table[ om ] << endl
Будет выведено:
input append

Кроме того, нельзя перебрать все значения перечисления:

// не поддерживается
for ( open_modes iter = input; iter != append; ++inter )
// ...

Для определения перечисления служит ключевое слово enum, а имена элементов задаются в фигурных скобках, через запятую. По умолчанию первый из них равен 0, следующий – 1 и так далее. С помощью оператора присваивания это правило можно изменить. При этом каждый следующий элемент без явно указанного значения будет на 1 больше, чем элемент, идущий перед ним в списке. В нашем примере мы явно указали значение 1 для input, при этом output и append будут равны 2 и 3. Вот еще один пример:

// shape == 0, sphere == 1, cylinder == 2, polygon == 3
enum Forms{ share, spere, cylinder, polygon };

Целые значения, соответствующие разным элементам одного перечисления, не обязаны отличаться. Например:

// point2d == 2, point2w == 3, point3d == 3, point3w == 4
enum Points { point2d=2, point2w, point3d=3, point3w=4 };

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

void mumble() {
Points pt3d = point3d; // правильно: pt2d == 3
// ошибка: pt3w инициализируется типом int
Points pt3w = 3;
// ошибка: polygon не входит в перечисление Points
pt3w = polygon;
// правильно: оба объекта типа Points
pt3w = pt3d;
}

Однако в арифметических выражениях перечисление может быть автоматически преобразовано в тип int. Например:

const int array_size = 1024;
// правильно: pt2w преобразуется int
int chunk_size = array_size * pt2w;

3.9. Тип "массив"

Мы уже касались массивов в разделе 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;
   // ...
}

3.9.1. Многомерные массивы

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

int ia[ 4 ][ 3 ];

Первая величина (4) задает количество строк, вторая (3) – количество столбцов. Объект ia определен как массив из четырех строк по три элемента в каждой. Многомерные массивы тоже могут быть инициализированы:

int ia[ 4 ][ 3 ] = {
    { 0, 1, 2 },
    { 3, 4, 5 },
    { 6, 7, 8 },
    { 9, 10, 11 }
};

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

int ia[4][3] = { 0,1,2,3,4,5,6,7,8,9,10,11 };

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

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

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

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

При обращении к элементам многомерного массива необходимо использовать индексы для каждого измерения (они заключаются в квадратные скобки). Так выглядит инициализация двумерного массива с помощью вложенных циклов:

int main()
{
   const int rowSize = 4;
   const int colSize = 3;
   int ia[ rowSize ][ colSize ];
   for ( int = 0; i < rowSize; ++i )
   for ( int j = 0; j < colSize; ++j )
   ia[ i ][ j ] = i + j j;
}

Конструкция

ia[ 1, 2 ]

является допустимой с точки зрения синтаксиса С++, однако означает совсем не то, чего ждет неопытный программист. Это отнюдь не объявление двумерного массива 1 на 2. Агрегат в квадратных скобках – это список выражений через запятую, результатом которого будет последнее значение 2 (см. оператор "запятая” в разделе 4.2). Поэтому объявление ia[1,2] эквивалентно ia[2]. Это еще одна возможность допустить ошибку.
3.9.2. Взаимосвязь массивов и указателей

Если мы имеем определение массива:

 int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 };

то что означает простое указание его имени в программе?

ia;

Использование идентификатора массива в программе эквивалентно указанию адреса его первого элемента:

ia;
&ia[0]

Аналогично обратиться к значению первого элемента массива можно двумя способами:

// оба выражения возвращают первый элемент
*ia;
ia[0];

Чтобы взять адрес второго элемента массива, мы должны написать:

&ia[1];

Как мы уже упоминали раньше, выражение

 ia+1;

также дает адрес второго элемента массива. Соответственно, его значение дают нам следующие два способа:

*(ia+1);
ia[1];

Отметим разницу в выражениях:

*ia+1
и
*(ia+1);

Операция разыменования имеет более высокий приоритет, чем операция сложения (о приоритетах операций говорится в разделе 4.13). Поэтому первое выражение сначала разыменовывает переменную ia и получает первый элемент массива, а затем прибавляет к нему 1. Второе же выражение доставляет значение второго элемента.

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

#include <iostream>
int main()
{
   int ia[9] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 };
   int *pbegin = ia;
   int *pend = ia + 9;
   while ( pbegin != pend ) {
      cout << *pbegin <<;
      ++pbegin;
   }
}

Указатель pbegin инициализируется адресом первого элемента массива. Каждый проход по циклу увеличивает этот указатель на 1, что означает смещение его на следующий элемент. Как понять, где остановиться? В нашем примере мы определили второй указатель pend и инициализировали его адресом, следующим за последним элементом массива ia. Как только значение pbegin станет равным pend, мы узнаем, что массив кончился. Перепишем эту программу так, чтобы начало и конец массива передавались параметрами в некую обобщенную функцию, которая умеет печатать массив любого размера:

#inc1ude <iostream>
void ia_print( int *pbegin, int *pend )
{
  while ( pbegin != pend ) {
     cout << *pbegin << ' ';
     ++pbegin;
  }
}

int main()
{
   int ia[9] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 };
   ia_print( ia, ia + 9 );
}

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

#inc1ude <iostream>
template <c1ass e1emType>
void print( elemType *pbegin, elemType *pend )
{
   while ( pbegin != pend ) {
      cout << *pbegin << ' ';
      ++pbegin;
   }
}

Теперь мы можем вызывать нашу функцию print() для печати массивов любого типа:

int main()
{
   int ia[9] = { 0, 1, 1, 2, 3, 5, 8, 13, 21 };
   double da[4] = { 3.14, 6.28, 12.56, 25.12 };
   string sa[3] = { "piglet", "eeyore", "pooh" };
   print( ia, ia+9 );
   print( da, da+4 );
   print( sa, sa+3 );
}

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

#include <a1gorithm>
int main()
{
   int ia[6] = { 107, 28, 3, 47, 104, 76 };

   string sa[3] = { "piglet", "eeyore", "pooh" };
   sort( ia, ia+6 );
   sort( sa, sa+3 );
};

(Мы подробно остановимся на обобщенных алгоритмах в главе 12; в Приложении будут приведены примеры их использования.)
В стандартной библиотеке С++ содержится набор классов, которые инкапсулируют использование контейнеров и указателей. (Об этом говорилось в разделе 2.8.) В следующем разделе мы займемся стандартным контейнерным типом vector, являющимся объектно-ориентированной реализацией массива.
3.10. Класс vector

Использование класса vector (см. раздел 2.8) является альтернативой применению встроенных массивов. Этот класс предоставляет гораздо больше возможностей, поэтому его использование предпочтительней. Однако встречаются ситуации, когда не обойтись без массивов встроенного типа. Одна из таких ситуаций – обработка передаваемых программе параметров командной строки, о чем мы будем говорить в разделе 7.8. Класс vector, как и класс string, является частью стандартной библиотеки С++.
Для использования вектора необходимо включить заголовочный файл:

#include <vector>

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

vector< int > ivec( 10 );

что аналогично определению массива встроенного типа:

int ia[ 10 ];

Для доступа к отдельным элементам вектора применяется операция взятия индекса:

void simp1e_examp1e()
{
   const int e1em_size = 10;
   vector< int > ivec( e1em_size );
   int ia[ e1em_size ];
   for ( int ix = 0; ix < e1em_size; ++ix )
   ia[ ix ] = ivec[ ix ];
   // ...
}

Мы можем узнать размерность вектора, используя функцию size(), и проверить, пуст ли вектор, с помощью функции empty(). Например:

void print_vector( vector<int> ivec )
{
   if ( ivec.empty() )
     return;
   for ( int ix=0; ix< ivec.size(); ++ix )
   cout << ivec[ ix ] << ' ';
}

Элементы вектора инициализируются значениями по умолчанию. Для числовых типов и указателей таким значением является 0. Если в качестве элементов выступают объекты класса, то инициатор для них задается конструктором по умолчанию (см. раздел 2.3). Однако инициатор можно задать и явно, используя форму:

vector< int > ivec( 10, -1 );

Все десять элементов вектора будут равны -1.
Массив встроенного типа можно явно инициализировать списком:

int ia[ 6 ] = { -2, -1, О, 1, 2, 1024 };

Для объекта класса vector аналогичное действие невозможно. Однако такой объект может быть инициализирован с помощью массива встроенного типа:

// 6 элементов ia копируются в ivec
vector< int > ivec( ia, ia+6 );

Конструктору вектора ivec передаются два указателя – указатель на начало массива ia и на элемент, следующий за последним. В качестве списка начальных значений допустимо указать не весь массив, а некоторый его диапазон:

// копируются 3 элемента: ia[2], ia[3], ia[4]
vector< int > ivec( &ia[ 2 ], &ia[ 5 ] );

Еще одним отличием вектора от массива встроенного типа является возможность инициализации одного объекта типа vector другим и использования операции присваивания для копирования объектов. Например:

vector< string > svec;
void init_and_assign()
{
   // один вектор инициализируется другим
   vector< string > user_names( svec );
   // ...
   // один вектор копируется в другой
   svec = user_names;
}

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

vector< string > text;

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

string word;
   while ( cin >> word ) {
     text.push_back( word );
     // ...
   }

Хотя мы можем использовать операцию взятия индекса для перебора элементов вектора:

cout << "считаны слова: \n";
for ( int ix =0; ix < text.size(); ++ix )
    cout << text[ ix ] << ' ';
cout << endl;

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

cout << "считаны слова: \n";
for ( vector<string>::iterator it = text.begin();
it != text.end(); ++it )
cout << *it << ' ';
cout << endl;

Итератор – это класс стандартной библиотеки, фактически являющийся указателем на элемент массива.
Выражение

*it;

разыменовывает итератор и дает сам элемент вектора. Инструкция

++it;

сдвигает указатель на следующий элемент. Не нужно смешивать эти два подхода. Если следовать идиоме STL при определении пустого вектора:

vector<int> ivec;

будет ошибкой написать:

ivec[0] = 1024;

У нас еще нет ни одного элемента вектора ivec; количество элементов выясняется с помощью функции size().

Можно допустить и противоположную ошибку. Если мы определили вектор некоторого размера, например:

vector<int> ia( 10 );

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

const int size = 7;
int ia[ size ] = { 0, 1, 1, 2, 3, 5, 8 };
vector< int > ivec( size );
for ( int ix = 0; ix < size; ++ix )
   ivec.push_back( ia[ ix ] );

Имелась в виду инициализация вектора ivec значениями элементов ia, вместо чего получился вектор ivec размера 14.
Следуя идиоме STL, можно не только добавлять, но и удалять элементы вектора. (Все это мы рассмотрим подробно и с примерами в главе 6.)
Упражнение 3.24

Имеются ли ошибки в следующих определениях?
int ia[ 7 ] = { 0, 1, 1, 2, 3, 5, 8 };

(a) vector< vector< int > > ivec;
(b) vector< int > ivec = { 0, 1, 1, 2, 3, 5, 8 };
(c) vector< int > ivec( ia, ia+7 );
(d) vector< string > svec = ivec;
(e) vector< string > svec( 10, string( "null" ));
Упражнение 3.25

Реализуйте следующую функцию:
bool is_equa1( const int*ia, int ia_size,
const vector<int> &ivec );
Функция is_equal() сравнивает поэлементно два контейнера. В случае разного размера контейнеров "хвост” более длинного в расчет не принимается. Понятно, что, если все сравниваемые элементы равны, функция возвращает true, если отличается хотя бы один – false. Используйте итератор для перебора элементов. Напишите функцию main(), обращающуюся к is_equal().
3.11. Класс complex

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

#include <complex>

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

2 + 3i

где 2 – действительная часть, а 3i – мнимая. Вот примеры определений объектов типа complex:

// чисто мнимое число: 0 + 7-i
comp1ex< double > purei( 0, 7 );
// мнимая часть равна 0: 3 + Oi
comp1ex< float > rea1_num( 3 );
// и вещественная, и мнимая часть равны 0: 0 + 0-i
comp1ex< long double > zero;
// инициализация одного комплексного числа другим
comp1ex< double > purei2( purei );

Поскольку complex, как и vector, является шаблоном, мы можем конкретизировать его типами float, double и long double, как в приведенных примерах. Можно также определить массив элементов типа complex:

complex< double > conjugate[ 2 ] = {
  complex< double >( 2, 3 ),
  complex< double >( 2, -3 )
};

Вот как определяются указатель и ссылка на комплексное число:

complex< double > *ptr = &conjugate[0];
complex< double > &ref = *ptr;

Комплексные числа можно складывать, вычитать, умножать, делить, сравнивать, получать значения вещественной и мнимой части. (Более подробно мы будем говорить о классе complex в разделе 4.6.)
3.12. Директива typedef

Директива typedef позволяет задать синоним для встроенного либо пользовательского типа данных. Например:

typedef double wages;
typedef vector<int> vec_int;
typedef vec_int test_scores;
typedef bool in_attendance;
typedef int *Pint;

Имена, определенные с помощью директивы typedef, можно использовать точно так же, как спецификаторы типов:

// double hourly, weekly;
wages hourly, weekly;
// vector<int> vecl( 10 );
vec_int vecl( 10 );
// vector<int> test0( c1ass_size );
const int c1ass_size = 34;
test_scores test0( c1ass_size );
// vector< bool > attendance;
vector< in_attendance > attendance( c1ass_size );
// int *table[ 10 ];
Pint table [ 10 ];

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

typedef char *cstring;

Каков тип переменной cstr в следующем объявлении:

extern const cstring cstr;

Ответ, который кажется очевидным:

const char *cstr

Однако это неверно. Спецификатор const относится к cstr, поэтому правильный ответ – константный указатель на char:

char *const cstr;
3.13. Спецификатор 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 – сообщить компилятору, что тот не может определить, кто и как может изменить значение данного объекта. Поэтому компилятор не должен выполнять оптимизацию кода, использующего данный объект.
3.14. Класс pair

Класс pair (пара) стандартной библиотеки С++ позволяет нам определить одним объектом пару значений, если между ними есть какая-либо семантическая связь. Эти значения могут быть одинакового или разного типа. Для использования данного класса необходимо включить заголовочный файл:

#include <utility>

Например, инструкция

pair< string, string > author( "James", "Joyce" );

создает объект author типа pair, состоящий из двух строковых значений.
Отдельные части пары могут быть получены с помощью членов first и second:

string firstBook;

if ( Joyce.first == "James" &&
   Joyce.second == "Joyce" )
firstBook = "Stephen Hero";

Если нужно определить несколько однотипных объектов этого класса, удобно использовать директиву typedef:

typedef pair< string, string > Authors;
Authors proust( "marcel", "proust" );
Authors joyce( "James", "Joyce" );
Authors musil( "robert", "musi1" );

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

class EntrySlot;
extern EntrySlot* 1ook_up( string );
typedef pair< string, EntrySlot* > SymbolEntry;

SymbolEntry current_entry( "author", 1ook_up( "author"));
// ...
if ( EntrySlot *it = 1ook_up( "editor" ))
{
   current_entry.first = "editor";
   current_entry.second = it;
}

(Мы вернемся к рассмотрению класса pair в разговоре о контейнерных типах в главе 6 и об обобщенных алгоритмах в главе 12.)
3.15. Типы классов

Механизм классов позволяет создавать новые типы данных; с его помощью введены типы string, vector, complex и pair, рассмотренные выше. В главе 2 мы рассказывали о концепциях и механизмах, поддерживающих объектный и объектно-ориентированный подход, на примере реализации класса Array. Здесь мы, основываясь на объектном подходе, создадим простой класс String, реализация которого поможет понять, в частности, перегрузку операций – мы говорили о ней в разделе 2.3. (Классы подробно рассматриваются в главах 13, 14 и 15). Мы дали краткое описание класса для того, чтобы приводить более интересные примеры. Читатель, только начинающий изучение С++, может пропустить этот раздел и подождать более систематического описания классов в следующих главах.)
Наш класс String должен поддерживать инициализацию объектом класса String, строковым литералом и встроенным строковым типом, равно как и операцию присваивания ему значений этих типов. Мы используем для этого конструкторы класса и перегруженную операцию присваивания. Доступ к отдельным символам String будет реализован как перегруженная операция взятия индекса. Кроме того, нам понадобятся: функция size() для получения информации о длине строки; операция сравнения объектов типа String и объекта String со строкой встроенного типа; а также операции ввода/вывода нашего объекта. В заключение мы реализуем возможность доступа к внутреннему представлению нашей строки в виде строки встроенного типа.
Определение класса начинается ключевым словом class, за которым следует идентификатор – имя класса, или типа. В общем случае класс состоит из секций, предваряемых словами public (открытая) и private (закрытая). Открытая секция, как правило, содержит набор операций, поддерживаемых классом и называемых методами или функциями-членами класса. Эти функции-члены определяют открытый интерфейс класса, другими словами, набор действий, которые можно совершать с объектами данного класса. В закрытую секцию обычно включают данные-члены, обеспечивающие внутреннюю реализацию. В нашем случае к внутренним членам относятся _string – указатель на char, а также _size типа int. _size будет хранить информацию о длине строки, а _string – динамически выделенный массив символов. Вот как выглядит определение класса:

#inc1ude <iostream>
class String;
istream& operator>>( istream&, String& );
ostream& operator<<( ostream&, const String& );
class String {
public:
   // набор конструкторов
   // для автоматической инициализации
   // String strl; // String()
   // String str2( "literal" ); // String( const char* );
   // String str3( str2 ); // String( const String& );
   String();
   String( const char* );
   String( const String& );

   // деструктор
   ~String();
   // операторы присваивания
   // strl = str2
   // str3 = "a string literal"
   String& operator=( const String& );
   String& operator=( const char* );
   // операторы проверки на равенство
   // strl == str2;
   // str3 == "a string literal";
   bool operator==( const String& );
   bool operator==( const char* );
   // перегрузка оператора доступа по индексу
   // strl[ 0 ] = str2[ 0 ];
   char& operator[]( int );
   // доступ к членам класса
   int size() { return _size; }
   char* c_str() { return _string; }

private:
   int _size;
   char *_string;
}

Класс String имеет три конструктора. Как было сказано в разделе 2.3, механизм перегрузки позволяет определять несколько реализаций функций с одним именем, если все они различаются количеством и/или типами своих параметров. Первый конструктор

String();

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

String str1;

для str1 вызывается такой конструктор.
Два оставшихся конструктора имеют по одному параметру. Так, для

String str2("строка символов");

вызывается конструктор

String(const char*);

а для

String str3(str2);

конструктор

String(const String&);

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

String str4(1024);

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

return_type operator op (parameter_list);

где operator – ключевое слово, а op – один из предопределенных операторов: +, =, ==, [] и так далее. (Точное определение синтаксиса см. в главе 15.) Вот объявление перегруженного оператора взятия индекса:

char& operator[] (int);

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

String object("Danny");
String *ptr = new String ("Anna");
String array[2];
Вот как выглядит вызов функции size() для этих объектов:
vector<int> sizes( 3 );

// доступ к члену для objects (.);
// objects имеет размер 5
sizes[ 0 ] = object.size();

// доступ к члену для pointers (->)
// ptr имеет размер 4
sizes[ 1 ] = ptr->size();

// доступ к члену (.)
// array[0] имеет размер 0
sizes[ 2 ] = array[0].size();

Она возвращает соответственно 5, 4 и 0.
Перегруженные операторы применяются к объекту так же, как обычные:

String namel( "Yadie" );
String name2( "Yodie" );
// bool operator==(const String&)
if ( namel == name2 )
   return;
else
   // String& operator=( const String& )
   namel = name2;

Объявление функции-члена должно находиться внутри определения класса, а определение функции может стоять как внутри определения класса, так и вне его. (Обе функции size() и c_str() определяются внутри класса.) Если функция определяется вне класса, то мы должны указать, кроме всего прочего, к какому классу она принадлежит. В этом случае определение функции помещается в исходный файл, допустим, String.C, а определение самого класса – в заголовочный файл (String.h в нашем примере), который должен включаться в исходный:

// содержимое исходного файла: String.С
// включение определения класса String
#inc1ude "String.h"
// включение определения функции strcmp()
#inc1ude <cstring>
bool // тип возвращаемого значения
String:: // класс, которому принадлежит функция
operator== // имя функции: оператор равенства
(const String &rhs) // список параметров
{
  if ( _size != rhs._size )
     return false;
  return strcmp( _strinq, rhs._string ) ?
     false : true;
}

Напомним, что strcmp() – функция стандартной библиотеки С. Она сравнивает две строки встроенного типа, возвращая 0 в случае равенства строк и ненулевое значение в случае неравенства. Условный оператор (?:) проверяет значение, стоящее перед знаком вопроса. Если оно истинно, возвращается значение выражения, стоящего слева от двоеточия, в противном случае – стоящего справа. В нашем примере значение выражения равно false, если strcmp() вернула ненулевое значение, и true – если нулевое. (Условный оператор рассматривается в разделе 4.7.)
Операция сравнения довольно часто используется, реализующая ее функция получилась небольшой, поэтому полезно объявить эту функцию встроенной (inline). Компилятор подставляет текст функции вместо ее вызова, поэтому время на такой вызов не затрачивается. (Встроенные функции рассматриваются в разделе 7.6.) Функция-член, определенная внутри класса, является встроенной по умолчанию. Если же она определена вне класса, чтобы объявить ее встроенной, нужно употребить ключевое слово inline:

inline bool
String::operator==(const String &rhs)
{
   // то же самое
}

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

inline bool
String::operator==(const char *s)
{
   return strcmp( _string, s ) ? false : true;
}

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

#include <cstring>
// default constructor
inline String::String()
{
   _size = 0;
   _string = 0;
}

inline String::String( const char *str )
{
   if ( ! str ) {
     _size = 0; _string = 0;
   }
  else {
     _size = str1en( str );
     _string = new char[ _size + 1 ];
     strcpy( _string, str );
   }

// copy constructor
inline String::String( const String &rhs )
{
  size = rhs._size;
  if ( ! rhs._string )
     _string = 0;
  else {
     _string = new char[ _size + 1 ];
      strcpy( _string, rhs._string );
  }
}

Поскольку мы динамически выделяли память с помощью оператора new, необходимо освободить ее вызовом delete, когда объект String нам больше не нужен. Для этой цели служит еще одна специальная функция-член – деструктор, автоматически вызываемый для объекта в тот момент, когда этот объект перестает существовать. (См. главу 7 о времени жизни объекта.) Имя деструктора образовано из символа тильды (~) и имени класса. Вот определение деструктора класса String. Именно в нем мы вызываем операцию delete, чтобы освободить память, выделенную в конструкторе:

inline String: :~String() { delete [] _string; }

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

String namel( "orville" ), name2( "wilbur" );
namel = "Orville Wright";
this является указателем, адресующим объект name1 внутри тела функции операции присваивания.
this всегда указывает на объект класса, через который происходит вызов функции. Если
ptr->size();
obj[ 1024 ];

то внутри size() значением this будет адрес, хранящийся в ptr. Внутри операции взятия индекса this содержит адрес obj. Разыменовывая this (использованием *this), мы получаем сам объект. (Указатель this детально описан в разделе 13.4.)

inline String&
String::operator=( const char *s )
{
   if ( ! s ) {
     _size = 0;
     delete [] _string;
     _string = 0;
   }
   else {
     _size = str1en( s );
     delete [] _string;
     _string = new char[ _size + 1 ];
     strcpy( _string, s );
   }
   return *this;
}

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

inline String&
String::operator=( const String &rhs )
{
   // в выражении
   // namel = *pointer_to_string
   // this представляет собой name1,
   // rhs - *pointer_to_string.
   if ( this != &rhs ) {

Вот полный текст операции присваивания объекту String объекта того же типа:

inline String&
String::operator=( const String &rhs )
{
   if ( this != &rhs ) {
      delete [] _string;
      _size = rhs._size;
      if ( ! rhs._string )
         _string = 0;
      else {
         _string = new char[ _size + 1 ];
         strcpy( _string, rhs._string );
      }
   }
   return *this;
}

Операция взятия индекса практически совпадает с ее реализацией для массива Array, который мы создали в разделе 2.3:

#include <cassert>
inline char&
String::operator[] ( int elem )
{
  assert( elem >= 0 && elem < _size );
  return _string[ elem ];
}

Операторы ввода и вывода реализуются как отдельные функции, а не члены класса. (О причинах этого мы поговорим в разделе 15.2. В разделах 20.4 и 20.5 рассказывается о перегрузке операторов ввода и вывода библиотеки iostream.) Наш оператор ввода может прочесть не более 4095 символов. setw() – предопределенный манипулятор, он читает из входного потока заданное число символов минус 1, гарантируя тем самым, что мы не переполним наш внутренний буфер inBuf. (В главе 20 манипулятор setw() рассматривается детально.) Для использования манипуляторов нужно включить соответствующий заголовочный файл:

#include <iomanip>
inline istream&
operator>>( istream &io, String &s )
{
   // искусственное ограничение: 4096 символов
   const int 1imit_string_size = 4096;
   char inBuf[ limit_string_size ];
   // setw() входит в библиотеку iostream
   // он ограничивает размер читаемого блока до 1imit_string_size-l
   io >> setw( 1imit_string_size ) >> inBuf;
   s = mBuf; // String::operator=( const char* );
   return io;
}

Оператору вывода необходим доступ к внутреннему представлению строки String. Так как operator<< не является функцией-членом, он не имеет доступа к закрытому члену данных _string. Ситуацию можно разрешить двумя способами: объявить operator<< дружественным классу String, используя ключевое слово friend (дружественные отношения рассматриваются в разделе 15.2), или реализовать встраиваемую (inline) функцию для доступа к этому члену. В нашем случае уже есть такая функция: c_str() обеспечивает доступ к внутреннему представлению строки. Воспользуемся ею при реализации операции вывода:

inline ostream&
operator<<( ostream& os, const String &s )
{
   return os << s.c_str();
}
   

Ниже приводится пример программы, использующей класс String. Эта программа берет слова из входного потока и подсчитывает их общее число, а также количество слов "the" и "it" и регистрирует встретившиеся гласные.

#include <iostream>
#inc1ude "String.h"
int main() {
   int aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0,
   theCnt = 0, itCnt = 0, wdCnt = 0, notVowel = 0;
   // Слова "The" и "It"
   // будем проверять с помощью operator==( const char* )
   String but, the( "the" ), it( "it" );
   // operator>>( ostream&, String& )
   while ( cin >> buf ) {
      ++wdCnt;

      // operator<<( ostream&, const String& )
      cout << buf << ' ';

      if ( wdCnt % 12 == 0 )
         cout << endl;
      // String::operator==( const String& ) and
      // String::operator==( const char* );
      if ( buf == the | | buf == "The" )
         ++theCnt;
      else
      if ( buf == it || buf == "It" )
         ++itCnt;
   // invokes String::s-ize()
   for ( int ix =0; ix < buf.sizeO; ++ix )
   {
      // invokes String:: operator [] (int)
      switch( buf[ ix ] )
      {
         case 'a': case 'A': ++aCnt; break;
         case 'e': case 'E': ++eCnt; break;
         case 'i': case 'I': ++iCnt; break;
         case 'o': case '0': ++oCnt; break;
         case 'u': case 'U': ++uCnt; break;
         default: ++notVowe1; break;
       }
     }
   }
   // operator<<( ostream&, const String& )
   cout << "\n\n"
      << "Слов: " << wdCnt << "\n\n"
      << "the/The: " << theCnt << '\n'
      << "it/It: " << itCnt << "\n\n"
      << "согласных: " < <notVowel << "\n\n"
      << "a: " << aCnt << '\n'
      << "e: " << eCnt << '\n'
      << "i: " << ICnt << '\n'
      << "o: " << oCnt << '\n'
      << "u: " << uCnt << endl;
}

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

Alice Emma has long flowing red hair. Her Daddy says when the
wind blows through her hair, it looks almost alive, 1ike 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?"

Слов: 65
the/The: 2
it/It: 1
согласных: 190
a: 22
e: 30
i: 24
о: 10
u: 7

Упражнение 3.26

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

Модифицируйте тестовую программу так, чтобы она подсчитывала и согласные b, d, f, s, t.
Упражнение 3.28

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

class String {
public:
   // ...
   int count( char ch ) const;
   // ...
};

Упражнение 3.29

Реализуйте оператор конкатенации строк (+) так, чтобы он конкатенировал две строки и возвращал результат в новом объекте String. Вот объявление функции:

class String {
public:
   // ...
   String operator+( const String &rhs ) const;
   // ...
};
Категория: С++ | Добавил: r2d2 (29.09.2011)
Просмотров: 1747 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Born in Ussr
Залогиниться
Турниры

/j clan ussr /j clan cccp