迭代器分类与失效规则总表
迭代器是 STL 的黏合剂。写出通用代码的前提是知道"手上这个迭代器能做什么",写出安全代码的前提是知道"什么时候它会失效"。
一、五类迭代器
| 类别 | 能力 | 典型代表 |
|---|---|---|
| 输入迭代器 Input | 单遍读、++ | std::istream_iterator |
| 输出迭代器 Output | 单遍写、++ | std::ostream_iterator、back_inserter |
| 前向迭代器 Forward | 多遍读写、++ | std::forward_list |
| 双向迭代器 Bidirectional | 加 -- | std::list、std::map |
| 随机访问迭代器 RandomAccess | 加 +=、-、下标 | std::vector、std::deque、裸指针 |
C++20 进一步把随机访问拆成"连续迭代器(contiguous)",vector 的迭代器满足,deque 不满足,因此 vector 的元素可以安全地传给 memcpy。
二、算法对迭代器的要求
| 算法 | 最低要求 |
|---|---|
std::find / count | 输入迭代器 |
std::copy | 输入 + 输出 |
std::reverse | 双向 |
std::sort | 随机访问 |
std::stable_sort | 随机访问 |
std::nth_element | 随机访问 |
std::binary_search | 前向(实际要求有序,随机访问更高效) |
把 list 传给 std::sort 会得到一长串模板报错。正确做法是 lst.sort(),或先拷贝到 vector。
三、各容器失效规则
| 容器 | 插入 | 删除 | 备注 |
|---|---|---|---|
vector | 扩容时全部失效;否则插入点后失效 | 删除点后失效 | 引用、指针同样失效 |
deque | 两端插入只失效迭代器,不失效引用 | 中间删除失效全部 | 引用稳定性优于 vector |
list | 不失效任何迭代器 | 仅被删元素失效 | 节点式容器优势 |
forward_list | 同上 | 同上 | 只能前向 |
map/set | 不失效任何迭代器 | 仅被删元素失效 | 红黑树节点稳定 |
unordered_map/unordered_set | rehash 时全部失效 | 仅被删元素失效 | 引用在 rehash 后仍有效(节点不变) |
std::string | 扩容时全部失效 | 删除点后失效 | c_str() 同步失效 |
注意 unordered_map 的微妙之处:rehash 会让迭代器失效,但元素引用与指针仍然有效,因为节点本身没有移动。
四、通用遍历写法
template <class Container>
void dump(const Container& c) {
for (typename Container::const_iterator it = c.begin(); it != c.end(); ++it) {
std::cout << *it << "\n";
}
}
C++11 以后直接:
template <class Container>
void dump(const Container& c) {
for (const auto& x : c) std::cout << x << "\n";
}
五、需要"删除当前元素"的通用写法
C++11 起,顺序容器与关联容器的 erase 都返回下一个迭代器:
for (auto it = c.begin(); it != c.end(); ) {
if (shouldDrop(*it)) it = c.erase(it);
else ++it;
}
forward_list 例外:没有 erase(it),要用 erase_after(it) 或 remove_if。
六、插入迭代器
#include <iterator>
std::vector<int> dst;
std::copy(src.begin(), src.end(), std::back_inserter(dst)); // push_back
std::copy(src.begin(), src.end(), std::inserter(dst, dst.begin())); // insert
std::copy(src.begin(), src.end(), std::ostream_iterator<int>(std::cout, " ")); // 输出
back_inserter 是"输出迭代器适配器",让只支持覆盖写入的算法变成追加写入。C++98 里同样可用。
七、反向与常量迭代器
for (auto it = v.rbegin(); it != v.rend(); ++it) use(*it);
// const 正确性
const std::vector<int>& cv = v;
for (auto it = cv.begin(); it != cv.end(); ++it) use(*it); // const_iterator
从 iterator 转 const_iterator 是隐式允许的,反向不行。需要显式转换时用 std::next(v.begin(), i) 之外的方式,或 C++11 的 std::make_move_iterator 等适配器。
八、迭代器 vs 下标 vs 指针
| 场景 | 推荐 |
|---|---|
| 顺序遍历 | 范围 for |
| 需要位置、删除元素 | 迭代器 + erase 返回值 |
| 数值索引、密集访问 | 下标(vector 专用) |
| 需要 C 接口 | data()(C++11 起) |
| 需要跨容器通用 | 迭代器 + 模板 |
九、调试技巧
- 开启调试模式:libstdc++ 用
-D_GLIBCXX_DEBUG,会检查迭代器越界、失效访问; - ASan:
-fsanitize=address能抓到访问已释放内存; - 不要比较不同容器的迭代器,也不要在
end()上解引用; - 不要保存迭代器跨操作:任何修改容器的操作前,先确认失效规则。
迭代器的复杂度是 C++ 通用性的代价。规则记住了,剩下的就是手感。