迭代器模式:遍历聚合对象,C++ STL迭代器原理,自定义迭代器

迭代器模式,说白了就是提供一种统一的方式去遍历容器里的元素,而不暴露容器的内部结构。你想想看,我们写C++的时候,用vector、list、map,遍历方式各不相同。但有了迭代器,一切就变得整齐划一了。

我个人习惯把迭代器想象成「一个会走路的指针」。它知道当前指向哪个元素,也知道怎么走到下一个。这种抽象,让算法和容器彻底解耦——这也是STL设计的核心思想。

为什么需要迭代器模式?

假设你写了一个自定义的集合类,比如一个二叉树。如果没有迭代器,用户想遍历你的树,就得知道你的节点结构、遍历顺序。这太糟糕了。迭代器模式就是来解决这个问题的。

我在项目中遇到过这样一个场景:一个老同事写了一个自定义的图结构,遍历的时候要求调用方传入一个回调函数。后来需求变了,需要支持多种遍历顺序(BFS、DFS),回调函数里塞满了状态判断,代码变得一塌糊涂。我当时建议改成迭代器模式,每个迭代器只负责一种遍历策略,调用方用起来清爽多了。

核心思想:迭代器模式将「遍历行为」从「聚合对象」中分离出来。聚合对象只负责存储数据,迭代器负责访问数据。

C++ STL迭代器原理

STL的迭代器设计得非常精巧。它不是一个具体的类,而是一组概念(concept)。不同的容器提供不同能力的迭代器,但都遵循相同的接口约定。

STL迭代器分为五类,能力从弱到强:

迭代器类别 支持的操作 典型容器
输入迭代器 只读,单向,一次遍历 istream_iterator
输出迭代器 只写,单向,一次遍历 ostream_iterator
前向迭代器 读写,单向,可多次遍历 forward_list
双向迭代器 读写,可前可后 list, set, map
随机访问迭代器 读写,支持跳跃、下标、算术运算 vector, deque, array

为什么要有这么多分类?因为不同的容器底层结构不同,能提供的操作也不同。比如链表没法直接跳转到第N个元素,所以list的迭代器不支持 it + 5 这种操作。STL通过迭代器分类,在编译期就限制了不合理的操作。

我的经验:写泛型算法时,尽量使用最弱的迭代器要求。比如一个算法只需要前向迭代器,就不要声明成随机访问迭代器。这样你的算法能用在更多容器上。

自定义迭代器:从零开始写一个

嗯,这里要注意。自定义迭代器在C++里其实有一套固定的「套路」。你只需要按照STL的约定,实现几个必要的操作符就行了。

我以一个简单的动态数组为例,写一个只读的前向迭代器:

template<typename T>
class MyArray {
public:
    // 迭代器类型定义
    class Iterator {
    public:
        using iterator_category = std::forward_iterator_tag;
        using value_type = T;
        using difference_type = std::ptrdiff_t;
        using pointer = const T*;
        using reference = const T&;

        Iterator(pointer ptr) : m_ptr(ptr) {}

        reference operator*() const { return *m_ptr; }
        pointer operator->() const { return m_ptr; }

        // 前置++
        Iterator& operator++() {
            ++m_ptr;
            return *this;
        }
        // 后置++
        Iterator operator++(int) {
            Iterator tmp = *this;
            ++(*this);
            return tmp;
        }

        bool operator==(const Iterator& other) const {
            return m_ptr == other.m_ptr;
        }
        bool operator!=(const Iterator& other) const {
            return !(*this == other);
        }

    private:
        pointer m_ptr;
    };

    Iterator begin() const { return Iterator(m_data); }
    Iterator end() const { return Iterator(m_data + m_size); }

    // ... 构造、析构、增删等省略
private:
    T* m_data;
    size_t m_size;
};

你看,核心就是那几个操作符:*++==!=。再加上类型别名,告诉STL这个迭代器的能力等级。

避坑指南:我曾经在自定义迭代器时忘了定义 iterator_category,结果std::advance这种算法直接编译报错。STL内部会根据这个标签选择最优的实现。没有它,编译器就不知道你的迭代器能做什么。

迭代器适配器:站在巨人的肩膀上

STL还提供了一些迭代器适配器,它们不直接遍历容器,而是对遍历行为进行「加工」。

  • 反向迭代器rbegin() / rend(),从尾到头遍历。底层其实就是把 ++ 映射成 --
  • 插入迭代器back_inserterfront_inserterinserter。赋值操作变成插入操作,非常适合配合 std::copy 使用。
  • 流迭代器istream_iteratorostream_iterator。把输入输出流当成容器来遍历。

举个例子,从标准输入读取整数,存入vector:

std::vector<int> vec;
std::copy(
    std::istream_iterator<int>(std::cin),
    std::istream_iterator<int>(),  // 默认构造表示流结束
    std::back_inserter(vec)
);

一行代码搞定。这就是迭代器模式的威力——算法和容器完全解耦,中间通过迭代器这座桥连接。

迭代器失效:一个必须警惕的问题

迭代器虽好,但有个大坑:迭代器失效。当你修改容器结构时(插入、删除、扩容),已有的迭代器可能变得无效。

我曾经在线上排查过一个bug,症状是程序偶尔崩溃,而且只在数据量大的时候出现。查了两天才发现,是有人在遍历vector时插入了新元素,导致迭代器指向了被重新分配的内存。嗯,从那以后,我写代码时对迭代器生命周期格外小心。

不同容器的失效规则不同:

  • vector:插入或删除可能导致所有迭代器失效(因为可能重新分配内存)。
  • list:插入不会使已有迭代器失效,删除只会使被删除元素的迭代器失效。
  • map/set:插入不会使已有迭代器失效,删除只会使被删除元素的迭代器失效。
安全做法:如果你需要在遍历时修改容器,考虑使用 erase 的返回值来更新迭代器,或者先把要操作的元素记录下来,遍历完再统一处理。

迭代器模式的知识体系

下面这张图总结了迭代器模式的核心脉络:

迭代器模式知识体系 迭代器模式 设计意图 分离遍历行为与聚合对象 统一接口,隐藏内部结构 支持多种遍历策略 STL实现 五类迭代器(输入/输出/前向/双向/随机) 迭代器适配器(反向/插入/流) 迭代器失效规则 标签派发(iterator_category) 自定义实践 实现必要操作符(* ++ == !=) 定义类型别名(category/value_type等) begin() / end() 接口 注意迭代器生命周期管理 核心:统一遍历接口,解耦算法与容器

迭代器模式在C++里不是「要不要用」的问题,而是「怎么用好」的问题。STL已经把它融入了语言的血脉。你写的每一个 std::sort、每一个 std::find,背后都是迭代器在默默工作。

最后说一句:如果你要写自己的容器,一定要提供迭代器。这不仅是为了STL算法的兼容,更是对使用者的一种尊重。好的接口设计,就是让别人用起来觉得「本该如此」。


专注资料整理