C++ 迭代器失效
更新时间:2026-08-27。本文是
languages/cpp/主题高手层第 11 篇。入门篇讲了迭代器的用法,这篇讲它的"暗面"——容器被修改后,手上的迭代器/引用/指针可能就失效了。失效的迭代器解引用是未定义行为:不报错、不崩溃,给你一个垃圾值,或者在某次看似无关的修改后突然崩溃。这是 C++ 程序"幽灵 bug"的重要来源。
本文要回答的问题
- 什么是迭代器失效?为什么 vector 插入元素会让旧迭代器"变废"?
- 各容器的失效规则有什么不同?(vector vs map vs unordered_map)
- 循环里删除元素怎么安全写?
- reserve 和 rehash 跟失效有什么关系?
失效的本质:指向的位置"不再是那个东西"
迭代器本质是"指向容器中某个位置的句柄"。容器修改后,如果元素被移动或内存被重新分配,旧句柄就指向了错误的位置。解引用旧迭代器 = 读错位内存。
失效分两类:
- 引用/指针失效:元素搬家了(vector 扩容搬走全部元素)。
- 迭代器失效:迭代器内部状态指向的位置已不属于容器(map 节点删除、unordered 桶重建)。
各容器的失效规则一览
| 容器 | 插入导致失效 | 删除导致失效 | 关键点 |
|---|---|---|---|
vector / string | 扩容时全部失效;不扩容时仅插入点之后 | 被删元素及其之后的迭代器 | 扩容是重灾区 |
deque | 两端插入可能全部失效;中间插入全部失效 | 被删元素及其之后 | 结构特殊 |
list / forward_list | 不失效 | 仅被删元素 | 节点独立分配 |
set / map | 不失效 | 仅被删元素 | 红黑树节点独立 |
unordered_set/map | rehash 时全部失效;否则不失效 | 仅被删元素 | rehash 才是雷 |
记忆口诀:连续存储的容器(vector/deque)插入删除会波及"后面的"迭代器;节点式容器(list/map)只有被删的那个失效;哈希容器怕 rehash。
cpp
#include <vector>
#include <map>
#include <unordered_map>
// vector:插入导致扩容 → 全部失效
std::vector<int> v{1, 2, 3};
auto it = v.begin();
v.push_back(4); // 可能扩容 → it 失效!
// *it = 10; // 未定义行为!
// map:删除只影响被删元素
std::map<int, int> m{{1, 1}, {2, 2}, {3, 3}};
auto it2 = m.begin(); // 指向 {1,1}
m.erase(3); // 删除 {3,3}
// it2 仍然有效!
// unordered_map:插入触发 rehash → 全部失效
std::unordered_map<int, int> um{{1, 1}, {2, 2}};
auto it3 = um.begin();
um.insert({100, 100}); // 触发 rehash → it3 失效!
循环里删除:经典错误与安全写法
3.1 经典错误:删除后继续用失效迭代器
cpp
std::vector<int> v{1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it % 2 == 0) {
v.erase(it); // 错!erase 后 it 失效,++it 是未定义行为
}
}3.2 正确写法一:利用 erase 返回值(C++11 起)
cpp
for (auto it = v.begin(); it != v.end();) {
if (*it % 2 == 0) {
it = v.erase(it); // erase 返回下一个有效迭代器
} else {
++it;
}
}3.3 正确写法二:标准库算法 + erase 惯用法
cpp
#include <algorithm>
// remove 把偶数移到末尾,返回新的逻辑结尾;erase 真正删除
v.erase(std::remove_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; }), v.end());erase-remove 惯用法是删除的"标准答案":remove 不真正删(只重排),erase 收尾。注意 remove 本身不失效问题——它在循环里安全移动元素。
3.4 map 的删除安全
map 删除只影响被删节点,循环删除更简单:
cpp
std::map<int, int> m{{1, 1}, {2, 2}, {3, 3}};
for (auto it = m.begin(); it != m.end();) {
if (it->second == 2) {
m.erase(it++); // 先自增再删:it++ 返回旧值副本
} else {
++it;
}
}reserve 与 rehash:提前"占地"防失效
4.1 vector 的 reserve
扩容是 vector 失效的主因。reserve 提前预留容量,只要不超过预留量就不会扩容、不会失效:
cpp
std::vector<int> v;
v.reserve(10000); // 提前分配,预知规模时用
for (int i = 0; i < 10000; ++i) v.push_back(i);
// 全程无扩容 → 期间取得的迭代器不会因 push 失效4.2 unordered 的 reserve / rehash
cpp
std::unordered_map<int, int> um;
um.reserve(10000); // 提前开桶,避免多次 rehash
um.rehash(20000); // 手动设置桶数rehash 时机:当元素数超过 max_load_factor() * bucket_count() 时触发。reserve 预开桶可避免插入途中的 rehash 导致"全部迭代器失效"。
失效 bug 的排查线索
| 现象 | 可能的元凶 |
|---|---|
循环删除后 Segmentation fault | erase 后继续用失效迭代器 |
| 值"诡异"(错位/垃圾值) | vector 扩容后旧迭代器解引用 |
崩溃在 ++it 或 *it | 迭代器已失效未察觉 |
| 崩溃位置离修改点很远 | 引用被保存、之后容器扩容 |
防范口诀:
- 保存的迭代器/引用/指针,跨越"容器修改"使用前,先确认该操作会不会让它们失效。
- 循环删除优先用
erase返回值 /erase-remove/it++技巧。 - 规模已知时用
reserve预分配,既是性能优化也是失效规避。
与本站性能主线衔接
- vector 扩容:扩容导致元素拷贝(性能)且迭代器全失效(正确性),
reserve一箭双雕,见 vector 深入。 - 哈希表 rehash:rehash 的代价与规避,见 unordered 容器。
- 内存布局:失效根因是内存搬移,与 对象布局与缓存、L3 缓存 关联。
- 性能剖析:反复扩容导致的
memcpy热点可用 perf 观察。
一句话总结
迭代器失效的规则一句话:"vector/deque 连续存储波及后面、list/map 节点式只殃及被删者、unordered 怕 rehash";循环删除用 erase 返回值或 erase-remove,规模已知先 reserve——既是性能优化也是失效规避。
上一篇:锁与死锁 下一篇:STL 容器选型深挖