迭代器(Iterator)是连接容器与算法的 "桥梁",它提供了一种统一的方式访问容器中的元素,而无需暴露容器的内部实现。
不同容器的底层实现差异很大(如 vector
是动态数组,list
是双向链表),但迭代器屏蔽了这些差异,让算法(如 sort
、for_each
)可以用相同的方式处理任何容器。
算法只需依赖迭代器提供的接口,无需关心容器的具体类型,实现了 "泛型编程" 的核心思想。
一、迭代器分类
迭代器类型 | 支持的操作(核心) | 对应容器示例 |
---|---|---|
输入迭代器(Input Iterator) | 只读访问(*it )、单向递增(++it )、比较相等(== /!= ) |
istream_iterator (输入流迭代器) |
输出迭代器(Output Iterator) | 只写访问(*it = value )、单向递增(++it ) |
ostream_iterator (输出流迭代器) |
前向迭代器(Forward Iterator) | 读写访问、单向递增(++it )、可重复遍历 |
unordered_map 、unordered_set |
双向迭代器(Bidirectional Iterator) | 读写访问、双向移动(++it /--it )、可双向遍历 |
list 、map 、set |
随机访问迭代器(Random Access Iterator) | 读写访问、随机访问(it + n /it - n 、it[n] )、比较大小(< /> ) |
vector 、deque 、array |
- 输入 / 输出迭代器:功能最弱,仅支持单方向遍历和基本读写(输入只读,输出只写),适用于流操作。
- 前向迭代器:支持重复遍历同一序列(可多次递增),但只能向前移动,适合单向链表类容器。
- 双向迭代器 :在向前迭代的基础上支持向后移动(
--it
),适合双向链表 、**平衡树(如红黑树)**类容器。 - 随机访问迭代器 :功能最强,支持直接跳转到任意位置(如
it += 5
),访问效率与数组下标相当,适合连续内存 容器(如vector
)。
功能最弱到最强依次为:输入迭代器 → 输出迭代器 → 前向迭代器 → 双向迭代器 → 随机访问迭代器。
1、输入迭代器(Input Iterator):只读、单向遍历
1.1 核心特性
- 功能 :仅支持 "只读访问" 和 "单向递增"(
++
),无法修改元素,且遍历过程中元素可能失效(不能重复遍历)。 - 关键操作 :
*it
(读元素)、++it
(向后移动)、it1 == it2
/it1 != it2
(比较)。 - 本质:"一次性读取工具",如从流中读取数据,读取后数据可能不再可用。
1.2 适用容器 / 场景
- 标准库中的
istream_iterator
(从输入流读取数据)、forward_list
的 const 迭代器(部分场景)。示例:用istream_iterator
读取标准输入
1.3 示例
用 istream_iterator
读取标准输入
cpp
#include <iostream>
#include <iterator> // 包含 istream_iterator
#include <vector>
using namespace std;
int main() {
cout << "输入多个整数(按 Ctrl+D 结束):" << endl;
// istream_iterator<int> 是输入迭代器,从 cin 读取 int
istream_iterator<int> input_begin(cin);
istream_iterator<int> input_end; // 尾后迭代器(标记结束)
vector<int> nums;
// 遍历输入流,读取所有整数(输入迭代器仅支持 ++ 和 *)
for (auto it = input_begin; it != input_end; ++it) {
nums.push_back(*it); // *it 读取当前元素(只读,不能修改)
}
// 输出结果
cout << "你输入的整数:";
for (int num : nums) {
cout << num << " ";
}
return 0;
}
输出:
输入多个整数(按 Ctrl+D 结束):
10 20 30 40
你输入的整数:10 20 30 40
关键注意点
- 输入迭代器不能重复遍历 :若再次
++it
回到之前的位置,*it
结果未定义(如流已读取过数据,无法回溯)。 - 仅支持 "只读":
*it = 5
编译错误,无法修改元素。
2、输出迭代器(Output Iterator):只写、单向遍历
2.1 核心特性
- 功能 :仅支持 "只写访问" 和 "单向递增"(
++
),无法读取元素,用于将数据写入目标(如流、容器)。 - 关键操作 :
*it = value
(写元素)、++it
(向后移动)。 - 本质:"一次性写入工具",写入后无法读取,且每个位置仅能写入一次。
2.2 适用容器 / 场景
- 标准库中的
ostream_iterator
(向输出流写入数据)、back_inserter
(向容器尾部插入元素)。
2.3 示例
用 ostream_iterator
写入标准输出
cpp
#include <iostream>
#include <iterator> // 包含 ostream_iterator
#include <vector>
#include <algorithm> // 包含 copy
using namespace std;
int main() {
vector<int> nums = {1, 2, 3, 4, 5};
// ostream_iterator<int> 是输出迭代器,向 cout 写入 int(分隔符为 ", ")
ostream_iterator<int> output(cout, ", ");
// 用 copy 算法:将 nums 的元素通过输出迭代器写入 cout
copy(nums.begin(), nums.end(), output);
return 0;
}
输出:
plaintext
1, 2, 3, 4, 5,
关键注意点
- 输出迭代器不能读取 :
cout << *output
编译错误,无法解引用读取。 - 每个位置仅能写入一次:若对同一迭代器位置多次
*it = value
,结果未定义(可能覆盖或失效)。
3. 前向迭代器(Forward Iterator):读写、单向重复遍历
3.1 核心特性
- 功能 :支持 "读写访问" 和 "单向递增"(
++
),可重复遍历同一序列(元素不会因遍历失效)。 - 关键操作 :
*it
(读写元素)、++it
/it++
(向后移动)、it1 == it2
/it1 != it2
(比较)。 - 本质 :"单向可复用工具",比输入 / 输出迭代器灵活,但无法反向移动(无
--
)。
3.2 适用容器 / 场景
- 无序关联容器:
unordered_map
、unordered_set
(底层哈希表,迭代器仅支持单向移动)。 forward_list
(单向链表,仅支持前向迭代)。
3.3 示例
遍历 unordered_map
(前向迭代器)
cpp
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<string, int> score = {
{"Alice", 95}, {"Bob", 88}, {"Charlie", 92}
};
// unordered_map 的迭代器是前向迭代器,支持 ++ 和读写
auto it = score.begin();
while (it != score.end()) {
// 读写操作:修改 Bob 的分数(*it 是 pair<const string, int>)
if (it->first == "Bob") {
it->second = 90; // 写操作:修改 value
}
// 读操作:输出键值对
cout << it->first << ": " << it->second << endl;
++it; // 仅支持单向递增(无 --)
}
// 重复遍历(前向迭代器支持复用)
cout << "\n重复遍历:";
for (auto it = score.begin(); it != score.end(); ++it) {
cout << it->first << " ";
}
return 0;
}
输出:
plaintext
Bob: 90
Alice: 95
Charlie: 92
重复遍历:Bob Alice Charlie
关键注意点
- 不支持反向移动:
--it
编译错误,只能++
向后移动。 - 无序容器的遍历顺序不固定:
unordered_map
是哈希表,迭代器遍历顺序与插入顺序无关。
4. 双向迭代器(Bidirectional Iterator):读写、双向遍历
核心特性
- 功能 :在 "前向迭代器" 基础上,支持 "反向移动"(
--
),可双向遍历序列。 - 关键操作 :包含前向迭代器的所有操作 +
--it
/it--
(向前移动)。 - 本质:"双向可复用工具",适合需要反向遍历的场景(如链表)。
适用容器 / 场景
- 双向链表:
list
。 - 有序关联容器:
map
、set
(底层红黑树,支持双向移动)。
示例
遍历 list
(双向迭代器,支持反向遍历)
cpp
#include <iostream>
#include <list>
using namespace std;
int main() {
list<int> nums = {10, 20, 30, 40};
// 1. 正向遍历(++)
cout << "正向遍历:";
auto it = nums.begin();
while (it != nums.end()) {
*it += 5; // 写操作:每个元素加 5
cout << *it << " ";
++it;
}
cout << endl;
// 2. 反向遍历(--)
cout << "反向遍历:";
auto rit = nums.end();
--rit; // 从尾后迭代器向前移动到最后一个元素
while (true) {
cout << *rit << " ";
if (rit == nums.begin()) break;
--rit; // 反向移动
}
return 0;
}
输出:
plaintext
正向遍历:15 25 35 45
反向遍历:45 35 25 15
关键注意点
- 支持反向遍历,但不支持随机访问 :
it + 2
编译错误,无法直接跳转到指定位置。 list
的迭代器在插入 / 删除时稳定性高:插入元素时所有迭代器有效,删除元素时仅被删元素的迭代器失效。
5. 随机访问迭代器(Random Access Iterator):读写、随机访问
核心特性
- 功能:在 "双向迭代器" 基础上,支持 "随机访问"(直接跳转到任意位置),是功能最强的迭代器。
- 关键操作 :包含双向迭代器的所有操作 + 算术运算(
it + n
/it - n
)、下标访问(it[n]
)、大小比较(it1 < it2
/it1 > it2
)。 - 本质:"数组式访问工具",效率与原生指针相当,适合连续内存容器。
适用容器 / 场景
- 连续内存容器:
vector
(动态数组)、deque
(双端队列,分段连续)、array
(固定大小数组)。
示例
vector` 的随机访问迭代器
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3, 4, 5};
auto it = nums.begin();
// 1. 随机访问:直接跳转到第 3 个元素(索引 2)
it += 2;
cout << "第 3 个元素:" << *it << endl; // 输出 3
// 2. 算术运算:计算迭代器距离
auto end_it = nums.end();
cout << "容器大小:" << end_it - it << endl; // 输出 3(it 到 end_it 有 3 个元素)
// 3. 下标访问:等价于 *(it + n)
cout << "it[1] = " << it[1] << endl; // 输出 4(it+1 指向的元素)
// 4. 大小比较:判断迭代器位置
if (it < nums.end()) {
cout << "it 在 end 之前" << endl;
}
// 5. 双向移动:++ 和 --
++it;
cout << "it++ 后:" << *it << endl; // 输出 4
--it;
cout << "it-- 后:" << *it << endl; // 输出 3
return 0;
}
输出:
plaintext
第 3 个元素:3
容器大小:3
it[1] = 4
it 在 end 之前
it++ 后:4
it-- 后:3
关键注意点
- 随机访问迭代器可直接转为原生指针 :
vector<int>::iterator
本质是int*
(多数编译器实现),因此效率极高。 - 扩容时迭代器失效:
vector
扩容会分配新内存,原迭代器指向的旧内存失效,需重新获取迭代器。
二、迭代器失效
迭代器失效是指迭代器指向的内存位置无效(如被释放、移动),访问失效迭代器会导致未定义行为(崩溃、数据错乱)。不同迭代器对应的容器,失效规则不同,核心与容器底层实现相关:
迭代器类型 | 对应容器 | 插入元素时失效规则 | 删除元素时失效规则 |
---|---|---|---|
输入 / 输出迭代器 | 流、forward_list |
所有迭代器失效(流数据一次性) | 所有迭代器失效 |
前向迭代器 | unordered_map 、``unordered_set` |
未扩容:有效;扩容:所有失效 | 仅被删除元素的迭代器失效 |
双向迭代器 | list 、map 、set |
所有迭代器有效(节点不移动) | 仅被删除元素的迭代器失效 |
随机访问迭代器 | vector 、deque |
未扩容:插入位置后失效;扩容:全失效 | 删除位置后失效 |
1、序列式容器(vector、deque)
这类容器的元素在内存中是连续或半连续存储的,插入 / 删除操作可能导致元素移动或内存重分配,因此迭代器失效场景较多。
1.1 vector 容器
vector 基于动态数组实现,内存连续,迭代器本质是指向数组元素的指针。
失效场景:
- 插入元素(push_back/insert):
- 若插入后容器容量超过原容量(触发扩容,即内存重新分配),则所有迭代器、指针、引用全部失效(原内存被释放)。
- 若未触发扩容,插入位置之后的迭代器失效(元素后移导致地址变化)。
- 删除元素(erase/pop_back):
- 删除位置之后的迭代器失效(元素前移导致地址变化)。
- 被删除元素的迭代器直接失效。
- resize/clear 操作:
- resize 缩小容器时,超出新大小的元素被销毁,指向这些元素的迭代器失效。
- clear 清空容器后,所有迭代器失效(指向已销毁的元素)。
解决方案:
- 插入前通过
reserve(n)
预留足够容量,避免扩容导致的迭代器全失效。 - 删除元素时,利用
erase
的返回值更新迭代器(erase
返回被删除元素的下一个有效迭代器)。 - 避免在遍历中同时修改容器(如需修改,通过返回值实时更新迭代器)。
cpp
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> vec = {1, 2, 3, 4, 5};
// 错误示例:删除元素后迭代器失效
// for (auto it = vec.begin(); it != vec.end(); ) {
// if (*it == 3) {
// vec.erase(it); // it失效,后续++操作未定义
// } else {
// ++it;
// }
// }
// 正确示例:用erase返回值更新迭代器
for (auto it = vec.begin(); it != vec.end(); ) {
if (*it == 3) {
it = vec.erase(it); // 接收返回值,指向被删元素的下一个
} else {
++it;
}
}
// 插入前预留容量,避免扩容失效
vec.reserve(10); // 预留足够空间,后续插入不会扩容
auto it = vec.begin() + 2;
vec.insert(it, 6); // 未扩容,仅插入位置后迭代器失效,it仍有效
return 0;
}
1.2 deque 容器
deque 是 "双端队列",内存由多个连续块组成,迭代器需要同时记录块地址和块内偏移。
失效场景:
- 插入元素:
- 在两端(push_front/push_back)插入时,若未触发内存块分配,迭代器可能仍有效;若触发新块分配,所有迭代器失效。
- 在中间插入时,所有迭代器失效(内部结构重组)。
- 删除元素:
- 在两端删除时,可能导致迭代器失效(取决于具体实现)。
- 在中间删除时,所有迭代器失效。
解决方案:
- 避免在 deque 中间进行插入 / 删除操作(性能差且易导致迭代器失效)。
- 若需遍历中修改,操作后重新获取迭代器(不依赖原有迭代器)。
2、链表容器(list、forward_list)
list 是双向链表,forward_list 是单向链表,元素通过指针链接,内存不连续。
失效场景:
- 插入元素:所有迭代器均有效(仅新增节点,不影响原有节点的指针关系)。
- 删除元素 :仅指向被删除元素的迭代器失效,其他迭代器仍有效(节点间指针调整不影响其他节点)。
解决方案:
- 删除元素后,不再使用指向被删元素的迭代器即可。
- 遍历中删除时,同样可通过
erase
的返回值更新迭代器(list 的 erase 返回下一个有效迭代器)。
cpp
#include <list>
#include <iostream>
using namespace std;
int main() {
list<int> lst = {1, 2, 3, 4, 5};
// 遍历删除元素,仅被删元素的迭代器失效
for (auto it = lst.begin(); it != lst.end(); ) {
if (*it == 3) {
it = lst.erase(it); // 安全更新迭代器
} else {
++it;
}
}
// 插入元素后,原有迭代器仍有效
auto it = lst.begin();
lst.insert(it, 6); // 插入后it仍指向原位置(元素1)
return 0;
}
3、关联式容器(set、map、multiset、multimap)
这类容器基于红黑树实现(有序),元素按键值排序,节点通过指针链接。
失效场景:
- 插入元素:所有迭代器均有效(红黑树旋转仅调整指针,不改变节点地址)。
- 删除元素 :仅指向被删除元素的迭代器失效,其他迭代器(包括指向其他节点的迭代器)仍有效。
解决方案:
- 删除元素后,避免使用指向被删元素的迭代器。
- 遍历中删除时,通过
erase
返回值更新迭代器(C++11 后,关联容器的 erase 返回下一个有效迭代器)。
cpp
#include <map>
#include <iostream>
using namespace std;
int main() {
map<int, string> mp = {{1, "a"}, {2, "b"}, {3, "c"}};
// 遍历删除元素,仅被删元素的迭代器失效
for (auto it = mp.begin(); it != mp.end(); ) {
if (it->first == 2) {
it = mp.erase(it); // 更新迭代器
} else {
++it;
}
}
// 插入元素后,原有迭代器仍有效
auto it = mp.find(1);
mp.insert({4, "d"}); // 插入后it仍指向键1的节点
return 0;
}
4、无序容器(unordered_set、unordered_map 等)
这类容器基于哈希表实现,元素存储在桶(bucket)中,桶内是链表或数组。
失效场景:
- 插入元素:
- 若插入后未触发哈希表重建(负载因子未超过阈值),仅被插入桶的迭代器可能受影响,其他迭代器有效。
- 若触发重建(哈希表扩容并重新哈希),所有迭代器失效(节点地址可能改变)。
- 删除元素 :仅指向被删除元素的迭代器失效,其他迭代器有效(哈希表结构未变)。
解决方案:
- 插入前通过
reserve(n)
预留足够桶空间,避免重建导致的迭代器全失效。 - 删除元素后,不使用指向被删元素的迭代器。