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_inserterfront_inserterinserter,赋值时自动插入元素。配合 copy 算法特别好用。
  • 流迭代器istream_iteratorostream_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 时,多写写 vectorlist 的遍历代码,感受一下迭代器的行为差异。等你习惯了迭代器的思维方式,再看 STL 算法,就会觉得豁然开朗。

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