Перейти к содержимому
Теория · Контейнеры и итераторы · урок 5 из 11

Итераторы

Цикл по диапазону обходит все элементы контейнера, но не сообщает, где лежит текущий. А функциям, которые удаляют элементы, нужно именно место элемента в контейнере. У std::map и std::set индексов нет, поэтому место обозначает итератор (iterator) — объект, который указывает на элемент контейнера:

C++
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};
    for (auto it = v.begin(); it != v.end(); ++it) {
        std::cout << *it << ' ';  // 10 20 30
    }
}

begin и end

v.begin() возвращает итератор на первый элемент, а v.end() — на место сразу за последним. Элемента в позиции end() нет, и чтение *v.end() — неопределённое поведение. Поэтому цикл идёт, пока it != v.end(). У пустого контейнера begin() равен end(), и тело цикла не выполняется ни разу.

Итератор похож на указатель (pointer): *it даёт элемент, а ++it переходит к следующему. Полное имя его типа — std::vector<int>::iterator, поэтому тип выводят через auto. Так же устроены итераторы std::map и std::set, а цикл по диапазону — сокращённая запись такого цикла.

Удаление по итератору

v.erase(it) удаляет элемент, на который указывает it, и сдвигает следующие элементы на его место. После этого it и все итераторы за ним становятся недействительными (invalidated): пользоваться ими — неопределённое поведение. Взамен erase возвращает итератор на элемент, который шёл за удалённым. Поэтому цикл удаления продвигает итератор сам:

C++
std::vector<int> v{1, 2, 4, 5, 6};
for (auto it = v.begin(); it != v.end();) {
    if (*it % 2 == 0) {
        it = v.erase(it);
    } else {
        ++it;
    }
}
for (int x : v) {
    std::cout << x << ' ';  // 1 5
}

Шага в заголовке цикла нет. После удаления it уже указывает на следующий элемент, а ++it выполняется, только если ничего не удалили. У std::map и std::set вызов erase(it) делает недействительным только итератор на удалённый элемент, а цикл удаления пишут так же.

Частая ошибка

Цикл по вектору добавляет элементы в этот же вектор:

C++
std::vector<int> v{1, 2, 3};
for (int x : v) {
    v.push_back(x * 10);  // ✗
}

Цикл по диапазону получает итераторы begin() и end() один раз, до первой итерации. push_back всегда делает недействительным end(), а при перевыделении памяти — и все итераторы, указатели и ссылки на элементы. Поэтому этот цикл — неопределённое поведение при любой ёмкости. Запомните размер до цикла и обходите вектор по индексу: индекс остаётся верным и после перевыделения памяти.

C++
std::vector<int> v{1, 2, 3};
std::size_t n = v.size();
for (std::size_t i = 0; i < n; ++i) {
    v.push_back(v[i] * 10);
}
for (int x : v) {
    std::cout << x << ' ';  // 1 2 3 10 20 30
}