一、迭代器到底是什么?它的核心作用又是什么?
迭代器是STL中最重要的抽象之一。它不是指针,而是一个对象(通常是类模板实例),其设计目标是:
提供一种统一的、泛型的遍历方式了,让算法与容器解耦。
我们来看看迭代器的核心作用,也就是为什么需要迭代器:
-
统一访问接口
不同容器内部实现差异巨大,比如:std::vector 是连续内存数组;std::list 是双向链表;std::set 是红黑树。
如果没有迭代器,开发者必须为每种容器写不同的遍历代码。迭代器把"如何遍历"封装起来,所有容器都提供begin()、end()、rbegin()等接口,返回对应迭代器。
-
支持泛型算法
STL 算法(如 std::find、std::sort、std::copy、std::for_each)全部基于迭代器工作。只要传入满足要求的迭代器,就能对任意容器生效。这就是"算法与容器分离"的设计哲学。
-
封装与安全性
用户无需关心容器内部结构(节点指针、内存布局等),只需通过 *it、++it 等操作访问元素。迭代器还能在 Debug 模式下进行边界检查。
-
高效性
迭代器是针对具体容器优化的(如 vector 的随机访问迭代器本质上就是指针),性能与手写循环几乎无差别。
-
扩展性
自定义容器只需实现迭代器,就能无缝接入 STL 算法。
总的来说,迭代器是容器与算法之间的桥梁,实现了"写一次算法,适用于所有容器"。
二、迭代器的分类
1.迭代器分类不是并列的,而是严格的继承/精炼关系。能力强的可以完全替代能力弱的。
想象你在"逛超市买东西":
- Input Iterator(输入迭代器):
你拿着购物篮,只往前走,边走边看货架上的东西(只能读),看完就不能回头了(单趟遍历)。
典型:从文件/网络流(std::cin、istream_iterator)读数据。
记忆口诀:只能进,不能退;只能看,不能改;只能走一次。 - Output Iterator(输出迭代器):
你拿着笔,只往前走,边走边往篮子里写东西(只能写),不能回头看之前写了什么。
典型:往文件/控制台写(ostream_iterator、back_inserter)。
记忆口诀:只写不读,只进不退,单趟。
注意:Input 和 Output 经常一起出现,但它们是"平行"的(一个读、一个写)。 - Forward Iterator(前向迭代器):
Input + Output 的结合体,但可以多趟遍历(可以把迭代器复制一份,回头再走一遍)。
像在一条单行道上可以来回走,但不能后退。
典型:std::forward_list、unordered_set(无序容器)。
记忆口诀:能多趟读写,只能往前(Forward = 前向)。 - Bidirectional Iterator(双向迭代器):
Forward + 可以后退(--it)。
像在一条双行道上,可以往前走也可以往后退。
典型:std::list、std::set、std::map(有序关联容器)。
记忆口诀:双向 = Bidirectional = 可以 ++ 和 --。 - Random Access Iterator(随机访问迭代器):
Bidirectional + 可以跳跃(it + 5、it[3]、it < other 等)。
像在超市里可以直接跑到第 10 个货架,完全随机位置。
典型:std::vector、std::deque、std::string、std::array。
记忆口诀:Random = 像数组指针一样随意跳(支持算术运算)。 - Contiguous Iterator(连续迭代器,C++20 新增):
Random Access + 内存物理连续(元素一个挨着一个,没有空隙)。
这允许算法做更激进的优化(比如用 memcpy)。
典型:std::vector、std::array、std::string(std::deque 是 Random 但不是 Contiguous)。
记忆口诀:Contiguous = 连续内存,像 C 数组一样(C++20 才正式概念化)。
层次记忆:
Input/Output(单趟) -> Forward(多趟前向) -> Bidirectional(双向) -> Random Access(随机跳) -> Contiguous(连续内存)
从弱到强,每一级都包含上一级的所有能力。你只要记住这条升级链,就能快速推导出支持哪些操作。
2. 更简单的记忆方式
只能 ++:Input / Output / Forward
还能 --:Bidirectional
还能 +n / -n / [n] / < >:Random Access
还能保证内存连续:Contiguous
实际开发中:
大多数算法只需要 Forward 或更弱的就够了;std::sort、std::binary_search 等需要 Random Access;std::copy 等在 Contiguous 上会特别快。
3. 常见容器对应快速记忆表
| 容器类型 | 迭代器类别 | 记忆点 |
|---|---|---|
| vector / array / string | Random Access + Contiguous | 像裸数组,最强 |
| deque | Random Access(非 Contiguous) | 能随机,但内存分块 |
| list / set / map | Bidirectional | 双向链表/树,能前后走 |
| forward_list / unordered_* | Forward | 单向,只能往前 |
| istream / ostream | Input / Output | 流,单趟读写 |
三、迭代器什么时候会失效
先了解迭代器失效的定义:迭代器不再指向合法元素,或指向的内存已被释放/移动,继续使用会导致未定义行为,可能崩溃、数据错乱、段错误等。
失效的根本原因:容器内部存储结构发生改变(如内存重分配、元素移动、节点删除等)。
C++ 将迭代器按能力从弱到强分为 5 类(C++20 增加第 6 类),能力越强,支持的操作越多(这里不展开说明)
按容器分类,来讲讲不同容器失效规则:
- std::vector / std::deque / std::string(最容易失效)
- reallocation(重新内存分配)时,所有迭代器、指针、引用全部失效:
(1)push_back、insert、emplace导致容量不足时;
(2)reserve、resize(扩大);
(3)clear后resize或insert。 - erase/insert时:
(1)被擦除/插入位置及之后的所有迭代器失效;
(2)插入点之前(vector)的迭代器可能保持有效。 - 其他:pop_back 仅使指向被弹出元素的迭代器有效。
-
std::list / std::forward_list
insert、erase、splice仅使指向被操作元素的迭代器失效,其他所有迭代器始终保持有效(这是链表的最大优势)。
-
关联容器(std::set / std::map / multiset / multimap)
C++11 起:erase(it)仅使指向被擦除元素的迭代器失效,其他迭代器保持有效;insert绝不使任何现有迭代器失效(红黑树节点插入不移动其他节点)。
-
无序容器(std::unordered_set / unordered_map 等)
insert 可能触发 rehash,导致所有迭代器失效;erase 仅使指向被擦除元素的迭代器失效。
-
std::array
因为std::array(静态数组)是固定大小几乎永不失效(除非整个容器被销毁)。
示例(vector失效):
cpp
std::vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin() + 2; // 指向 3
v.insert(v.begin(), 0); // 可能 reallocate -> it 失效!
*it; // 未定义行为,危险!
应怎样避免迭代器失效:
- C++11 起推荐写法:auto it = v.erase(it);(erase 返回下一个有效迭代器)。
- 使用范围 for 循环或 std::erase_if(C++20)。
- 需要多次修改时,优先选择 std::list 或先收集要删除的元素再统一删除。
- Debug 模式下开启迭代器检查.
四、C++迭代器与指针的区别
讲解区别之前,先来看看两者的相似点:
都支持 * it、it->member、++it、it + n(随机访问时)等语法。
数组的指针 int* 天然就是 Random Access Iterator。
我用表格来对比两者的区别:
| 迭代器 | 指针 | |
|---|---|---|
| 本质 | 类对象(可能封装了节点指针、偏移等) | 裸内存地址(T*) |
| 适用范围 | 特定容器(vector、list、set 等) | 任意内存(数组、堆、栈) |
| 操作能力 | 取决于类别(Forward 迭代器不支持 --) | 永远是 Random Access(p + 5、p[3]) |
| 安全性 | Debug 模式有边界检查;不支持空指针语义 | 无任何检查;nullptr 合法但危险 |
| 失效机制 | 容器特定规则(reallocation、erase 等) | delete、realloc、vector reallocate 后失效 |
| 算术运算 | 仅 Random Access 支持 it + n、it - it2 | 任意指针都支持(但跨对象 UB) |
| 类型系统 | 支持 std::iterator_traits、std::next 等 | 普通 T*,无 traits 支持 |
| 典型用途 | 遍历 STL 容器、泛型算法 | 底层内存操作、C 风格数组 |
总结来说:
指针是迭代器的一种特例,但迭代器不一定是指针(list 的迭代器是指向节点的指针 + 封装);
迭代器更"智能"、更安全、更泛型;指针更"原始"、更危险、更底层;
所以永远不要把迭代器当成指针做危险操作。
cpp
// 迭代器
std::vector<int>::iterator it = v.begin(); // 可能就是 int*
// list 的迭代器
std::list<int>::iterator lit; // 内部是 _List_node*,不是连续内存
五、一些注意事项
- 现代 C++ 优先:范围 for + 结构化绑定 > 手动迭代器。
- 保存迭代器时:只在单次循环内保存,修改容器前重新获取。
- const 正确性:能用 const_iterator 就不要用普通迭代器。
- C++20 增强:std::ranges 库让迭代器使用更安全(std::ranges::find 等)。
- 常见错误:
循环中边遍历边 erase 不更新迭代器;
保存 end() 迭代器后修改容器;
对 unordered_* 容器假设迭代器永远有效。