Итератор (iterator) в C++ — это объект, который указывает на элемент контейнера и умеет переходить к следующему. Через итератор читают и меняют элемент (*it), сдвигаются (++it) и сравнивают позицию с концом (it != v.end()). Именно через пары итераторов с контейнерами работают алгоритмы STL: std::sort, std::find, std::accumulate.
Содержание
- Мини-словарь: что с чем путают
- Полный пример: begin, end и range-based for
- Категории итераторов: что умеет каждый
- std::next, std::advance, std::distance
- Инвалидация: когда итератор становится недействительным
- Итераторы в Си-семействе: C, C++, C# и Java
- Обратные и константные итераторы, поиск
- Свой итератор: чтобы работали range-for и алгоритмы
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — полный рабочий пример, категории итераторов, std::next и std::advance, инвалидация (главный источник ошибок) и собственный итератор. Примеры проверены в Apple clang 21 (libc++) и GCC 14.2 (libstdc++) с -std=c++20.
Мини-словарь: что с чем путают
- Итератор — объект-«позиция» в последовательности. У
std::vectorэто класс, который ведет себя как указатель, но указателем быть не обязан. - Указатель — адрес в памяти. Указатель на элемент массива — частный случай итератора: к нему применимы
*,++,!=. - Индекс — номер элемента. Работает только там, где есть доступ по номеру (
vector,array,string), а уlist,set,mapиндексов нет, итераторы есть. end()— позиция за последним элементом, а не сам последний элемент. Разыменовыватьend()нельзя: это неопределенное поведение (UB).
Полный пример: begin, end и range-based for
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{10, 20, 30};
// 1. Явный итератор: от begin() до end()
for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
*it += 1; // разыменование дает доступ к элементу
}
// 2. То же самое через range-based for (C++11)
for (int x : v) {
std::cout << x << ' ';
}
std::cout << '\n';
// 3. Расстояние между итераторами и end()
auto it = v.begin() + 1; // итератор на второй элемент
std::cout << *it << ' ' << (v.end() - v.begin()) << '\n';
}
Вывод:
11 21 31
21 3
Разбор. Тип итератора записывается как контейнер<тип>::iterator, на практике чаще пишут auto. Цикл идет, пока it != v.end(): сравнение именно с end(), а не <, потому что < есть не у всех итераторов. Range-based for for (int x : v) компилятор разворачивает в тот же цикл с begin()/end(), поэтому он работает с любым типом, у которого они есть. Разность v.end() - v.begin() дает число элементов, но только у итераторов произвольного доступа.
Если элемент нужно изменить в range-based for, берите ссылку: for (int& x : v) x += 1;. С int x меняется копия.
Категории итераторов: что умеет каждый
Возможности итератора зависят от контейнера. Стандарт делит итераторы на категории, каждая следующая добавляет операции к предыдущей (кроме выходного, он стоит отдельно).
| Категория | Операции сверх предыдущей | Где встречается |
|---|---|---|
| Входной (input) | *it на чтение, ++, ==/!=, один проход |
std::istream_iterator |
| Выходной (output) | *it = x на запись, ++, один проход |
std::ostream_iterator, std::back_inserter |
| Однонаправленный (forward) | многократный проход по тем же элементам | std::forward_list, unordered_map, unordered_set |
| Двунаправленный (bidirectional) | --it |
std::list, std::set, std::map |
| Произвольного доступа (random access) | it + n, it - n, it[n], <, разность итераторов |
std::deque |
| Непрерывный (contiguous, C++20) | элементы лежат в памяти подряд | std::vector, std::array, std::string, обычный массив |
Практический смысл таблицы: алгоритм требует минимальную категорию. std::sort нужны итераторы произвольного доступа, поэтому std::sort(l.begin(), l.end()) для std::list не скомпилируется — у списка есть свой метод l.sort().
std::next, std::advance, std::distance
Частая ошибка — сдвигать итератор списка через +:
#include <list>
int main() {
std::list<int> l{1, 2, 3, 4};
auto it = l.begin() + 2; // ошибка: у list нет operator+
return *it;
}
Компиляция останавливается, первая строка ошибки в clang 21 (libc++) и в GCC 14.2 (libstdc++):
ex2bad.cpp:5:25: error: invalid operands to binary expression ('iterator' (aka '__list_iterator<int, void *>') and 'int')
ex2bad.cpp:5:25: error: no match for 'operator+' (operand types are 'std::__cxx11::list<int>::iterator' and 'int')
Текст зависит от компилятора и библиотеки, но суть одна: оператора + у двунаправленного итератора нет. Исправление — функции из <iterator>, которые работают с любой категорией:
#include <iostream>
#include <iterator>
#include <list>
#include <vector>
int main() {
std::list<int> l{1, 2, 3, 4};
std::vector<int> v{1, 2, 3, 4};
auto li = std::next(l.begin(), 2); // list: 2 шага ++, O(n)
auto vi = std::next(v.begin(), 2); // vector: сразу +2, O(1)
std::cout << *li << ' ' << *vi << '\n';
std::advance(li, -1); // двунаправленный: можно назад
std::cout << *li << ' ' << *std::prev(l.end()) << '\n';
std::cout << std::distance(l.begin(), l.end()) << '\n';
}
Вывод:
3 3
2 4
4
| Функция | Что делает | Меняет итератор? |
|---|---|---|
std::next(it, n) |
возвращает копию, сдвинутую на n | нет |
std::prev(it, n) |
копия, сдвинутая назад (нужен bidirectional) | нет |
std::advance(it, n) |
сдвигает сам it |
да |
std::distance(a, b) |
число шагов от a до b |
нет |
Граница: эти функции не проверяют выход за end(). Сдвиг дальше конца — UB, поэтому число шагов нужно сверять с размером контейнера самому.
Инвалидация: когда итератор становится недействительным
Инвалидация — ситуация, когда после изменения контейнера сохраненный итератор больше не указывает на корректный элемент. Любое действие с ним — UB: программа может «работать», печатать мусор или падать.
Неверный код — добавление в вектор во время обхода:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{1, 2, 3};
v.shrink_to_fit(); // просим capacity == size: push_back перевыделит память
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == 2) {
v.push_back(20); // итератор it становится недействительным
}
std::cout << *it << '\n';
}
}
Без диагностики такой код обычно не падает, а печатает мусор: в прогоне clang 21 вывод был 1 0 2 0 1 2 3 20 20 20, в GCC 14.2 — сотни чисел из освобожденной памяти. Конкретный симптом зависит от компилятора и запуска, это UB. С AddressSanitizer (clang++ -g -fsanitize=address) видно, что происходит на самом деле: после 1 программа останавливается с ошибкой
==82396==ERROR: AddressSanitizer: heap-use-after-free on address 0x6020000000d4 ...
push_back перевыделил память, старый буфер освобожден, а it указывает в него. Исправление при удалении элементов — брать итератор, который возвращает erase, или в C++20 — std::erase_if:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{1, 2, 3, 4, 5, 6};
// Удаление в цикле: erase возвращает итератор на следующий элемент
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) {
it = v.erase(it); // не делаем ++it после erase
} else {
++it;
}
}
for (int x : v) std::cout << x << ' ';
std::cout << '\n';
// C++20: то же одной строкой, возвращает число удаленных
std::vector<int> w{1, 2, 3, 4, 5, 6};
auto removed = std::erase_if(w, [](int x) { return x % 2 == 0; });
std::cout << removed << ' ' << w.size() << '\n';
}
Вывод: 1 3 5 и 3 3. Цикл с erase для вектора квадратичный в худшем случае (каждый erase сдвигает хвост), std::erase_if проходит за один раз — на больших данных выбирайте его. Для добавления при обходе надежнее собрать новые элементы в отдельный вектор и дописать после цикла.
Упрощенная сводка правил (подробные случаи, например для deque, — в документации конкретного контейнера):
| Контейнер | Вставка | Удаление |
|---|---|---|
vector, string |
при перевыделении памяти — все итераторы; иначе от точки вставки и дальше | от удаленного элемента и дальше |
list, set, map |
не инвалидирует | только итераторы на удаленные элементы |
unordered_map, unordered_set |
при rehash — все итераторы (ссылки на элементы остаются) | только на удаленные |
Итераторы в Си-семействе: C, C++, C# и Java
Запрос «iterator c» часто задают про чистый C. В языке C отдельных итераторов нет, их роль играет указатель, и идея begin/end оттуда же:
#include <stdio.h>
int main(void) {
int a[] = {3, 1, 4};
const int *end = a + sizeof a / sizeof a[0]; /* адрес "за последним" */
for (const int *p = a; p != end; ++p) {
printf("%d ", *p);
}
printf("\n");
return 0;
}
Программа печатает 3 1 4. Указатель на позицию сразу за массивом стандарт C разрешает вычислять и сравнивать, но не разыменовывать — ровно как end() в C++.
| Язык | Что играет роль итератора | Шаг и доступ | Конец |
|---|---|---|---|
| C | указатель | ++p, *p |
указатель за последним элементом |
| C++ | container::iterator |
++it, *it |
end() |
| C# | IEnumerator<T> |
MoveNext(), Current |
MoveNext() вернул false |
| Java | Iterator<T> |
hasNext(), next() |
hasNext() вернул false |
В C# и Java итератор сам знает, где конец, а в C++ конец — отдельный объект, и пара begin/end задает диапазон. Как это устроено в C#, разобрано в статье про цикл foreach в C#.
Обратные и константные итераторы, поиск
#include <iostream>
#include <map>
#include <string>
#include <vector>
int main() {
const std::vector<int> v{1, 2, 3};
for (auto it = v.rbegin(); it != v.rend(); ++it) std::cout << *it << ' ';
std::cout << '\n';
std::map<std::string, int> ages{{"Ann", 30}, {"Bob", 25}};
auto found = ages.find("Bob");
if (found != ages.end()) { // end() = "не найдено"
std::cout << found->first << '=' << found->second << '\n';
}
auto cit = v.cbegin();
// *cit = 5; // не скомпилируется: const_iterator только для чтения
std::cout << *cit << '\n';
}
Вывод: 3 2 1, Bob=25, 1. rbegin()/rend() дают обход с конца, cbegin() — константный итератор, запись через него — ошибка компиляции (clang: cannot assign to return value because function 'operator*' returns a const value, GCC: assignment of read-only location). find возвращает end(), если ключа нет, поэтому результат сравнивают с end() до разыменования. Доступ к полям элемента — через ->, как у указателя.
Свой итератор: чтобы работали range-for и алгоритмы
Чтобы класс работал в for (x : obj) и с алгоритмами STL, ему нужны begin()/end(), а итератору — *, ++ и ==. Пример — диапазон целых чисел, который ничего не хранит, а вычисляет значения на лету:
#include <cstddef>
#include <iostream>
#include <iterator>
#include <numeric>
// Диапазон целых чисел [first, last) с собственным итератором
class IntRange {
public:
class iterator {
public:
using iterator_concept = std::forward_iterator_tag; // для C++20-концептов
using iterator_category = std::input_iterator_tag; // для старых алгоритмов, как у views::iota
using value_type = int;
using difference_type = std::ptrdiff_t;
using reference = int; // значение вычисляется, не хранится
iterator() = default;
explicit iterator(int v) : cur_(v) {}
int operator*() const { return cur_; }
iterator& operator++() { ++cur_; return *this; } // ++it
iterator operator++(int) { auto t = *this; ++cur_; return t; } // it++
bool operator==(const iterator&) const = default;
private:
int cur_ = 0;
};
IntRange(int first, int last) : first_(first), last_(last < first ? first : last) {}
iterator begin() const { return iterator(first_); }
iterator end() const { return iterator(last_); }
private:
int first_, last_;
};
static_assert(std::forward_iterator<IntRange::iterator>); // проверка концепта C++20
int main() {
IntRange r(1, 6);
for (int x : r) std::cout << x << ' '; // range-based for
std::cout << '\n';
std::cout << std::accumulate(r.begin(), r.end(), 0) << '\n'; // алгоритм STL
IntRange empty(5, 1); // last < first -> пустой диапазон
std::cout << std::distance(empty.begin(), empty.end()) << '\n';
}
Вывод: 1 2 3 4 5, затем 15 и 0. Конструктор защищает от last < first: без этого begin() никогда не догнал бы end(), и цикл ушел бы в бесконечность с переполнением int. static_assert с концептом std::forward_iterator проверяет интерфейс итератора при компиляции. Два тега категории нужны потому, что operator* возвращает значение, а не ссылку: по правилам C++17 такой итератор формально только входной, для концептов C++20 — однонаправленный. Похожий прием у std::views::iota: для int у него iterator_category тоже входной, а iterator_concept — произвольного доступа (там реализованы еще --, += и <).
Выводы
- Итератор в C++ — объект-позиция:
*itдает элемент,++itсдвигает, конец диапазона —end(), который стоит за последним элементом. - Возможности зависят от категории:
it + nиstd::sortесть только у итераторов произвольного доступа, дляlistиmapсдвигайте черезstd::next/std::advance. - Изменение контейнера во время обхода может инвалидировать итераторы; удаляйте через
it = erase(it)илиstd::erase_if. - В C роль итератора играет указатель, в C# и Java — объекты с
MoveNext/hasNext, но идея «позиция + шаг + признак конца» общая. - Свой итератор делает класс совместимым с range-based for и алгоритмами STL; концепты C++20 проверяют его при компиляции.
Где применяется / связь с практикой
Освойте тему на практике
Итераторы — основа работы с STL: через них пишутся обходы контейнеров, вызовы алгоритмов, поиск в map и set, собственные коллекции. На собеседованиях по C++ регулярно спрашивают про категории итераторов и инвалидацию. Контейнеры, итераторы и алгоритмы системно разбирают на курсе «C++-разработчик. Базовый уровень», а попробовать формат можно на бесплатных открытых уроках Otus.
FAQ
Чем iterator отличается от const_iterator?
Через const_iterator элемент можно только читать. Его возвращают cbegin()/cend() и begin() у константного контейнера.
Можно ли сравнивать итераторы разных контейнеров?
Нет, сравнение итераторов из разных контейнеров — UB, даже если типы совпадают. Сравнивают только итераторы одного диапазона.
Нужно ли подключать <iterator>, чтобы пользоваться begin() и end()?
Для методов контейнера — нет, они объявлены в заголовке контейнера. <iterator> нужен для std::next, std::advance, std::distance, std::back_inserter и концептов вроде std::forward_iterator.



