3. 迭代器入门:迭代器设计思想、迭代器分类与失效问题
迭代器,说白了就是 STL 里连接算法和容器的“桥梁”。我刚开始学 C++ 那会儿,总觉得迭代器不就是个指针吗?后来在项目里写了一个通用排序函数,才发现没有迭代器,代码根本没法写得这么优雅。
这一节,咱们就聊聊迭代器的设计思想、五种分类,以及那个让无数人头疼的——迭代器失效问题。
3.1 迭代器的设计思想:为什么需要它?
你想想看,如果没有迭代器,你要遍历一个 vector 可以用下标,遍历一个 list 就得用节点指针,遍历一个 map 又得用键值对。每种容器都有自己的遍历方式,那算法怎么写?难道每种容器都写一套?
迭代器就是为了解决这个问题的。它定义了一套统一的接口:++ 往前走,* 取元素,== 判相等。不管底层是连续内存还是链表,算法只跟迭代器打交道。
核心思想:迭代器将“访问容器元素”这个行为抽象出来,让算法与容器解耦。
我在项目中遇到过这样一个场景:需要把一段数据从 vector 拷贝到 list 里。如果没有迭代器,你得写两个循环。但有了迭代器,一行 copy(v.begin(), v.end(), lst.begin()) 就搞定了。这就是抽象的力量。
3.2 迭代器分类:五种能力等级
STL 把迭代器分成了五类,按能力从弱到强排列。我习惯把它们想象成“交通工具”——从步行到高铁,能力越强,能做的事越多。
| 分类 | 能力 | 典型容器 | 支持的操作 |
|---|---|---|---|
| 输入迭代器 | 只读,单向,一次遍历 | istream_iterator | ++, *, ==, != |
| 输出迭代器 | 只写,单向,一次遍历 | ostream_iterator | ++, * = |
| 前向迭代器 | 读写,单向,可多次遍历 | forward_list | 输入+输出+可保存副本 |
| 双向迭代器 | 读写,双向 | list, set, map | 前向 + -- |
| 随机访问迭代器 | 读写,任意跳转 | vector, deque, array | 双向 + +n, -n, [], < |
嗯,这里要注意:输入和输出迭代器是“一次性的”。你读完一个元素,再想读一遍?不行,它已经过去了。这就像从键盘读数据,你不能把敲过的键再敲一遍。
我曾经在写一个日志解析器时,用了 istream_iterator 从文件流里读数据。一开始没注意它是输入迭代器,想用 std::sort 排序——结果编译报错。后来才意识到,排序需要随机访问迭代器,输入迭代器根本不够格。
避坑指南:写模板函数时,尽量用最弱的迭代器要求。比如只读遍历就用输入迭代器,别一上来就写随机访问。这样你的算法才能用在更多容器上。
3.3 迭代器分类的层级关系
这五类迭代器其实是一个“能力金字塔”。随机访问迭代器拥有所有下层迭代器的能力,双向迭代器拥有前向的能力,以此类推。
下面这张图可以帮你理清关系:
从图中可以看出,前向迭代器同时继承了输入和输出迭代器的能力。所以,如果你写一个算法只需要输入迭代器,那它也能用在随机访问迭代器上——这就是“能力向下兼容”。
3.4 迭代器失效问题:一个老生常谈的坑
迭代器失效,说白了就是:你手里拿着的迭代器,指向的元素已经“不是原来的它”了。可能是被删了,可能是内存重新分配了,也可能是元素位置变了。
我刚开始写 C++ 时,就因为这个 bug 调试了整整一个下午。当时在遍历一个 vector 时往里插入元素,结果程序直接崩溃。后来才知道,vector 的插入操作会导致所有迭代器失效。
3.4.1 各容器的失效情况
| 容器 | 插入操作 | 删除操作 | 重新分配 |
|---|---|---|---|
| vector / string | 插入点之后全失效 | 删除点之后全失效 | 全部失效 |
| deque | 全部失效(除首尾) | 删除点之后全失效 | 全部失效 |
| list / forward_list | 仅当前迭代器失效 | 仅被删元素迭代器失效 | 无重新分配 |
| set / map / unordered_* | 仅当前迭代器失效 | 仅被删元素迭代器失效 | 无重新分配 |
注意:对于 vector,只要插入或删除导致容量变化(重新分配内存),所有迭代器、指针、引用都会失效。即使没有重新分配,插入/删除点之后的迭代器也会失效。
3.4.2 正确的遍历删除写法
我曾经在代码审查时看到过这样的写法:
// ❌ 错误写法:删除后迭代器失效
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == 0) {
v.erase(it); // it 失效了,但后面还在 ++it
}
}
正确的做法是利用 erase 的返回值:
// ✅ 正确写法:利用 erase 返回下一个有效迭代器
for (auto it = v.begin(); it != v.end(); ) {
if (*it == 0) {
it = v.erase(it); // erase 返回下一个元素的位置
} else {
++it;
}
}
对于 list 和 map,erase 只会让被删元素的迭代器失效,其他迭代器依然有效。但为了代码的一致性,我建议统一使用上面的模式。
个人习惯:如果需要在遍历时频繁删除,我一般优先考虑 list 或 map,而不是 vector。因为 vector 的删除操作会移动大量元素,性能开销大,而且迭代器失效范围广。
3.5 迭代器适配器:让迭代器更强大
STL 还提供了一些迭代器适配器,它们本质上是“包装”了普通迭代器,赋予新的行为。这里简单提几个常用的:
- 反向迭代器:
rbegin()/rend(),让遍历方向反过来。我经常用它来实现“从后往前查找”。 - 插入迭代器:
back_inserter、front_inserter、inserter,赋值时自动插入元素。配合copy算法特别好用。 - 流迭代器:
istream_iterator、ostream_iterator,把输入输出流当成容器来遍历。
举个例子,从标准输入读整数,存入 vector:
#include <iostream>
#include <iterator>
#include <vector>
int main() {
std::vector<int> v;
std::copy(
std::istream_iterator<int>(std::cin),
std::istream_iterator<int>(), // 默认构造表示流结束
std::back_inserter(v)
);
// 现在 v 里就是所有输入的整数
return 0;
}
这段代码看起来有点“魔法”,但拆开看其实很简单:istream_iterator 从 cin 读整数,back_inserter 把每个整数 push_back 到 vector 里。一行代码搞定输入和存储。
3.6 小结
迭代器是 STL 的“胶水”,把容器和算法粘在一起。理解迭代器的分类,能帮你写出更通用的代码。而迭代器失效问题,是每个 C++ 开发者都会遇到的坑——记住一点:修改容器后,之前拿到的迭代器可能就“不靠谱”了。
我个人建议,刚开始接触 STL 时,多写写 vector 和 list 的遍历代码,感受一下迭代器的行为差异。等你习惯了迭代器的思维方式,再看 STL 算法,就会觉得豁然开朗。