迭代器模式:模式动机与定义

迭代器模式,说白了就是提供一种统一的方式来遍历容器里的元素。你想想看,数组有数组的遍历方式,链表有链表的遍历方式,树有树的遍历方式——如果每次都要自己写循环逻辑,那代码得多乱?

我刚开始做C++开发那会儿,就踩过这个坑。当时项目里用了好几种自定义容器,每种容器的遍历方式都不一样。有的用下标,有的用指针偏移,有的还得手动维护一个状态变量。每次换容器,遍历代码就得重写一遍。后来我意识到,这其实就是个设计问题——我们需要一个统一的遍历接口。

迭代器模式的核心动机就这么简单:将遍历行为与容器实现解耦。让客户端代码不关心底层容器是数组还是链表,只管调用统一的next()、hasNext()接口就行。

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

角色与结构

迭代器模式涉及这么几个角色,我画了个图帮你理清关系:

迭代器模式结构图 «interface» Iterator + hasNext(): bool + next(): Element ConcreteIterator - cursor: int + hasNext(): bool + next(): Element «interface» Aggregate + createIterator(): Iterator ConcreteAggregate - data: Element[] + createIterator(): Iterator 创建 Client 接口 具体实现 依赖/创建

结构其实不复杂,就四个角色:

  • Iterator(迭代器接口):定义hasNext()、next()等遍历方法。这是所有迭代器的统一契约。
  • ConcreteIterator(具体迭代器):实现迭代器接口,维护遍历时的当前位置。比如数组迭代器内部就存个下标。
  • Aggregate(聚合接口):定义createIterator()方法,返回一个迭代器对象。
  • ConcreteAggregate(具体聚合):实现聚合接口,返回对应的具体迭代器实例。

嗯,这里要注意:客户端只跟Iterator和Aggregate接口打交道,不关心具体实现。这就是依赖倒置原则的体现。

C++代码实现

咱们直接上代码。我习惯用模板来实现,这样迭代器可以适配任意元素类型。

// 迭代器接口
template<typename T>
class Iterator {
public:
    virtual ~Iterator() = default;
    virtual bool hasNext() const = 0;
    virtual T& next() = 0;
};

// 聚合接口
template<typename T>
class Aggregate {
public:
    virtual ~Aggregate() = default;
    virtual std::unique_ptr<Iterator<T>> createIterator() = 0;
};

// 具体聚合:数组容器
template<typename T, size_t N>
class ArrayAggregate : public Aggregate<T> {
private:
    T data[N];
    size_t size = N;
public:
    ArrayAggregate(std::initializer_list<T> list) {
        std::copy(list.begin(), list.end(), data);
    }
    
    std::unique_ptr<Iterator<T>> createIterator() override;
    
    T& operator[](size_t index) { return data[index]; }
    size_t getSize() const { return size; }
};

// 具体迭代器
template<typename T, size_t N>
class ArrayIterator : public Iterator<T> {
private:
    ArrayAggregate<T, N>& aggregate;
    size_t cursor = 0;
public:
    explicit ArrayIterator(ArrayAggregate<T, N>& agg) : aggregate(agg) {}
    
    bool hasNext() const override {
        return cursor < aggregate.getSize();
    }
    
    T& next() override {
        if (!hasNext()) {
            throw std::out_of_range("迭代器越界");
        }
        return aggregate[cursor++];
    }
};

// 实现createIterator
template<typename T, size_t N>
std::unique_ptr<Iterator<T>> ArrayAggregate<T, N>::createIterator() {
    return std::make_unique<ArrayIterator<T, N>>(*this);
}

// 客户端使用
int main() {
    ArrayAggregate<int, 5> arr = {1, 2, 3, 4, 5};
    auto it = arr.createIterator();
    
    while (it->hasNext()) {
        std::cout << it->next() << " ";
    }
    // 输出: 1 2 3 4 5
    return 0;
}

个人经验:我在项目中习惯用std::unique_ptr管理迭代器生命周期,避免裸指针带来的内存泄漏。如果你用C++17以上,可以考虑std::shared_ptr,但迭代器通常是局部的,unique_ptr更轻量。

STL迭代器原理

说到迭代器,就不得不提STL。STL的迭代器设计得相当精巧,它其实不是用「模式」的方式实现的,而是用traits(特性萃取)技术。

为什么会这样?因为STL追求的是零开销抽象。如果用虚函数实现迭代器接口,每次调用next()都有虚函数开销,这在高性能场景下是不能接受的。

STL的做法是:通过模板和迭代器traits,在编译期就确定迭代器的类型和操作。你看STL的迭代器分类:

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

我记得有一次面试,面试官问我:「vector的迭代器是什么?」我回答:「本质上就是个原生指针。」他点点头。确实,vector<T>::iterator通常就是T*的typedef,因为vector的内存是连续的,指针自增就能遍历所有元素。

但list的迭代器就不能是原生指针了,因为list节点在内存里不连续。所以list的迭代器是一个类对象,重载了operator++、operator*等操作符。

避坑指南:我曾经在项目里犯过一个错误——在遍历vector时往容器里插入元素,导致迭代器失效。STL的迭代器失效规则很复杂,简单说:vector的插入/删除会使之后的所有迭代器失效;list的插入不会使已有迭代器失效,但删除会使被删元素的迭代器失效。写代码时一定要小心。

STL迭代器的精髓在于:它用编译期多态(模板)替代了运行期多态(虚函数)。你写一个算法模板,传入不同的迭代器,编译器会生成不同的代码。这就是为什么std::sort要求随机访问迭代器,而std::find只要求输入迭代器——因为算法对迭代器能力的要求不同。

嗯,说到这儿,我想起一个实用技巧:如果你自己写容器,想让它能跟STL算法配合使用,只需要提供begin()和end()方法,返回符合要求的迭代器就行。这样你的容器就能直接用std::find、std::sort等算法了。

迭代器模式看起来简单,但用好了能让代码的扩展性提升一个档次。我个人觉得,理解迭代器模式的关键不在于记住UML图,而在于理解「遍历与容器分离」这个思想。STL就是把这个思想发挥到极致的典范。

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