Menu

Итераторы в C++: begin, end и типы итераторов

Как итераторы C++ работают в роли обобщённых указателей внутрь контейнеров: begin() и end(), разыменование, продвижение, варианты const/reverse, а также ловушки с инвалидацией и разыменованием end(), приводящие к неопределённому поведению.

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

Что такое итератор на самом деле

Каждый стандартный контейнер — vector, string, map, set, list — хранит свои элементы внутри по-разному. vector — это непрерывный блок, map — сбалансированное дерево, list — связанные узлы. И всё же обходить их все можно одинаково. Делает это возможным итератор: небольшой объект, который «указывает на» один элемент и знает, как шагнуть к следующему.

Считайте итератор обобщённым указателем. Вы получаете его из begin(), читаете элемент, на который он указывает, через *, и продвигаете его вперёд через ++. Части складываются вот так:

v.begin() возвращает итератор на первый элемент; *it даёт вам этот элемент; ++it переходит к следующему. Эта тройка — разыменовать, продвинуть, сравнить — и есть вся ментальная модель.

begin(), end() и полуоткрытый диапазон

Вторая половина картины — это end(). Принципиально важно: end() не указывает на последний элемент — он указывает на ячейку сразу за последним элементом. Это намеренный «полуоткрытый» диапазон [begin, end): begin включён, end — сигнал остановки.

Такая конструкция делает стандартный цикл чистым — вы идёте, пока итератор не станет равен end():

Обратите внимание: it != v.end(), а не it < v.end(). Большинство итераторов контейнеров (например, map или list) не поддерживают <, только == и !=, поэтому != — переносимый выбор. А auto избавляет вас от написания vector<int>::iterator вручную — компилятор выводит тип сам.

Случай пустого контейнера разрешается естественным образом: когда контейнер пуст, begin() == end(), поэтому тело цикла ни разу не выполняется. Никакой особой обработки не нужно.

Никогда не разыменовывайте end()

Самая частая ошибка с итераторами — разыменование end(). Поскольку он указывает на одну позицию за последним элементом, *v.end() читает память, которая вам не принадлежит, — это неопределённое поведение, то есть аварийное завершение или молчаливый мусор, а не дружелюбная ошибка:

vector<int> v = {1, 2, 3};
cout << *v.end();   // НЕОПРЕДЕЛЁННОЕ ПОВЕДЕНИЕ - end() не является элементом

Та же ловушка подстерегает функции поиска. std::find возвращает end(), когда не находит значение, поэтому перед разыменованием нужно проверить результат:

Всегда сравнивайте возвращённый итератор с end(), прежде чем его разыменовывать. Забыть этот if — одна из самых частых причин аварий в STL-коде новичков.

const, cbegin и обратные (reverse) итераторы

Контейнеры выдают разные разновидности итераторов в зависимости от ваших нужд:

  • begin() / end() — обычные итераторы для чтения/записи (*it = ... работает).
  • cbegin() / cend()const_iterator; через них можно читать, но нельзя изменять элемент.
  • rbegin() / rend() — обратные итераторы, идущие с конца к началу; ++ на самом деле движется назад.

Обратные итераторы — это чистый способ пройти в обратном порядке без возни с индексной арифметикой:

С обратными итераторами вы по-прежнему пишете ++it, чтобы продвигаться, — итератор сам обрабатывает «обратное» направление внутри. Используйте cbegin()/cend() (или const-ссылку на контейнер), когда цикл должен только читать, чтобы компилятор не дал вам случайно записать.

Итераторы map выдают pairs

Не каждый итератор — это тонкая обёртка над указателем. Итератор std::map обходит дерево, и его разыменование даёт вам std::pair из ключа и значения, доступных через ->first и ->second (как и указатель, итератор поддерживает ->):

Цикл for на основе диапазона построен прямо на begin()/end(), поэтому для обычного прохода вперёд вы обычно будете тянуться именно к нему. Явные итераторы оправдывают себя, когда вам нужен обратный обход, позиция элемента или передача диапазона в алгоритм.

Главная ловушка: инвалидация итераторов

Это та западня, в которую рано или поздно попадает каждый. Когда вы меняете структуру контейнера, существующие итераторы могут стать инвалидированными — они указывают на память, которая была освобождена или перемещена. Использование такого — неопределённое поведение.

Для vector метод push_back может перевыделить весь буфер для роста, инвалидировав каждый действующий итератор. Удаление во время обхода ещё более печально знаменито — это классическая авария:

vector<int> v = {1, 2, 3, 4};
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it % 2 == 0)
        v.erase(it);   // ОШИБКА - erase инвалидирует it, и тогда ++it — это UB
}

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

Заметьте, что в заголовке for нет ++it — тело само решает, продвигаться ли. (В реальном коде идиома erase-remove или std::erase_if из C++20 делают это одной строкой.) Правило, которое стоит запомнить: любая операция, добавляющая или удаляющая элементы, может инвалидировать итераторы, поэтому не держите старый итератор через такое изменение.

Далее: алгоритмы

Теперь, когда вы умеете описывать диапазон парой begin/end, вы открыли для себя всю библиотеку алгоритмов STL. Функциям вроде sort, find, count и accumulate всё равно, какой у вас контейнер, — они работают с диапазонами итераторов, так что один и тот же вызов работает на vector, массиве или их фрагменте. Дальше мы заставим эти итераторы работать и позволим стандартной библиотеке выполнять циклы за вас.

Часто задаваемые вопросы

Что такое итератор в C++?

Итератор — это объект, который указывает на элемент внутри контейнера и умеет переходить к следующему. Первый получают через container.begin(), а маркер позиции за последним элементом — через container.end(). Разыменуйте его через *it, чтобы прочитать или записать элемент, и продвигайте через ++it. Итераторы — это общий интерфейс, благодаря которому алгоритмы STL работают с любым контейнером.

В чём разница между итератором и указателем в C++?

Для vector или массива итератор ведёт себя почти точно как указатель: вы разыменовываете через *, продвигаете через ++ и сравниваете через ==/!=. Но итератор — это концепция, а не обязательно сырой указатель: итератор map или list обходит дерево или связанные узлы, поэтому это тип-класс, который перегружает * и ++. Указатели — это одна из разновидностей итераторов; итераторы обобщают эту идею на любой контейнер.

Что вызывает инвалидацию итераторов в C++?

Изменение структуры контейнера может оставить существующие итераторы указывающими на освобождённую или перемещённую память. Для vector метод push_back может выполнить перевыделение и инвалидировать все итераторы; erase инвалидирует итераторы на удалённом элементе и после него. Использование инвалидированного итератора — это неопределённое поведение. Чтобы оставаться в безопасности, используйте итератор, который возвращает erase, или зарезервируйте ёмкость заранее.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ