迭代器模式:模式动机与定义、C++ STL迭代器剖析、自定义迭代器实现

为什么需要迭代器?

先问大家一个问题:你写代码的时候,有没有遇到过这种情况?

你有一个 std::vector<int>,想遍历它。过两天需求变了,容器换成了 std::list<int>。好,你开始改代码——遍历的索引 [] 操作符不能用了,得换成 begin()/end()++ 的方式。再过两周,又换成了 std::set<int>,遍历逻辑又得改一遍。

烦不烦?

我个人习惯是,从一开始就用迭代器。为什么?因为迭代器把「怎么遍历」和「遍历什么」分开了。你想想看,不管底层是数组、链表还是红黑树,迭代器的用法几乎一模一样。这就是迭代器模式的核心思想。

迭代器模式的定义:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。

说白了,就是让遍历逻辑和容器解耦。我在项目中遇到过好几次这样的场景:业务逻辑里写死了 for (int i = 0; i < vec.size(); ++i),后来要改成并发遍历,或者要支持过滤、映射等操作,改起来简直想哭。如果一开始就用迭代器,这些扩展会轻松很多。

C++ STL 迭代器剖析

STL 里的迭代器,其实就是一个「智能指针」的抽象。它支持 ++*==!= 这些操作,让你像操作指针一样操作容器里的元素。

STL 把迭代器分成了五类,我整理了一张表:

类别 支持的操作 典型容器
输入迭代器 ++*(只读)、==!= istream_iterator
输出迭代器 ++*(只写) ostream_iterator
前向迭代器 输入 + 输出 + 多次遍历 forward_list
双向迭代器 前向 + -- list、set、map
随机访问迭代器 双向 + []+=-=<> vector、deque、array

嗯,这里要注意:随机访问迭代器是最强大的,但不是所有容器都支持。比如 std::list 只支持双向迭代器,你不能对它做 it + 5 这种操作。我曾经在代码里写过 std::advance(it, 5),结果没注意迭代器类型,编译报错找了半天——后来养成了习惯,写泛型代码时一定用 std::iterator_traits 来检查迭代器类别。

迭代器适配器:让遍历更灵活

STL 还提供了一些迭代器适配器,我挑几个常用的说说:

  • 反向迭代器rbegin() / rend(),从尾到头遍历。我经常用它来做「最近使用」列表的展示。
  • 插入迭代器back_inserterfront_inserterinserter。配合 std::copy 用,简直不要太爽。
  • 流迭代器istream_iteratorostream_iterator。从标准输入读数据,或者把数据写到标准输出,一行代码搞定。

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

std::vector<int> data;
std::copy(
    std::istream_iterator<int>(std::cin),
    std::istream_iterator<int>(),
    std::back_inserter(data)
);

你看,三行代码就搞定了。不用写循环,不用管边界。这就是迭代器的魅力。

自定义迭代器实现

有时候 STL 提供的迭代器不够用,你得自己写。比如你有一个自定义的容器,或者你想实现一个特殊的遍历逻辑。

我给大家展示一个简单的例子:实现一个「步进迭代器」,每次跳 step 个元素。

template<typename Iterator>
class StepIterator {
public:
    using iterator_category = std::forward_iterator_tag;
    using value_type = typename std::iterator_traits<Iterator>::value_type;
    using difference_type = typename std::iterator_traits<Iterator>::difference_type;
    using pointer = typename std::iterator_traits<Iterator>::pointer;
    using reference = typename std::iterator_traits<Iterator>::reference;

    StepIterator(Iterator it, difference_type step)
        : it_(it), step_(step) {}

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

    StepIterator& operator++() {
        std::advance(it_, step_);
        return *this;
    }

    StepIterator operator++(int) {
        StepIterator tmp = *this;
        ++(*this);
        return tmp;
    }

    bool operator==(const StepIterator& other) const {
        return it_ == other.it_;
    }

    bool operator!=(const StepIterator& other) const {
        return !(*this == other);
    }

private:
    Iterator it_;
    difference_type step_;
};

使用起来也很简单:

std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
StepIterator start(vec.begin(), 3);
StepIterator end(vec.end(), 3);

for (auto it = start; it != end; ++it) {
    std::cout << *it << " ";  // 输出: 1 4 7 10
}

小提示:自定义迭代器时,一定要正确声明 iterator_category。这会影响 STL 算法对迭代器的优化。比如你声明为 random_access_iterator_tag,但实际不支持随机访问,那 std::sort 用上去就会出问题。

迭代器失效:一个常见的坑

警告:迭代器失效是 C++ 里最容易踩的坑之一。我曾经在项目里因为没注意迭代器失效,导致了一个隐蔽的内存越界 bug,查了整整两天。

常见的失效场景:

  • vector 扩容:所有迭代器、指针、引用全部失效。
  • deque 中间插入/删除:所有迭代器失效。
  • list 插入/删除:只有被删除元素的迭代器失效,其他不受影响。
  • map/set 插入/删除:只有被删除元素的迭代器失效。

我的建议是:如果你要在遍历过程中修改容器,尽量用 erase 返回的迭代器:

// 正确做法
for (auto it = vec.begin(); it != vec.end(); ) {
    if (*it % 2 == 0) {
        it = vec.erase(it);  // erase 返回下一个有效迭代器
    } else {
        ++it;
    }
}

迭代器模式的核心逻辑

最后,我用一张图来总结迭代器模式的核心结构:

迭代器模式核心结构 客户端 Aggregate + CreateIterator() ConcreteAggregate + CreateIterator() Iterator + First() / Next() / IsDone() ConcreteIterator + First() / Next() / IsDone() 使用 使用 继承 继承 关联

这张图展示了迭代器模式的四个核心角色:客户端、聚合接口、具体聚合、迭代器接口、具体迭代器。客户端通过迭代器接口来遍历聚合对象,而不需要知道聚合对象的具体类型。这就是「封装变化」的精髓。

好了,关于迭代器模式就聊到这里。记住一句话:迭代器是连接算法和容器的桥梁。用好它,你的代码会变得更灵活、更可维护。


公众号:蓝海资料掘金营,微信deep3321