Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
138
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

252

}

svec получает емкость 32 при размере 0. Однако эксперименты показали, что любое изменение начальной емкости для вектора, у которого она по умолчанию отлична от 1, ведет к снижению производительности. Так, для векторов типа string и double увеличение емкости с помощью reserve() дало худшие показатели. С другой стороны,

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

Таблица 6.4. Время в секундах для вставки 10 000 элементов при различной емкости*

Емкость

Время в секундах

 

 

1 по умолчанию

670

4,096

555

8,192

444

10,000

222

*Сложный класс размером 8000 байт с

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

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

Упражнение 6.2

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

Упражнение 6.3

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

Упражнение 6.4

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

(a)Целые числа

(b)Указатели на большие сложные объекты

(c)Большие сложные объекты

6.4. Как определить последовательный контейнер?

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

С++ для начинающих

253

#include <vector> #inclnde <list> #include <deque> #include <map>

#include <set>

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

vector< string > svec;

следует тип данных его элементов12. Например:

list< int >

ilist;

Переменная svec определяется как вектор, способный содержать элементы типа string, а ilist как список с элементами типа int. Оба контейнера при таком определении

if ( svec.empty() != true )

пусты. Чтобы убедиться в этом, можно вызвать функцию-член empty():

; // что-то не так

Простейший метод вставки элементов использование функции-члена push_back(),

string text_word;

while ( cin >> text_word )

которая добавляет элементы в конец контейнера. Например: svec.push_back( text_word );

Здесь строки из стандартного ввода считываются в переменную text_word, и затем копия каждой строки добавляется в контейнер svec с помощью push_back().

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

int ia[ 4 ] = { 0, 1, 2, 3 };

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

vector< string, allocator > svec;

list< int, allocator >

ilist;

С++ для начинающих

254

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

Использование push_back() ilist.push_back( ia[ ix ] );

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

создаст последовательность 0, 1, 2, 3, а push_front() ilist.push_front( ia[ ix ] );

создаст последовательность 3, 2, 1, 0. 13

Мы можем при создании явно указать размер массива как константным, так и

#include <list> #include <vector> #include <string>

extern int get_word_count( string file_name ); const int list_size = 64;

list< int > ilist( list_size );

неконстантным выражением:

vector< string > svec(get_word_count(string("Chimera")));

Каждый элемент контейнера инициализируется значением по умолчанию, соответствующим типу данных. Для int это 0. Для строкового типа вызывается конструктор по умолчанию класса string.

list< int > ilist( list_size, -1 );

Мы можем указать начальное значение всех элементов: vector< string > svec( 24, "pooh" );

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

svec.resize( 2 * svec.size() );

Размер svec в этом примере удваивается. Каждый новый элемент получает значение по умолчанию. Если мы хотим инициализировать его каким-то другим значением, то оно указывается вторым параметром функции-члена resize():

13 Если функция-член push_front() используется часто, следует применять тип deque, а не vector: в deque эта операция реализована наиболее эффективно.

С++ для начинающих

255

// каждый новый элемент получает значение "piglet"

svec.resize( 2 * svec.size(), "piglet" );

Кстати, какова наиболее вероятная емкость svec при определении, если его начальный размер равен 24? Правильно, 24! В общем случае минимальная емкость вектора равна его текущему размеру. При удвоении размера емкость, как правило, тоже удваивается

vector< string > svec2( svec );

Мы можем инициализировать новый контейнер с помощью существующего. Например: list< int > ilist2( ilist ) ;

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

ivecl: 1 3 5 7 9 12 ivec2: 0 1 1 2 3 5 8 13 ivec3: 1 3 9

ivec4: 1 3 5 7 ivec5: 2 4

//первый неравный элемент: 1, О

//ivecl больше чем ivec2

ivecl < ivec2 //false ivec2 < ivecl //true

//первый неравный элемент: 5, 9 ivecl < ivec3 //true

//все элементы равны, но ivec4 содержит меньше элементов

//следовательно, ivec4 меньше, чем ivecl

ivecl < ivec4 //false

// первый неравный элемент: 1, 2 ivecl < ivec5 //true

ivecl == ivecl //true ivecl == ivec4 //false ivecl != ivec4 //true

ivecl > ivec2 //true ivec3 > ivecl //true ivec5 > ivec2 //true

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

операция равно”;

операция меньше” (все операции сравнения контейнеров, о которых говорилось выше, используют только эти две операции сравнения);

значение по умолчанию (для класса это означает наличие конструктора по умолчанию).

С++ для начинающих

256

Все предопределенные типы данных, включая указатели и классы из стандартной библиотеки С++ удовлетворяют этим требованиям.

Упражнение 6.5

#include <string> #include <vector> #include <iostream>

#int main()

{

vector<string> svec; svec.reserve( 1024 );

string text_word;

while ( cin >> text_word ) svec.push_back( text_word );

svec.resize( svec.size()+svec.size()/2 );

// ...

Объясните, что делает данная программа:

}

Упражнение 6.6

Может ли емкость контейнера быть меньше его размера? Желательно ли, чтобы емкость была равна размеру: изначально или после вставки элемента? Почему?

Упражнение 6.7

Если программа из упражнения 6.5 прочитает 256 слов, то какова наиболее вероятная емкость контейнера после изменения размера? А если она считает 512 слов? 1000? 1048?

Упражнение 6.8

Какие из данных классов не могут храниться в векторе:

С++ для начинающих

257

(a) class cl1 { public:

c11( int=0 ); bool operator==(); bool operator!=(); bool operator<=(); bool operator<();

// ...

};

(b) class c12 { public:

c12( int=0 ); bool operator!=(); bool operator<=(); // ...

};

(с) class c13 { public:

int ival;

};

(d) class c14 { public:

c14( int, int=0 ); bool operator==(); bool operator!=(); // ...

}

6.5. Итераторы

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

++iter;

перемещает итератор так, что он указывает на следующий элемент контейнера, а

*iter;

разыменовывает итератор, возвращая элемент, на который он указывает.

Все контейнеры имеют функции-члены begin() и end().

begin() возвращает итератор, указывающий на первый элемент контейнера.

end() возвращает итератор, указывающий на элемент, следующий за последним в контейнере.

for ( iter = container. begin();

iter != container.end(); ++iter )

Чтобы перебрать все элементы контейнера, нужно написать:

С++ для начинающих

258

do_something_with_element( *iter );

Объявление итератора выглядит слишком сложным. Вот определение пары итераторов

// vector<string> vec; vector<string>::iterator iter = vec.begin();

вектора типа string:

vector<string>::iterator iter_end = vec.end();

Вклассе vector для определения iterator используется typedef. Синтаксис vector<string>::iterator

ссылается на iterator, определенный с помощью typedef внутри класса vector, содержащего элементы типа string.

for( ; iter != iter_end; ++iter )

Для того чтобы напечатать все элементы вектора, нужно написать: cout << *iter << '\n';

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

В дополнение к типу iterator в каждом контейнере определен тип const_iterator, который необходим для навигации по контейнеру, объявленному как const.

#include <vector>

void even_odd( const vector<int> *pvec, vector<int> *pvec_even, vector<int> *pvec_odd )

{

// const_iterator необходим для навигации по pvec vector<int>::const_iterator c_iter = pvec->begin(); vector<int>::const_1terator c_iter_end = pvec->end();

for ( ; c_iter != c_iter_end; ++c_iter ) if ( *c_iter % 2 )

pvec_even->push_back( *c_iter ); else pvec_odd->push_back( *c_iter );

const_iterator позволяет только читать элементы контейнера:

}

Что делать, если мы хотим просмотреть некоторое подмножество элементов, например взять каждый второй или третий элемент, или хотим начать с середины? Итераторы поддерживают адресную арифметику, а значит, мы можем прибавить некоторое число к итератору:

vector<int>::iterator iter = vec->begin()+vec.size()/2;

iter получает значение адреса элемента из середины вектора, а выражение

С++ для начинающих

259

iter += 2;

сдвигает iter на два элемента.

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

ilist.begin() + 2;

так как для перемещения на два элемента необходимо два раза перейти по адресу, содержащемуся в закрытом члене next. У классов vector и deque перемещение на два элемента означает прибавление 2 к указателю на текущий элемент. (Адресная арифметика рассматривается в разделе 3.3.)

Объект контейнерного типа может быть инициализирован парой итераторов,

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

#include <vector> #include <string> #include <iostream>

int main()

{

vector<string> svec; string intext;

while ( cin >> intext ) svec.push_back( intext );

// обработать svec ...

копируемым.) Допустим, есть вектор:

}

Вот как можно определить новые векторы, инициализируя их элементами первого

int main() { vector<string> svec;

//...

//инициализация svec2 всеми элементами svec vector<string> svec2( svec.begin(), svec.end() );

//инициализация svec3 первой половиной svec vector<string>::iterator it =

svec.begin() + svec.size()/2; vector<string> svec3 ( svec.begin(), it );

//...

вектора:

}

С++ для начинающих

260

Использование специального типа istream_iterator (о нем рассказывается в разделе

#include <vector> #include <string> #include <iterator>

int mainQ

{

//привязка istream_iterator к стандартному вводу istream_iterator<string> infile( cin );

//istream_iterator, отмечающий конец потока istream_iterator<string> eos;

//инициализация svec элементами, считываемыми из cin; vector<string> svec( infile, eos );

//...

12.4.3) упрощает чтение элементов из входного потока в svec:

}

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

#include <string> string words[4] = {

"stately", "plump", "buck", "mulligan"

массив строк:

};

Мы можем инициализировать вектор с помощью указателей на первый элемент массива и на элемент, следующий за последним:

vector< string > vwords( words, words+4 );

Второй указатель служит стражем”: элемент, на который он указывает, не копируется.

int ia[6] = { 0, 1, 2, 3, 4, 5 };

Аналогичным образом можно инициализировать список целых элементов: list< int > ilist( ia, ia+6 );

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

Упражнение 6.9

Какие ошибки допущены при использовании итераторов:

С++ для начинающих

261

const vector< int > ivec;

vector< string >

svec;

list< int >

ilist;

(a) vector<int>::iterator it = ivec.begin();

(b) list<int>::iterator it = ilist.begin()+2;

(c)vector<string>::iterator it = &svec[0];

(d)for ( vector<string>::iterator

it = svec.begin(); it != 0; ++it )

// ...

Упражнение 6.10

int ia[7] = { 0, 1, 1, 2, 3, 5, 8 }; string sa[6] = {

"Fort Sumter", "Manassas", "Perryville", "Vicksburg", "Meridian", "Chancellorsvine" };

(a)vector<string> svec( sa, &sa[6] );

(b)list<int> ilist( ia+4, ia+6 );

(c)list<int> ilist2( ilist.begin(), ilist.begin()+2 );

(d)vector<int> ivec( &ia[0], ia+8 );

(e)list<string> slist( sa+6, sa );

Найдите ошибки в использовании итераторов:

(f)vector<string> svec2( sa, sa+6 );

6.6.Операции с последовательными контейнерами

Функция-член push_back() позволяет добавить единственный элемент в конец контейнера. Но как вставить элемент в произвольную позицию? А целую последовательность элементов? Для этих случаев существуют более общие операции.

vector< string > svec; list< string > slist; string spouse( "Beth" );

slist.insert( slist.begin(), spouse );

Например, для вставки элемента в начало контейнера можно использовать: svec.insert( svec.begin(), spouse );

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