一、什么是迭代器失效?
迭代器可以理解为"指向容器中某个元素的位置"。
例如:
vector<int> nums = {10, 20, 30};
// it 指向 nums 中的第一个元素 10
auto it = nums.begin();
cout << *it << endl;
这里的:
*it
表示访问迭代器当前指向的元素。
所谓迭代器失效,就是容器发生某些操作后,原来的迭代器不再能安全地访问原来的元素位置。
继续使用失效迭代器,可能导致:
程序崩溃
访问错误数据
运行结果异常
未定义行为
除了迭代器,某些操作还可能导致原来的指针和引用失效。
例如:
vector<int> nums = {1, 2, 3};
// p 指向第一个元素
int* p = &nums[0];
// ref 是第一个元素的引用
int& ref = nums[0];
如果 vector 后续发生扩容,p、ref 和原来的迭代器都可能失效。
二、为什么会出现迭代器失效?
不同容器的底层结构不同,所以失效规则也不同。
vector:底层是连续内存的动态数组。
map:底层通常是红黑树。
unordered_map:底层通常是哈希表。
其中,vector 最容易发生迭代器失效,因为它需要保证元素在一段连续内存中存储。
当 vector 容量不足时,它会申请一块新的、更大的连续内存,再把旧元素移动过去。此时原来的地址就可能全部变化。
而 map 使用树结构存储节点,插入新节点一般不会移动旧节点。
unordered_map 使用哈希表存储元素,当桶数量变化时会发生 rehash,原来的迭代器可能失效。
三、vector 的迭代器失效
vector 是面试中最常被问到的迭代器失效容器。
它的底层是动态数组,元素在内存中连续存储。
1. vector 扩容为什么会导致迭代器失效?
先看一个例子:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums;
// 先放入一个元素
nums.push_back(10);
// 保存第一个元素的迭代器、指针和引用
auto it = nums.begin();
int* p = &nums[0];
int& ref = nums[0];
// 继续插入元素,可能触发扩容
nums.push_back(20);
nums.push_back(30);
nums.push_back(40);
// 如果中间发生扩容,
// it、p、ref 都可能已经失效
cout << nums[0] << endl;
return 0;
}
vector 内部有两个重要概念:
nums.size(); // 当前元素数量
nums.capacity(); // 当前已经申请的容量
当:
size > capacity
时,vector 通常会:
1. 申请一块更大的连续内存。
2. 将旧元素移动或复制到新内存。
3. 释放旧内存。
因此,原来的迭代器、指针和引用仍然指向旧内存,自然就失效了。
可以简单理解为:
扩容前:
it、p、ref ───> [10]
扩容后:
nums ───> [10][20][30][40]
it、p、ref ───> 旧内存,已经不能继续使用
2. vector 的 push_back 会不会导致迭代器失效?
答案是:可能会。
vector<int> nums = {1, 2, 3};
auto it = nums.begin();
nums.push_back(4);
如果 push_back(4) 时发生扩容,那么:
所有迭代器失效。
所有指针失效。
所有引用失效。
如果没有发生扩容,则:
原来指向已有元素的迭代器、指针和引用通常仍然有效。
但是 end() 迭代器会失效。
例如:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums;
// 预留 10 个元素空间,减少后续扩容概率
nums.reserve(10);
nums.push_back(1);
nums.push_back(2);
// it 指向第一个元素
auto it = nums.begin();
// 当前容量足够,通常不会发生扩容
nums.push_back(3);
// it 仍然可以安全访问第一个元素
cout << *it << endl;
return 0;
}
这里的 reserve(10) 只是提前申请容量,不能改变 size()。
vector<int> nums;
nums.reserve(10);
cout << nums.size() << endl; // 0
cout << nums.capacity() << endl; // 至少为 10
3. vector 的 insert 为什么会导致迭代器失效?
在 vector 中间插入元素时,后面的元素需要向后移动。
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3, 4};
// it 指向元素 3
auto it = nums.begin() + 2;
// 在下标 1 的位置插入 100
nums.insert(nums.begin() + 1, 100);
// 原来的 it 可能失效,不能继续使用
// cout << *it << endl;
return 0;
}
如果插入操作触发扩容:
所有迭代器、指针和引用都会失效。
即使没有扩容,插入位置及其后面的元素都发生了移动,因此:
插入位置之前的迭代器通常仍然有效。
插入位置及其之后的迭代器、指针和引用会失效。
4. vector 的 erase 为什么会导致迭代器失效?
删除 vector 中间元素后,后面的元素需要向前移动来填补空位。
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {10, 20, 30, 40};
// it 指向 20
auto it = nums.begin() + 1;
// 删除元素 20
nums.erase(it);
// 原来的 it 已经失效,不能继续使用
// cout << *it << endl;
return 0;
}
对于 vector::erase():
被删除位置及其之后的迭代器、指针和引用都会失效。
因此,遍历 vector 时删除元素,不能直接写成下面这样:
vector<int> nums = {1, 2, 3, 4, 5};
for (auto it = nums.begin(); it != nums.end(); ++it) {
if (*it % 2 == 0) {
nums.erase(it); // 删除后 it 已经失效
}
}
上面代码的问题是:
erase(it) 后,it 已经失效。
循环末尾还会执行 ++it。
此时相当于操作失效迭代器。
5. vector 中安全删除元素的正确写法
正确方式是接收 erase() 的返回值。
erase() 会返回被删除元素之后的下一个有效迭代器。
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3, 4, 5, 6};
for (auto it = nums.begin(); it != nums.end(); ) {
// 删除所有偶数
if (*it % 2 == 0) {
// erase 返回下一个有效位置
it = nums.erase(it);
} else {
// 当前元素不删除,迭代器正常后移
++it;
}
}
for (int num : nums) {
cout << num << " ";
}
return 0;
}
输出结果:
1 3 5
这段代码需要重点理解:
it = nums.erase(it);
删除当前元素后,不再手动 ++it,而是直接使用 erase() 返回的下一个有效迭代器。
6. vector 常见操作的失效规则
| 操作 | 迭代器、指针、引用失效情况 |
|---|---|
push_back() / emplace_back() 触发扩容 |
全部失效 |
push_back() / emplace_back() 未扩容 |
原有元素位置通常有效,但 end() 失效 |
insert() 触发扩容 |
全部失效 |
insert() 未扩容 |
插入位置及其之后失效 |
erase() |
删除位置及其之后失效 |
clear() |
全部失效 |
reserve() 导致重新分配内存 |
全部失效 |
resize() 导致扩容 |
可能全部失效 |
可以简单记忆:
vector 只要发生扩容,全部失效。
vector 在中间插入或删除,操作位置及其后面的迭代器容易失效。
四、map 的迭代器失效
map 是有序键值对容器,底层通常使用红黑树实现。
与 vector 不同,map 的元素通常是独立节点存储的。插入新元素时,不需要整体移动原有元素。
1. map 插入元素会导致原迭代器失效吗?
通常不会。
#include <iostream>
#include <map>
using namespace std;
int main() {
map<int, string> students;
students[1] = "Tom";
students[2] = "Jack";
// it 指向 key 为 1 的元素
auto it = students.find(1);
// 插入新元素
students[3] = "Alice";
// 原来的 it 仍然有效
cout << it->first << " : " << it->second << endl;
return 0;
}
输出结果:
1 : Tom
对于 map:
插入元素通常不会使已有元素的迭代器、指针和引用失效。
这也是 map 和 vector 的重要区别。
2. map 删除元素会导致哪些迭代器失效?
在 map 中,删除某个元素时:
只有指向被删除元素的迭代器、指针和引用失效。
其他元素的迭代器通常仍然有效。
示例:
#include <iostream>
#include <map>
using namespace std;
int main() {
map<int, string> students;
students[1] = "Tom";
students[2] = "Jack";
students[3] = "Alice";
auto it1 = students.find(1);
auto it2 = students.find(2);
// 删除 key 为 2 的元素
students.erase(it2);
// it2 已经失效,不能继续使用
// cout << it2->second << endl;
// it1 指向的元素没有被删除,仍然有效
cout << it1->first << " : " << it1->second << endl;
return 0;
}
3. 遍历 map 时安全删除元素
遍历 map 时删除元素,也推荐使用 erase() 的返回值。
#include <iostream>
#include <map>
using namespace std;
int main() {
map<int, string> students;
students[1] = "Tom";
students[2] = "Jack";
students[3] = "Alice";
students[4] = "Bob";
for (auto it = students.begin(); it != students.end(); ) {
// 删除所有偶数 key 对应的元素
if (it->first % 2 == 0) {
// erase 返回下一个有效迭代器
it = students.erase(it);
} else {
++it;
}
}
for (const auto& item : students) {
cout << item.first << " : " << item.second << endl;
}
return 0;
}
输出结果:
1 : Tom
3 : Alice
4. map 常见操作的失效规则
| 操作 | 迭代器、指针、引用失效情况 |
|---|---|
insert() / emplace() |
已有元素通常不失效 |
operator[] 插入新元素 |
已有元素通常不失效 |
erase(it) |
只有被删除元素失效 |
clear() |
全部失效 |
简单记忆:
map 插入通常不影响旧迭代器。
map 删除谁,谁的迭代器失效。
五、unordered_map 的迭代器失效
unordered_map 是无序键值对容器,底层通常是哈希表。
它最大的特点是查找效率高,但由于可能发生 rehash,它的迭代器失效规则比 map 更需要注意。
1. 什么是 rehash?
unordered_map 内部会把元素分配到不同的桶中。
例如:
桶 0:元素 A
桶 1:元素 B、元素 C
桶 2:元素 D
当元素越来越多时,如果桶数量不够,哈希冲突可能增加,查找效率下降。
这时 unordered_map 可能会重新申请更多桶,并重新计算每个元素应该放到哪个桶中。
这个过程叫:
rehash,中文通常叫重哈希。
2. unordered_map 插入元素会导致迭代器失效吗?
答案是:可能会。
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<int, string> students;
students[1] = "Tom";
students[2] = "Jack";
auto it = students.find(1);
// 插入新元素时,可能触发 rehash
students[3] = "Alice";
// 如果发生 rehash,it 可能已经失效
// cout << it->second << endl;
return 0;
}
如果插入元素导致 rehash:
所有迭代器都会失效。
但是需要注意:
rehash 通常不会让指向元素的引用和指针失效。
真正失效的重点是迭代器。
为了避免频繁 rehash,可以提前预留空间:
#include <unordered_map>
using namespace std;
int main() {
unordered_map<int, string> students;
// 预计要插入 100 个元素,提前预留空间
students.reserve(100);
students[1] = "Tom";
students[2] = "Jack";
return 0;
}
reserve() 可以减少插入过程中的重哈希次数,但不代表任何情况下都绝对不会发生 rehash。
3. unordered_map 删除元素的影响
删除元素时:
只有指向被删除元素的迭代器、指针和引用失效。
其他元素通常仍然有效。
示例:
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<int, string> students;
students[1] = "Tom";
students[2] = "Jack";
students[3] = "Alice";
auto it1 = students.find(1);
auto it2 = students.find(2);
// 删除 key 为 2 的元素
students.erase(it2);
// it2 已经失效,不能继续使用
// cout << it2->second << endl;
// it1 仍然指向 key 为 1 的元素
cout << it1->first << " : " << it1->second << endl;
return 0;
}
4. 遍历 unordered_map 时安全删除元素
和 vector、map 一样,删除时接收 erase() 的返回值。
#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
unordered_map<int, string> students;
students[1] = "Tom";
students[2] = "Jack";
students[3] = "Alice";
students[4] = "Bob";
for (auto it = students.begin(); it != students.end(); ) {
// 删除 key 为偶数的元素
if (it->first % 2 == 0) {
it = students.erase(it);
} else {
++it;
}
}
for (const auto& item : students) {
cout << item.first << " : " << item.second << endl;
}
return 0;
}
注意:unordered_map 是无序容器,因此最终输出顺序不固定。
5. unordered_map 常见操作的失效规则
| 操作 | 迭代器、指针、引用失效情况 |
|---|---|
插入元素但未发生 rehash |
原有迭代器通常仍有效 |
插入元素且发生 rehash |
所有迭代器失效 |
reserve() / rehash() |
所有迭代器失效 |
erase(it) |
只有被删除元素失效 |
clear() |
全部失效 |
简单记忆:
unordered_map 插入时要防 rehash。
一旦 rehash,原来的迭代器可能全部失效。
六、三种容器的迭代器失效对比
| 操作 | vector | map | unordered_map |
|---|---|---|---|
| 插入元素 | 可能扩容,可能全部失效 | 原有迭代器通常有效 | 可能 rehash,迭代器可能全部失效 |
| 中间插入 | 插入位置及之后通常失效 | 不适用下标位置概念 | 不适用下标位置概念 |
| 删除一个元素 | 删除位置及之后失效 | 仅被删元素失效 | 仅被删元素失效 |
| 扩容 / 重分配 | 全部失效 | 不存在类似扩容移动 | rehash 后迭代器失效 |
| clear | 全部失效 | 全部失效 | 全部失效 |
| 是否有序 | 按插入顺序 | 按 key 排序 | 无序 |
可以这样快速记忆:
vector:最容易失效,扩容全失效,删除后面也失效。
map:最稳定,插入不影响,删除谁谁失效。
unordered_map:插入可能 rehash,rehash 后迭代器失效。
七、遍历容器时删除元素的通用写法
无论是 vector、map 还是 unordered_map,遍历时删除元素都推荐使用下面的写法:
for (auto it = container.begin(); it != container.end(); ) {
if (满足删除条件) {
it = container.erase(it);
} else {
++it;
}
}
核心原因是:
erase(it) 后,原来的 it 不一定还能使用。
erase 的返回值正好是下一个有效位置。
下面是一个通用示例:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3, 4, 5};
for (auto it = nums.begin(); it != nums.end(); ) {
if (*it > 3) {
// 删除大于 3 的元素
it = nums.erase(it);
} else {
++it;
}
}
for (int num : nums) {
cout << num << " ";
}
return 0;
}
输出结果:
1 2 3
八、常见错误写法
1. 删除后继续使用旧迭代器
错误写法:
for (auto it = nums.begin(); it != nums.end(); ++it) {
if (*it == 3) {
nums.erase(it);
// it 已经失效,后续 ++it 存在风险
}
}
正确写法:
for (auto it = nums.begin(); it != nums.end(); ) {
if (*it == 3) {
it = nums.erase(it);
} else {
++it;
}
}
2. 用范围 for 循环直接删除元素
错误写法:
for (int num : nums) {
if (num == 3) {
nums.erase(nums.begin());
}
}
范围 for 循环本质上也会使用迭代器。
在循环过程中修改容器结构,可能导致内部迭代器失效,因此不推荐这样写。
如果需要删除元素,应该改用显式迭代器循环。
3. 以为 reserve 可以解决所有 vector 失效问题
reserve() 的作用是提前预留容量,主要减少扩容带来的失效问题。
但是它不能解决中间插入、删除元素带来的迭代器失效问题。
例如:
vector<int> nums = {1, 2, 3, 4};
nums.reserve(100);
auto it = nums.begin() + 2;
// 即使容量足够,中间插入仍会移动后面的元素
nums.insert(nums.begin() + 1, 100);
// it 仍然可能失效
因此:
reserve 只能减少扩容问题,
不能解决 vector 元素移动问题。
九、面试高频问题整理
1. 什么是迭代器失效?
迭代器失效是指容器经过插入、删除、扩容或重哈希等操作后,原来的迭代器不再能安全访问原来的元素位置。继续使用失效迭代器可能导致程序崩溃或未定义行为。
2. vector 为什么容易发生迭代器失效?
因为 vector 底层是连续内存的动态数组。当容量不足时,vector 会重新申请更大的内存,并把原元素移动过去,原来的迭代器、指针和引用仍指向旧内存,所以会失效。
另外,在中间插入和删除元素时,后面的元素需要移动,因此操作位置及其之后的迭代器也可能失效。
3. vector 扩容后哪些内容会失效?
如果 vector 发生扩容:
所有迭代器失效。
所有指针失效。
所有引用失效。
因为元素存储地址发生了变化。
4. map 插入元素会导致迭代器失效吗?
通常不会。
map 底层通常是红黑树,插入新节点不需要移动已有节点,因此已有元素的迭代器、指针和引用通常仍然有效。
删除元素时,只有指向被删除元素的迭代器、指针和引用失效。
5. unordered_map 为什么插入后迭代器可能失效?
unordered_map 底层是哈希表。插入元素后,如果负载过高,可能触发 rehash,也就是重新分配桶并重新组织元素。
一旦发生 rehash,原来的迭代器可能全部失效。
因此如果预计要插入大量数据,可以提前调用 reserve(),减少重哈希概率。
6. 遍历容器时如何安全删除元素?
推荐写法是:
for (auto it = container.begin(); it != container.end(); ) {
if (满足条件) {
it = container.erase(it);
} else {
++it;
}
}
因为 erase() 会返回下一个有效迭代器,可以避免继续操作已经失效的旧迭代器。
十、总结
迭代器失效是 STL 面试和实际开发中都非常重要的问题。
vector 底层使用连续内存,扩容会导致所有迭代器、指针和引用失效;中间插入和删除会导致操作位置及其之后的迭代器失效。
map 的底层通常是红黑树,插入元素通常不会影响已有迭代器;删除元素时,只有被删除元素对应的迭代器失效。
unordered_map 的底层通常是哈希表,插入元素时可能触发 rehash。一旦发生 rehash,原来的迭代器可能全部失效,因此大量插入前可以使用 reserve() 提前预留空间。
最后可以简单记忆:
vector:
扩容时全部失效;
插入和删除时,操作位置及之后容易失效。
map:
插入通常不失效;
删除谁,谁失效。
unordered_map:
插入可能触发 rehash;
rehash 后迭代器可能全部失效。
遍历删除元素:
使用 it = container.erase(it)。
掌握这些规则后,可以避免 STL 容器操作中很多隐蔽的崩溃和错误。