迭代器模式:模式动机与定义、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_inserter、front_inserter、inserter。配合std::copy用,简直不要太爽。 - 流迭代器:
istream_iterator、ostream_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;
}
}
迭代器模式的核心逻辑
最后,我用一张图来总结迭代器模式的核心结构:
这张图展示了迭代器模式的四个核心角色:客户端、聚合接口、具体聚合、迭代器接口、具体迭代器。客户端通过迭代器接口来遍历聚合对象,而不需要知道聚合对象的具体类型。这就是「封装变化」的精髓。
好了,关于迭代器模式就聊到这里。记住一句话:迭代器是连接算法和容器的桥梁。用好它,你的代码会变得更灵活、更可维护。