Итераторы

Последнее обновление: 27.03.2023

Итераторы обеспечивают доступ к элементам контейнера и представляют реализацию распространенного паттерна объектно-ориентированного программирования "Iterator". С помощью итераторов очень удобно перебирать элементы. В C++ итераторы реализуют общий интерфейс для различных типов контейнеров, что позволяет использовать единой подход для обращения к элементам разных типов контейнеров.

Стоит отметить, что итераторы имеют только контейнеры, адаптеры контейнеров — типы std::stack, std::queue и std::priority_queue итераторов не имеют.

Итератор описывается типом iterator. Для каждого контейнера конкретный тип итератора будет отличаться. Так, итератор для контейнера list<int> представляет тип list<int>::iterator, а итератор контейнера vector<int> представляет тип vector<int>::iterator и так далее. Однако общий функционал, который применяется для доступа к элементам, будет аналогичен.

Для получения итераторов контейнеры в C++ обладают такими функциями, как begin() и end(). Функция begin() возвращает итератор, который указывает на первый элемент контейнера (при наличии в контейнере элементов). Функция end() возвращает итератор, который указывает на следующую позицию после последнего элемента, то есть по сути на конец контейнера. Если контейнер пуст, то итераторы, возвращаемые обоими методами begin и end совпадают. Если итератор begin не равен итератору end, то между ними есть как минимум один элемент.

Обе этих функции возвращают итератор для конкретного типа контейнера:

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> numbers{ 1,2,3,4 };
    std::vector<int>::iterator iter = numbers.begin();  // получаем итератор
}

В данном случае создается вектор - контейнер типа vector, который содержит значения типа int. И этот контейнер инициализируется набором {1, 2, 3, 4}. И через метод begin() можно получить итератор для этого контейнера. Причем этот итератор будет указывать на первый элемент контейнера.

С итераторами можно проводить следующие операции:

  • *iter: получение элемента, на который указывает итератор

  • ++iter: перемещение итератора вперед для обращения к следующему элементу

  • --iter: перемещение итератора назад для обращения к предыдущему элементу. Итераторы контейнера forward_list не поддерживают операцию декремента.

  • iter1 == iter2: два итератора равны, если они указывают на один и тот же элемент

  • iter1 != iter2: два итератора не равны, если они указывают на разные элементы

  • iter + n: возвращает итератор, который смещен от итератора iter на n позиций вперед

  • iter - n: возвращает итератор, который смещен от итератора iter на n позиций назад

  • iter += n: перемещает итератор на n позиций вперед

  • iter -= n: перемещает итератор на n позиций назад

  • iter1 - iter2: возвращает количество позиций между итераторами iter1 и iter2

  • >, >=, <, <=: операции сравнения. Один итератор больше другого, если указывает на элемент, который ближе к концу

Стоит отметить, что итераторы не всех контейнеров поддерживают все эти операции.

Итераторы для типов std::forward_list, std::unordered_set и std::unordered_map не поддерживают операции --, -= и -. (поскольку std::forward_list - однонаправленный список, где каждый элемент хранит указатель только на следующий элемент)

Итераторы для типа std::list поддерживают операции инкремента и декремента, но не поддерживаются операции +=, -=, + и -. Те же ограничения имеют итераторы контейнеров std::map и std::set.

Операции +=, -=, +, -, <, <=, >, >= и <=> поддерживаются только итераторами произвольного доступа (итераторы контейнеров std::vector, array и deque)

Получение и изменение элемента контейнера

Поскольку итератор по сути представляет указатель на определенный элемент, то через этот указатель мы можем получить текущий элемент итератора и изменить его значение:

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> numbers{ 1,2,3,4 };
    auto iter { numbers.begin() };  // получаем итератор

    // получаем элемент, на который указывает итератор
    std::cout << *iter << std::endl;    // 1
    // изменяем элемент
    *iter = 125;
    // проверяем изменение элемента
    std::cout << numbers[0] << std::endl;    // 125
}

После получения итератора он будет указывать на первый элемент контейнера. То есть при выражение *iter возвратит первый элемент вектора.

Прибавляя или отнимая определенное число, можно переместить итератор вперед или назад на определенное количество элементов:

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> numbers{ 10, 20, 30, 40 };
    auto iter { numbers.begin() };  // получаем итератор

    // переходим на 1 элемент вперед ко 2-му элементу
    ++iter;
    std::cout << *iter << std::endl;    // 20

    // переходим на 2 элемента вперед к 4-му элементу
    iter +=2; 
    std::cout << *iter << std::endl;    // 40

    // переходим назад на 3 элемента к 1-му элементу
    iter = iter - 3;
    std::cout << *iter << std::endl;    // 10
}

Опять же повторю, что стоит учитывать, что не все операции поддерживаются итераторами всех контейнеров.

Перебор контейнера

Например, используем итераторы для перебора элементов вектора:

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> numbers{ 10, 20, 30, 40 };
    auto iter { numbers.begin() };  // получаем итератор

    while(iter!=numbers.end())    // пока не дойдем до конца
    {
        std::cout << *iter << std::endl;// получаем элементы через итератор
        ++iter;             // перемещаемся вперед на один элемент
    }

    // аналогичный пример с циклом for
    for(auto start{numbers.begin()}; start !=numbers.end(); start++ )
    {
        std::cout << *start << std::endl;
    }
}

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

Константные итераторы

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

const vector<int> numbers{1, 2, 3, 4, 5};
for(auto iter {numbers.begin()}; iter != numbers.end(); ++iter)
{
	std::cout << *iter << std::endl;
	// так нельзя сделать
	//*iter = (*iter) * (*iter);
}

В данном случае итератор iter будет представлять тип std::vector<int>::const_iterator.

Для получения константного итератора также можно использовать функции cbegin() и cend. При этом даже если контейнер не представляет константу, но для его перебора используется константный итератор, то опять же нельзя изменять значения элементов этого контейнера:

#include <iostream>
#include <vector>

int main()
{
	std::vector<int> numbers { 1, 2, 3, 4, 5 };
	for (auto iter {numbers.cbegin()}; iter != numbers.cend(); ++iter)
	{
		std::cout << *iter << std::endl;
		// так нельзя сделать, так как итератор константный
		//*iter = (*iter) * (*iter);
	}
}

Стоит отметить, что для типов std::set (множество) и std::map (словарь) доступны только константные итераторы.

Реверсивные итераторы

Реверсивные итераторы позволяют перебирать элементы контейнера в обратном направлении. Для получения реверсивного итератора применяются функции rbegin() и rend(), а сам итератор представляет тип reverse_iterator:

#include <iostream>
#include <vector>
 
int main()
{
    std::vector<int> numbers { 1, 2, 3, 4, 5 };
    for (auto iter {numbers.rbegin()}; iter != numbers.rend(); ++iter)
    {
        std::cout << *iter << "\t";
    }
    std::cout << std::endl;
}

В данном случае итератор будет представлять тип std::vector<int>::reverse_iterator. Консольный вывод программы:

5       4       3       2       1

Если надо обеспечить защиту от изменения значений контейнера, то можно использовать константный реверсивный итератор, который представлен типом const_reverse_iterator и который можно получить с помощью функций crbegin() и crend():

#include <iostream>
#include <vector>
 
int main()
{
    std::vector<int> numbers { 1, 2, 3, 4, 5 };
    for (auto iter {numbers.crbegin()}; iter != numbers.crend(); ++iter)
    {
        std::cout << *iter << std::endl;
        // так нельзя сделать, так как итератор константный
		//*iter = (*iter) * (*iter);
    }
}

Итераторы для массивов

Для массивов в C++ также имеется поддержка итераторов. Для этого в стандартной библиотеке С++ определены функции std::begin() (возвращает итератор на начало массива) и std::end() (возвращает итератор на конец массива):

int data[]{4, 5, 6, 7, 8};
// получаем итератор на начало массива 
auto iter = std::begin(data);
// получаем итератор на конец массива
auto end = std::end(data);

Как и контейнеры, массив можно перебрать с помощью итераторов:

#include <iostream>
 
int main()
{
    int data[]{4, 5, 6, 7, 8};
    // перебор массива с помощью итераторов
    for(auto iter {std::begin(data)}; iter != std::end(data); iter++)
    {
        std::cout << *iter << std::endl;
    }
}

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

#include <iostream>
#include <vector>
 
int main()
{
    int data[]{4, 5, 6, 7, 8};
    std::vector<int> numbers { 1, 2, 3, 4};
    // добавляем в конец вектора numbers из массива data элементы со 2-го по предпоследний (включительно)
    numbers.insert(numbers.end(), std::begin(data) + 1, std::end(data)-1);
    for (auto iter {numbers.begin()}; iter != numbers.end(); ++iter)
    {
        std::cout << *iter << "\t";
    }
    std::cout << std::endl;
}

Здесь строка

numbers.insert(numbers.end(), std::begin(data) + 1, std::end(data)-1);

Добавляет в вектор numbers, начиная с позиции, на которую указывает итератор numbers.end() (то есть в самый конец вектора), диапазон элементов массива data. Начало этого диапазона задается выражением std::begin(data) + 1 (то есть со 2-го элемента), а конуц - выражением std::end(data)-1 (то есть по предпоследний элемент включительно). Консольный вывод:

1       2       3       4       5       6       7
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850