Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]
.pdfС++ для начинающих |
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() (итератор, адресующий некоторый элемент контейнера) задает позицию, а второй – вставляемое перед этой позицией значение. В примере выше элемент добавляется в начало контейнера. А так можно реализовать вставку в произвольную позицию: