Итераторы
Итераторы, как было замечено выше, являются центральным механизмом, обеспечивающим работу с данными контейнеров. Они являются аналогом указателей и делают возможным циклический перебор всех элементов контейнера. Существуют разные виды итераторов, поскольку различные алгоритмы по-разному обращаются к данным. Каждый класс контейнера может порождать итераторы, необходимые для работы адекватных ему алгоритмов.
Подобно указателю, итератор может ссылаться на единственный элемент данных; пара итераторов может задавать определенный диапазон контейнера; итератор может иметь т. н. запредельное значение, аналогичное NULL и означающее, что его нельзя разыменовывать.
Следует упомянуть, что при вызове различных алгоритмов для диапазона, заданного парой итераторов, второй из них соответствует не последнему значению итератора в диапазоне, а следующему за ним.
Основными операциями над итераторами- являются, как и в случае указателей, разыменование и инкремент. Если итератор i после конечного ряда приращений может стать равным итератору j, то говорят, что итератор j достижим из i. Если к итератору, достигшему верхней границы диапазона, применить операцию инкремента, он примет запредельное значение.
Сделав такие предварительные замечания, мы перейдем теперь к конкретному изучению итераторов библиотеки стандартных шаблонов.
Типы итераторов
Существует пять основных форм итераторов:
Итераторы, стоящие в этом списке ниже, выводятся из тех, что находятся выше. Это едва ли не единственный пример классовой иерархии в 8TL.
В следующей таблице показано, какими контейнерами стандартной библиотеки генерируются те ли иные итераторы.
Таблица 10.2. Итераторы, генерируемые стандартной библиотекой
Форма итератора | Контейнеры |
входной итератор | istream iterator |
выходной итератор | ostream iterator |
двунаправленный итератор | List set и multiset map и multimap |
итератор произвольного доступа |
обычные указатели vector deque |
Указатели как итераторы
Чтобы продемонстрировать, как применяются итераторы, мы рассмотрим простейший вид итератора — обычный указатель. Следующая программа вызывает стандартный алгоритм find() для поиска значения в обычном массиве.
#include <algorithm>
#include <iostream>
using namespace std;
#define SIZE 50 int iArr[SIZE] ;
int main() {
iArr[30] = 33;
int *ip = find(iArr, iArr + SIZE, 33);
if (ip != iArr + SIZE)
cout<< "Value "<< *ip<< " found at position "<< (ip - iArr)<< endl;
else
cout << "Value not found."<< endl;
return 0;
}
Прежде всего обратите внимание, что программа, применяющая стандартную библиотеку C++, должна специфицировать директивой using namespace пространство имен std.
В примере объявляется “контейнер” — обычный массив длиной в 50 элементов, одному из его элементов присваивается значение 33 и вызывается алгоритм find () для поиска этого значения.
Алгоритму find () передаются три аргумента. Два из них — итераторы, задающие диапазон поиска. Первый из них в данном случае указывает на начальный элемент массива, второй имеет запредельное значение iArr + SIZE, т. е. смещен на один элемент за верхнюю границу массива. Третий аргумент задает искомое значение.
Если find () находит его в заданном диапазоне, алгоритм возвращает соответствующий ему итератор; если нет, возвращается запредельное значение.
Итераторы контейнеров
Итераторы, генерируемые классами контейнеров, используются точно таким же образом, как указатели в показанном выше примере, но для получения граничных значений итератора вь1зываются обычно функции вроде begin () или end () конкретного контейнерного объекта. Вот совершенно аналогичный предыдущему пример для контейнера-вектора:
#include <algorithm>
#include <vector>
#include <iostream>
using namespace std;
#define SIZE 50 vector<int> iVect(SIZE);
int main() {
iVect[30] = 33;
vector<int>::iterator ii =
find (iVect. begin (), iVect.endO, 33);
if (ii != iVect.endO)
cout << "Value "<< *ii<< " found at position "
<< distance(iVect.begin(), ii) << endl;
else
cout << "Value not found." <<end1;
return 0;
Объявляемый в программе контейнер имеет тип vector<int>, а итератор — тип vector<int>: : iterator. Каждый стандартный контейнер объявляет свой собственный вложенный класс iterator.
Далее мы вкратце рассмотрим различные формы итераторов.
Входные, выходные и поступательные итераторы
Простейший из итераторов — входной. Он может перемещаться по контейнеру только в поступательном направлении и допускает только чтение данных. Первые два параметра алгоритма find (), например, должны быть входными итераторами. Выходной итератор отличается от входного правом доступа. Он допускает только запись данных в контейнер.
К обеим этим формам итераторов можно применять, по меньшей мере, операцию неравенства (!=), разыменования (*) и инкремента (++).
Ниже показан пример копирования массива в вектор при посредстве выходного итератора и алгоритма copy (). Его последним параметром может быть любой выходной итератор. На самом деле тот же итератор здесь используется и как входной — в операторе вывода.
#include <algorithm>
#include <vector>
#include <iostream>
using namespace std;
double dArr[10] =
{1.0, 1.1, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9};
vector<double> dVect(lO);
int main()
{
vector<double>::iterator oi = dVect.begin ();
copy(dArr, dArr + 10, oi);
while (oi != dVect.endO) {
cout << *oi << endl;
oi++;
} return 0;
}
Итераторы потоков
Собственно, только входные и только выходные итераторы имеют смысл в основном при работе с потоками ввода-вывода, которые могут быть допускать либо только извлечение, либо только передачу данных. Любые контейнеры стандартной библиотеки генерируют более сложные, итераторы, которые, естественно, могут применяться и в качестве простых входных или выходных. / Вы уже хорошо знакомы со стандартными потоками cin и cout, извлечение и передача данных из которых производится операциями >> и <<. Однако возможен другой метод работы с этими потоками, при котором входной или выходной объект iostream преобразуется в итератор. Затем его можно передавать как аргумент стандартным алгоритмам.
Например, ниже показано, как можно применить выходной итератор для вывода на экран содержимого контейнера.
#include <algorithm>
#include <vector>
#include <iostream>
using namespace std;
int main( ) {
vector<int> iVect(lO);
for (int i=0; i<10; i++) iVect[i] = i;
cout<< "The vector contents are: { ";
copy(iVect.begin (),
iVect.endf), ostream_iterator<int>(cout, " "));
cout << "}." << endl;
return 0;
}
Последний параметр алгоритма copy () конструирует выходной итератор типа ostream iterator<int>. Параметрами конструктора являются выходной поток и строка - разделитель значений.
Поступательный итератор допускает как чтение, так и запись в контейнер. Однако, как и в случае двух предыдущих, возможен только инкремент, но не декремент итератора. Поступательные итераторы могут использоваться, например, в алгоритме replace (), который определяется так:
template <class Forwardlterator, class T>
void replace(Forwardlterator first,
Forwardlterator last,
const T &old_value,
const T &new_value);
Этот алгоритм заменяет все значения old_value, содержащиеся в контейнере, на new_value.
Двунаправленные итераторы
Двунаправленные итераторы допускают чтение и запись данных и к ним можно применять операции как инкремента, так и декремента. Такие итераторы могут быть, например, аргументами алгоритма reverse () , который меняет порядок элементов контейнера на обратный:
template <class Bidirectioriallterator>
void reverse(Bidirectionallterator first,Bidirectionallterator.last);
Такой алгоритм может быть полезен, если, скажем, из контейнера, уже сортированного в восходящем порядке, вы хотите получить контейнер с нисходящей сортировкой.
Итераторы произвольного доступа
Эти итераторы являются наиболее универсальными с точки зрения возможностей доступа. Их можно использовать для произвольного чтения и записи данных контейнера. (Обычные указатели принадлежат, кстати, к этому виду итераторов.) Такие итераторы участвуют в алгоритмах сортировки, входящих в стандартную библиотеку.
Алгоритм random_shuffle, также требующий итераторов произвольного доступа, случайным образом переставляет значения элементов в указанном диапазоне контейнера:
template <class RandomAccessIterator>
void random shuffle(RandomAccessIterator first, RandomAccessIterator last);
Итераторы вставки
Для вставки значений в контейнер применяются итераторы вставки. Их называют также адаптерами, поскольку они преобразуют контейнер в итератор, т. е. адаптируют его к специальному использованию в алгоритмах вроде copy (). Имеется три вида итераторов вставки:
При вставке данных в контейнер может произойти перемещение уже находившихся там данных, из-за чего некоторые итераторы станут недействительными. Это может случиться в случае вектора, но не списков, в которых данные при вставке не смещаются.
В типичном случае адаптер вставки применяется после поиска некоторого значения, как показано в следующем примере.
////////////////////////////////////////////////////////////////
// Inserter.срр: Демонстрация итераторов вставки. //
#include <algorithm>
#include <list>
#include <iostream>
#pragma hdrstop
#include <condefs.h>
using namespace.std;
int iArr[5] = (1, 2, 3, 4, 5);
//
// Функция вывода содержимого списка.
//
void Display(list<int> &1, const char *label)
(
cout << label<< ": { ";
copy (1 .begin (), 1.end(),
ostream_iterator<int>(cout, " "));
cout << "}" << endl;
}
int main(void) {
list<int> iLst; // Создание объекта списка.
// Копирование массива в список в обратном порядке:
copy(iArr, iArr + 5, front_inserter(iLst));
Display(iLst, "Before insertion");
// Поиск значения З:
list<int>::iterator i = find(iLst.begin(),
iLst.end(), 3) ;
// Вставка массива в список:
copy(iArr, iArr + 5, inserter(iLst, i));
Display(iLst, "After insertion ");
cin.ignore ();
return 0;
}
Рис. 11. 1 показывает результат работы программы. Можно отметить различие между inserter (iLst, i-Lst. begin ()) и front inserter (iLst). Первый адаптер вставляет данные в контейнер в прямом, а второй — в обратном порядке.
Рис. 11.1 Демонстрация адаптеров
Функции итераторов
Имеются две функции, которые могут оказаться полезными при работе с итераторами. Это advance () и distance (.) .
Функция advance () выполняет инкремент или декремент итератора указанное число раз. Ей передается итератор и число, определяющее число повторений инкремента или декремента (при отрицательном аргументе). Допустим, вам требуется найти некоторый элемент списка и установить итератор на несколько позиций за ним. Вы можете написать:
list<int> :: iterator i = find (iLst .begin (), iLst.endO, 3);
advance(i, 2); // Сдвигает итератор на 2 позиции вперед.
С функцией distance () вы уже встречались в примере параграфа “Итераторы контейнеров”, где с ее помощью выяснялась позиция итератора по отношению к началу вектора. Эта функция определяет количество инкрементов, которые нужно выполнить для перехода от одного итератора к другому. Она перегружена:
template <class Forwardlterator> iterator_traits<Forward!terator>::
difference_type distance(Forwardlterator first, Forwardlterator last) ;
template <class Forwardlterator, class Distance>
void distance(Forwardlterator first,
Forwardlterator last. Distance &n) ;
В упомянутом примере функция применялась в своей первой, несколько пугающей, форме, но на деле она проще второй, устаревшей. Она возвращает расстояние между итераторами как свое значение. Вторая форма функции передает расстояние в третьем параметре. Функция аккумулирует значение в этом параметре, поэтому его требуется перед вызовом инициализировать:
int d = 0;
distance (iLst, i, d);