我第一次用 std::sort 去排一个 std::list,编译报错报得那叫一个惨。模板错误刷了几十屏,最后 GCC 甩给我一句“no match for operator-”,我才反应过来:std::sort 要求随机访问迭代器,而 list 的迭代器只能双向移动。这件事对我触动挺大,因为在此之前,我写遍历循环从来都是 for (size_t i = 0; i < v.size(); i++),完全没意识到“遍历”这件事本身还有这么多讲究。后来系统性啃完迭代器模式,才明白它在 C++ 里根本不是一种可有可无的语法糖,而是整个 STL 泛型算法体系的基石,也是把容器、算法、适配器三类组件串起来的核心接口层。这篇文章我会从“迭代器模式到底解决了什么问题”讲起,把五种迭代器的能力阶梯、自定义迭代器的完整写法、迭代器失效的坑,以及 C++20 ranges 对迭代器体系的现代化改造一次性讲透。不管你是刚学 C++ 不久、被 STL 算法搞得一头雾水,还是写了一阵子业务代码、想搞明白怎么在自己的数据结构里也套用这套模式,这篇都应该能给你一些参考。
1. 从“算法不认 list”说起:迭代器到底解决了什么问题
1.1 std::sort 为什么拒绝 list
先回到开头的报错。std::sort 的签名要求传入的迭代器满足 RandomAccessIterator,它的实现里会做 it + n、it - it、it < it 这类操作,而 list 的迭代器只支持 ++it、--it、it != it,物理上根本没有“跳转”能力。链表的节点散落在内存各处,CPU 即使想跳也没法按偏移量定位——你要访问链表第 n 个节点,必须从前向后一步步走。
那 list 想排序怎么办?它自己实现了 std::list::sort(),底层是归并排序,只需要双向迭代器就够了。这就引出一个很关键的问题:排序算法明明有统一的抽象,为什么不能服务所有容器?
因为容器的存储结构决定了对元素进行"访问与移动"的代价。数组是连续内存,随机访问 O(1);链表是非连续内存,随机访问 O(n)。迭代器模式的精妙之处,就是把这些“存储结构差异”封装在迭代器内部,让算法只看迭代器暴露出的能力,而不关心背后到底连着数组还是链表。
1.2 迭代器模式的本质:把“怎么走”和“去哪”分离
如果让我用一句话概括迭代器模式,我会说:它把遍历逻辑从容器里抽出来,放到了一个独立的小对象里,这个小对象只对外暴露两个最基本的问题——“当前位置在哪”以及“下一个位置怎么走”。
类比生活里的场景,迭代器就是餐厅的服务员。你想吃饭(调用算法),不需要知道后厨食材放在哪个冰箱、调料摆在第几排,只需要跟服务员说“我要一份宫保鸡丁”。服务员负责从入口走到对应桌位、端菜、返回,这些遍历动作和顾客完全隔离。容器就是后厨,算法就是顾客,迭代器就是中间那个服务员。
这个“分离”带来了两个巨大的收益:
第一,算法可以和容器解耦。std::find 不关心你传进来的是 vector、list、deque 还是原生的 C 数组,只要迭代器满足“能前进、能解引用、能比较相等”这三个条件,算法就能正常工作。STL 里 100 多个算法,配合各种容器,理论上能组合出上千种用法,而不用像面向对象设计里那样为每个容器实现一套查找逻辑。
第二,遍历操作可以被组合和定制。迭代器本身是对象,所以你可以对迭代器做修饰、包装、过滤,比如逆序遍历、插入模式遍历、只读遍历、从输入流里“假装顺序遍历”……这些操作都是算法无关的,只是改变了迭代器“走法”。这就是后面要讲的迭代器适配器(iterator adapters)能玩出花样的基础。
很多人以为迭代器模式只是 STL 内部的一个实现细节,其实它是一种跨语言的通用设计模式。C# 的
IEnumerator、Java 的Iterator、Python 的__iter__/__next__,本质都是同一套思想——把集合的遍历行为抽象出来,让调用方可以统一操作。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 五类迭代器:能力阶梯决定代码边界
2.1 从 input 到 random_access,能力递增
C++ 标准把迭代器按能力划分成了五个等级。理解这套等级制度非常重要,因为每个 STL 算法都会声明它最少需要哪个等级的迭代器,等级不满足,编译直接失败——这是 C++ 在编译期就帮你拦住错误设计的手段。
我整理了一张表,列一下五种迭代器的能力和常见代表:
| 迭代器类别 | 能做什么 | 典型代表 | 可用于的算法示例 |
|---|---|---|---|
| Input(输入) | 只读、单遍扫描,++、*、== |
istream_iterator |
std::find、std::count_if |
| Output(输出) | 只写、单遍写入 | ostream_iterator、back_inserter |
std::copy 的目标 |
| Forward(前向) | 可读写、可多遍扫描 | forward_list 迭代器、unordered 容器迭代器 |
std::replace、std::unique |
| Bidirectional(双向) | 在前向基础上支持 -- |
list、set、map 迭代器 |
std::reverse、std::inplace_merge |
| Random Access(随机) | 在双向基础上支持 +n、[]、< |
vector、deque、原生指针 |
std::sort、std::binary_search |
注意这些能力是严格的包含关系:随机访问迭代器一定也是双向迭代器,双向迭代器也一定满足前向迭代器的要求。标准库通过 iterator_category 这个类型别名来标识迭代器的等级,算法在内部会根据这个标签选择不同的实现路径。
2.2 iterator_traits 与标签分发:STL 算法内部如何“看人下菜碟”
你可能好奇过,std::advance(it, n) 这个函数怎么做到对 vector 迭代器直接 it += n,对 list 迭代器却只能循环 n
