迭代器模式:模式动机与定义
迭代器模式,说白了就是提供一种统一的方式来遍历容器里的元素。你想想看,数组有数组的遍历方式,链表有链表的遍历方式,树有树的遍历方式——如果每次都要自己写循环逻辑,那代码得多乱?
我刚开始做C++开发那会儿,就踩过这个坑。当时项目里用了好几种自定义容器,每种容器的遍历方式都不一样。有的用下标,有的用指针偏移,有的还得手动维护一个状态变量。每次换容器,遍历代码就得重写一遍。后来我意识到,这其实就是个设计问题——我们需要一个统一的遍历接口。
迭代器模式的核心动机就这么简单:将遍历行为与容器实现解耦。让客户端代码不关心底层容器是数组还是链表,只管调用统一的next()、hasNext()接口就行。
定义:迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。
角色与结构
迭代器模式涉及这么几个角色,我画了个图帮你理清关系:
结构其实不复杂,就四个角色:
- 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就是把这个思想发挥到极致的典范。