C++ для начинающих - Липпман 15 стр.


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( vectorint 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 ( vectorstring::iterator it = text.begin();

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

cout *it ' ';

cout endl;

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

Выражение

*it;

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

++it;

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

vectorint ivec;

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

ivec[0] = 1024;

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

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

vectorint 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_equal( const int*ia, int ia_size,

const vectorint ivec );

Функция is_equal() сравнивает поэлементно два контейнера. В случае разного размера контейнеров "хвост" более длинного в расчет не принимается. Понятно, что, если все сравниваемые элементы равны, функция возвращает true, если отличается хотя бы один – false. Используйте итератор для перебора элементов. Напишите функцию main(), обращающуюся к is_equal().

3.11. Класс complex

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

#include complex

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

2 + 3i

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

// чисто мнимое число: 0 + 7-i

complex double purei( 0, 7 );

// мнимая часть равна 0: 3 + Oi

complex float rea1_num( 3 );

// и вещественная, и мнимая часть равны 0: 0 + 0-i

complex long double zero;

// инициализация одного комплексного числа другим

complex 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 vectorint vec_int;

typedef vec_int test_scores;

typedef bool in_attendance;

typedef int *Pint;

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

// double hourly, weekly;

wages hourly, weekly;

// vectorint vecl( 10 );

vec_int vecl( 10 );

// vectorint 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). Мы дали краткое описание класса для того, чтобы приводить более интересные примеры. Читатель, только начинающий изучение С++, может пропустить этот раздел и подождать более систематического описания классов в следующих главах.)

Назад Дальше