21.3 – Обзор итераторов STL
Итератор – это объект, который может перемещаться по контейнерному классу (перебирать его элементы), при этом пользователю не нужно знать, как реализован этот контейнер. Для многих классов (особенно списков и ассоциативных классов) итераторы являются основным способом доступа к элементам.
Итератор лучше всего представить себе как указатель на заданный элемент в контейнере с набором перегруженных операторов для предоставления набора четко определенных функций:
operator*– разыменование итератора возвращает элемент, на который итератор в данный момент указывает;operator++– перемещает итератор к следующему элементу в контейнере. Большинство итераторов также предоставляютoperator--для перехода к предыдущему элементу;operator==иoperator!=– базовые операторы сравнения, чтобы определить, указывают ли два итератора на один и тот же элемент. Чтобы сравнить значения, на которые указывают два итератора, сначала разыменуйте эти итераторы, а затем используйте оператор сравнения;operator=– присваивание итератору новой позиции (обычно в начало или в конец элементов контейнера). Чтобы присвоить значение элемента, на который указывает итератор, сначала разыменуйте этот итератор, а затем используйте оператор присваивания.
Каждый контейнер включает четыре основные функции-члена для использования с operator=:
begin()возвращает итератор, представляющий начало элементов в контейнере;end()возвращает итератор, представляющий элемент сразу за концом элементов;cbegin()возвращает константный (только для чтения) итератор, представляющий начало элементов в контейнере;cend()возвращает константный (только для чтения) итератор, представляющий элемент сразу за концом элементов.
Может показаться странным, что end() не указывает на последний элемент в списке, но это сделано в первую очередь для упрощения циклов: итерация по элементам может продолжаться до тех пор, пока итератор не достигнет end(), и тогда вы знаете, что перебрали все элементы контейнера.
Наконец, все контейнеры предоставляют (как минимум) два типа итераторов:
container::iteratorпредоставляет итератор для чтения/записи;container::const_iteratorпредоставляет итератор только для чтения.
Давайте посмотрим на несколько примеров использования итераторов.
Итерация по контейнеру vector
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vect;
for (int count=0; count < 6; ++count)
vect.push_back(count);
std::vector<int>::const_iterator it; // объявляем итератор только для чтения
it = vect.cbegin(); // присваиваем ему начало вектора
while (it != vect.cend()) // пока it не достигнет до конца
{
std::cout << *it << " "; // выводим значение элемента, на который указывает it
++it; // и переходим к следующему элементу
}
std::cout << '\n';
}
Этот напечатает следующее:
0 1 2 3 4 5
Итерация по контейнеру list
А теперь сделаем то же самое со списком:
#include <iostream>
#include <list>
int main()
{
std::list<int> li;
for (int count=0; count < 6; ++count)
li.push_back(count);
std::list<int>::const_iterator it; // объявляем итератор
it = li.cbegin(); // присваиваем ему начало списка
while (it != li.cend()) // пока it не достигнет до конца
{
std::cout << *it << " "; // выводим значение элемента, на который указывает it
++it; // и переходим к следующему элементу
}
std::cout << '\n';
}
Этот код печатает:
0 1 2 3 4 5
Обратите внимание, что код почти идентичен случаю с вектором, хотя векторы и списки внутри имеют почти совершенно разные реализации!
Итерация по контейнеру set
В следующем примере мы собираемся создать набор из 6 чисел и использовать итератор для печати значений в этом наборе:
#include <iostream>
#include <set>
int main()
{
std::set<int> myset;
myset.insert(7);
myset.insert(2);
myset.insert(-6);
myset.insert(8);
myset.insert(1);
myset.insert(-4);
std::set<int>::const_iterator it; // объявляем итератор
it = myset.cbegin(); // присваиваем ему начало набора
while (it != myset.cend()) // пока it не достигнет до конца
{
std::cout << *it << " "; // выводим значение элемента, на который указывает it
++it; // и переходим к следующему элементу
}
std::cout << '\n';
}
Эта программа дает следующий результат:
-6 -4 1 2 7 8
Обратите внимание, что, хотя заполнение набора отличается от того, как мы заполняем вектор и список, код, используемый для перебора элементов набора, по сути, идентичен.
Итерация по контейнеру map
Это немного сложнее. map и multimap принимают пары элементов (определяемые как std::pair). Чтобы было проще создавать пары, мы используем вспомогательную функцию make_pair(). std::pair позволяет получить доступ к элементам пары через члены first (первый) и second (второй). В нашем контейнере map мы используем первый элемент пары как ключ, а второй – как значение.
#include <iostream>
#include <map>
#include <string>
int main()
{
std::map<int, std::string> mymap;
mymap.insert(std::make_pair(4, "apple"));
mymap.insert(std::make_pair(2, "orange"));
mymap.insert(std::make_pair(1, "banana"));
mymap.insert(std::make_pair(3, "grapes"));
mymap.insert(std::make_pair(6, "mango"));
mymap.insert(std::make_pair(5, "peach"));
// объявляем константный итератор и присваиваем ему начало контейнера map
auto it{ mymap.cbegin() };
while (it != mymap.cend()) // пока it не достигнет конца
{
// выводим значение элемента, на который указывает it
std::cout << it->first << "=" << it->second << " ";
++it; // и переходим к следующему элементу
}
std::cout << '\n';
}
Эта программа дает следующий результат:
1=banana 2=orange 3=grapes 4=apple 5=peach 6=mango
Обратите внимание на то, насколько легко итераторы позволяют проходиться по всем элементам контейнера. Вам совершенно не нужно заботиться о том, как map хранит свои данные!
Заключение
Итераторы предоставляют простой способ перебрать элементы контейнерного класса без необходимости понимать, как реализован этот контейнерный класс. В сочетании с алгоритмами STL и функциями-членами контейнерных классов итераторы становятся еще мощнее. В следующем уроке вы увидите пример использования итератора для вставки элементов в список (который не предоставляет перегруженный operator[] для прямого доступа к своим элементам).
Стоит отметить один момент: итераторы должны быть реализованы для каждого класса, потому что итератору необходимо знать, как реализован класс. Таким образом, итераторы всегда привязаны к конкретным классам контейнеров.
