Что такое итератор на самом деле
Каждый стандартный контейнер — 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, или зарезервируйте ёмкость заранее.