Итераторы в C++: понятное руководство для начинающих с примерами и ошибками новичков

Итераторы в C++: понятное руководство для начинающих с примерами и ошибками новичков

Итераторы в C++: понятное руководство для начинающих с примерами и ошибками новичков

Поисковый запрос, под который оптимизирована статья: «итераторы в C++» и «итераторы C++ для начинающих».

Что такое итераторы в C++ и зачем они нужны

Итератор — это «указатель-подобный» объект, который позволяет перебирать элементы контейнера и передавать поддиапазоны в алгоритмы стандартной библиотеки. Благодаря итераторам алгоритмы не зависят от конкретного контейнера: один и тот же std::sort сортирует std::vector, а std::find ищет в std::list или std::string — всё через единый интерфейс begin/end.

Базовый синтаксис: begin, end, инкремент и разыменование

#include <vector>
#include <iostream>

int main() {
    std::vector<int> v {1, 2, 3};

    // Итерация с помощью итераторов
    for (auto it = v.begin(); it != v.end(); ++it) {
        std::cout << *it << " "; // *it — доступ к значению
    }
}

Ключевые операции: *it (разыменование), ++it (переход к следующему), сравнение с end() (конец диапазона). Никогда не разыменовывайте итератор, равный end() — это UB (неопределённое поведение).

Константные итераторы: защищаем данные от изменений

Если изменять элементы не нужно, используйте const_iterator или удобные cbegin()/cend().

#include <vector>
#include <iostream>

int main() {
    const std::vector<int> v {10, 20, 30};

    // v.begin() у const-объекта уже const_iterator,
    // но можно явно: v.cbegin(), v.cend()
    for (auto it = v.cbegin(); it != v.cend(); ++it) {
        // *it = 42; // Ошибка компиляции — нельзя менять
        std::cout << *it << " ";
    }
}

Итерация по std::map: пара key/value

#include <map>
#include <string>
#include <iostream>

int main() {
    std::map<std::string, int> scores {{"Ann", 5}, {"Bob", 8}};

    for (auto it = scores.begin(); it != scores.end(); ++it) {
        std::cout << it->first << ": " << it->second << "n";
    }
}

Элемент map — это std::pair<const Key, T>, поэтому доступ через it->first (ключ) и it->second (значение).

Категории итераторов (коротко и по делу)

  • Input — только чтение по одному проходу (потоки).
  • Forward — многократный проход вперёд (например, forward_list).
  • Bidirectional — как Forward + шаг назад (например, list, map).
  • RandomAccess — произвольный доступ и арифметика (например, vector, deque).
  • Contiguous (C++20) — элементы в непрерывной памяти (например, vector, string).
  • Чем «сильнее» категория, тем больше операций поддерживается (например, it + n доступно только для RandomAccess).

    Алгоритмы STL с итераторами: must‑have примеры

    #include <vector>
    #include <algorithm>   // find, sort, remove_if
    #include <numeric>     // accumulate
    #include <iostream>
    
    int main() {
        std::vector<int> v {3, 1, 4, 1, 5, 9};
    
        // Поиск первого вхождения 4
        auto it = std::find(v.begin(), v.end(), 4);
        if (it != v.end()) std::cout << "Нашли: " << *it << "n";
    
        // Сортировка (RandomAccess итераторы)
        std::sort(v.begin(), v.end());
    
        // Сумма элементов
        int sum = std::accumulate(v.begin(), v.end(), 0);
        std::cout << "Сумма: " << sum << "n";
    
        // Удаление нечётных: erase + remove_if (идиома erase-remove)
        v.erase(std::remove_if(v.begin(), v.end(), [](int x){ return x % 2 != 0; }), v.end());
    }
    

    Идиома erase-remove — базовый приём: remove_if сдвигает «ненужные» в конец и возвращает новый логический конец, а erase физически удаляет хвостовой диапазон.

    Инвалидация итераторов: где тонко — там рвётся

  • vector: вставки/удаления в середине и рост емкости могут инвалидировать все итераторы, ссылки и указатели на элементы. Безопаснее заново получать итераторы после модификаций.
  • list, forward_list: итераторы сохраняются при вставках/удалениях других элементов, инвалидируется только удалённый элемент.
  • map, set: вставки сохраняют валидность других итераторов; удаление инвалидирует итераторы на удалённые элементы.
  • #include <vector>
    #include <iostream>
    
    int main() {
        std::vector<int> v {1,2,3,4,5};
        auto it = v.begin(); // Указывает на 1
    
        // Вставка может перераспределить память и сделать it невалидным
        v.insert(v.begin() + 2, 42);
    
        // Нельзя больше использовать старый it — возьмите заново:
        it = v.begin();
        std::cout << *it; // ОК
    }
    

    Диапазоны C++20 (ranges): чище, короче, безопаснее

    Модерновый подход — не таскать пары begin(), end(), а работать с «диапазонами» и конвейерами представлений (views).

    #include <vector>
    #include <ranges>
    #include <iostream>
    
    int main() {
        std::vector<int> v {3,1,4,1,5,9,2,6};
    
        // Отфильтровать чётные и умножить на 10, затем вывести
        auto pipe = v | std::views::filter([](int x){ return x % 2 == 0; })
                      | std::views::transform([](int x){ return x * 10; });
    
        for (int x : pipe) std::cout << x << " ";
    }
    

    Компиляция с поддержкой C++20: для g++ -std=c++20, для MSVC выберите стандарт в настройках проекта. Диапазоны уменьшают ошибки с границами и улучшают читаемость.

    Частые ошибки новичков

  • Разыменование end() или итератора на удалённый элемент.
  • Хранение итераторов через долгие промежутки и повторное использование после модификаций контейнера.
  • Неправильная работа с erase в цикле: пропуск элементов. Решение — использовать возвращаемое значение erase как новый итератор.
  • Излишнее копирование: там, где достаточно const_iterator или диапазонов, используют мутабельные итераторы.
  • #include <vector>
    #include <iostream>
    
    int main() {
        std::vector<int> v {1,2,3,4,5};
        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 3 5
    }
    

    Мини‑практика: удалить дубликаты из vector

    Классический приём: сортировка + unique + erase.

    #include <vector>
    #include <algorithm>
    #include <iostream>
    
    int main() {
        std::vector<int> v {5,1,2,1,3,2,4,3};
    
        std::sort(v.begin(), v.end());
        auto last = std::unique(v.begin(), v.end());
        v.erase(last, v.end());
    
        for (int x : v) std::cout << x << " "; // 1 2 3 4 5
    }
    

    Здесь unique возвращает итератор на первый «повтор», а erase удаляет хвост диапазона дубликатов.

    Лучшие практики

  • Предпочитайте range-based for и C++20 ranges там, где это уместно — меньше кода и ошибок.
  • Используйте const_iterator или cbegin/cend, если не изменяете элементы — так вы документируете намерение и избегаете случайных модификаций.
  • После операций, которые могут инвалидировать итераторы (вставки, удаление, изменение ёмкости), получайте их заново.
  • Для удаления по условию применяйте идиому erase-remove или цикл с использованием возвращаемого erase.
  • Заключение

    Итераторы — фундаментальный инструмент C++: с ними вы эффективно используете контейнеры и алгоритмы STL, пишете менее связанный и более безопасный код. Освойте базовые паттерны (erase-remove, const_iterator), не забывайте о правилах инвалидации и постепенно переходите к C++20 ranges — это сделает ваш код короче и понятнее.

    Хотите системно прокачать язык и научиться писать производительный и современный C++‑код? Рекомендую пройти практический курс: Прокачать C++ с нуля до уровня уверенного разработчика — перейти к курсу.

    Источник

    НЕТ КОММЕНТАРИЕВ

    Оставить комментарий