迭代器基础:从输入到随机访问,再到失效与反向
迭代器,说白了就是 STL 的「万能胶水」。它把容器和算法粘在一起。我刚开始学 STL 时,总觉得迭代器很抽象——不就是个指针吗?后来踩过坑才明白,迭代器的分类体系,才是 STL 设计的精髓。
迭代器的五种分类
STL 把迭代器分成五类,每一类能做的事情不一样。你可以把它们想象成不同级别的「通行证」。
| 类别 | 能力 | 典型容器 |
|---|---|---|
| 输入迭代器 | 只读,单向,一次遍历 | istream_iterator |
| 输出迭代器 | 只写,单向,一次遍历 | ostream_iterator |
| 前向迭代器 | 读写,单向,可多次遍历 | forward_list |
| 双向迭代器 | 读写,双向 | list, set, map |
| 随机访问迭代器 | 读写,双向,支持跳跃 | vector, deque, array |
我个人习惯把随机访问迭代器叫做「全能选手」。它支持 it + n、it - n、it[n],甚至 it1 - it2 算距离。而输入迭代器只能 ++it 和 *it,连 --it 都不行。
核心原则:算法对迭代器类型的要求,只高不低。比如 sort() 要求随机访问迭代器,你拿 list 的双向迭代器去用,编译器直接报错。
输入/输出迭代器:最朴素的读写
输入迭代器,你想想看,就像从键盘读数据——只能往前读,不能回头。输出迭代器则像往屏幕写数据——写出去就没了。
#include <iostream>
#include <iterator>
#include <vector>
#include <algorithm>
int main() {
// 输入迭代器:从标准输入读整数
std::istream_iterator<int> in_iter(std::cin);
std::istream_iterator<int> eof; // 结束标记
// 输出迭代器:写到标准输出,用空格分隔
std::ostream_iterator<int> out_iter(std::cout, " ");
// 把输入读到 vector 里
std::vector<int> vec(in_iter, eof);
// 排序后输出
std::sort(vec.begin(), vec.end());
std::copy(vec.begin(), vec.end(), out_iter);
return 0;
}
我在项目中遇到过用 istream_iterator 读配置文件的情况。当时觉得挺酷,一行代码就把文件里的数字全读进来了。但要注意——输入迭代器只能遍历一次,你不能把 in_iter 保存起来后面再用。
前向迭代器与双向迭代器
前向迭代器比输入/输出迭代器强在哪?它可以多次遍历同一个序列。你想想看,输入迭代器读完一个元素就「废」了,前向迭代器可以反复读。
双向迭代器就更灵活了,能 ++ 也能 --。我早期写代码时,用 std::list 存了一堆任务,需要在中间插入和删除。双向迭代器让我能从容地前后移动。
小技巧:如果你只需要单向遍历,用前向迭代器就够了。别为了「万一以后需要反向」而选择双向迭代器——那通常意味着你选错了容器。
随机访问迭代器:性能之王
随机访问迭代器支持 it + 5 这种跳跃操作。为什么 vector 的 sort() 比 list 快那么多?就是因为随机访问迭代器能直接跳到中间位置,而双向迭代器只能一步一步挪。
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6};
// 随机访问迭代器支持指针算术
auto mid = vec.begin() + vec.size() / 2;
// 可以比较大小
if (vec.begin() < mid) {
// 前半部分排序
std::sort(vec.begin(), mid);
}
// 可以计算距离
auto dist = std::distance(vec.begin(), vec.end());
// 等价于 vec.end() - vec.begin()
return 0;
}
嗯,这里要注意:std::distance() 对随机访问迭代器是 O(1) 的,但对双向迭代器是 O(n) 的。我见过有人对 list 用 distance(),结果性能崩了。
迭代器失效问题:避坑指南
迭代器失效,是 C++ 新手最容易踩的坑之一。说白了就是:你拿着一个迭代器,指向容器里的某个元素,然后你修改了容器,这个迭代器就「废」了。
| 操作 | vector | list | deque |
|---|---|---|---|
| 插入 | 所有迭代器失效 | 仅当前迭代器有效 | 所有迭代器失效 |
| 删除 | 删除点之后全失效 | 仅被删元素失效 | 所有迭代器失效 |
| push_back | 可能全部失效(扩容) | 不影响 | 不影响 |
我曾经在项目中犯过一个经典错误:在遍历 vector 时删除元素,结果迭代器失效导致崩溃。正确的做法是用 erase() 返回的迭代器继续遍历。
// 错误示范:迭代器失效
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it % 2 == 0) {
vec.erase(it); // 崩溃!it 失效了
}
}
// 正确做法
for (auto it = vec.begin(); it != vec.end(); ) {
if (*it % 2 == 0) {
it = vec.erase(it); // erase 返回下一个有效迭代器
} else {
++it;
}
}
反向迭代器:从尾到头
反向迭代器,说白了就是把正向迭代器「倒过来用」。它用 rbegin() 和 rend() 获取,内部其实存的是正向迭代器。
我记得有一次要按时间倒序显示日志。用反向迭代器一行代码搞定:
#include <vector>
#include <iostream>
int main() {
std::vector<int> logs = {1, 2, 3, 4, 5};
// 反向遍历
for (auto rit = logs.rbegin(); rit != logs.rend(); ++rit) {
std::cout << *rit << " "; // 输出:5 4 3 2 1
}
// 注意:反向迭代器的 base() 返回对应的正向迭代器
auto rit = logs.rbegin();
auto it = rit.base(); // it 指向 logs[4] 的下一个位置
// 也就是说,rit 和 it 指向的元素是相邻的
return 0;
}
小细节:rbegin() 对应 end(),rend() 对应 begin()。这个对应关系有点绕,我建议你画个图理解一下。简单记:反向迭代器的 base() 总是指向正向的下一个位置。
知识体系总览
下面这张图把迭代器的分类、能力、典型容器串起来了。你可以把它当作速查表。
迭代器这东西,刚开始觉得抽象,用多了就发现它其实很自然。记住一条:能用低级别迭代器解决的问题,就别用高级别的。这既是性能考虑,也是接口设计的原则。
总结一下:迭代器是 STL 的骨架。理解分类体系,你就理解了为什么有些算法不能用在某些容器上。迭代器失效是运行时的大坑,写代码时多留个心眼。反向迭代器是个小工具,用好了能让代码更简洁。
公众号:蓝海资料掘金营,微信deep3321