【C++ 面试真题】聊聊 C++ 的迭代器
迭代器是标准库的"关节"------容器和算法全靠它连接。背得出"迭代器用来遍历容器"只是及格,真考你的是"五种迭代器类别各是什么能力、它和指针到底什么关系、那张'各容器失效规则表'、range-for 里增删容器为什么炸"。本文把迭代器一次讲透。
一、开场:迭代器有哪些类别?
❓ 介绍一下 C++ 的迭代器?
✅ 按能力分五级,层层递进:
| 类别 | 能力 | 典型代表 |
|---|---|---|
| 输入 | 单遍读、++ |
istream_iterator |
| 输出 | 单遍写、++ |
ostream_iterator |
| 前向 | 多遍读写、++ |
forward_list、unordered_* |
| 双向 | 再加回退 -- |
list、map、set |
| 随机访问 | 再加跳转 +n、[] |
vector、deque、裸指针 |
[C++20] 又补上最严格的连续迭代器(ContiguousIterator)------元素物理连续,vector、string、array、裸指针都属于。
回答思路:从"输入/输出 → 前向 → 双向 → 随机访问"报能力阶梯,每级点一个容器代表,再补一句"类别决定能用哪些算法"------这半句就是你的加分项,面试官多半会顺着它往下问。
💡 类别决定算法门槛 :
std::sort只吃随机访问迭代器,给它 list 直接编译报错------所以 list 自带成员sort();std::advance对随机访问 O(1)、对其余 O(n)。容器提供了哪类迭代器,就决定了它能"白嫖"哪些算法。
二、先说结论:泛化的指针,算法与容器的桥
❓ 迭代器到底是什么?
✅ 一句话:迭代器是"泛化的指针" ------用统一接口(*、++、==)遍历任意容器,让算法不必关心容器底层。
cpp
int a[] = {3, 1, 4};
sort(a, a + 3); // 裸指针当迭代器
vector<int> v{3, 1, 4};
sort(v.begin(), v.end()); // 同一个 sort
list<int> l{3, 1, 4};
l.sort(); // list 只能用自己的
💡 这是标准库的设计核心:"算法 + 容器"通过迭代器解耦 。九十多个标准算法只认迭代器区间
[first, last),不认容器------sort能排数组、能排 vector,靠的就是"裸指针也满足迭代器要求"这一条。
三、与指针的关系:像、是、超越
❓ 迭代器和指针到底什么关系?
✅ 三层:
- 是 :裸指针满足随机访问迭代器的全部要求,指针就是迭代器,算法通吃;
- 像 :迭代器支持
*、->、++、!=,用法与指针一致; - 超越:迭代器是类对象时能干指针干不了的事------调试模式带边界检查、流迭代器把流包装成序列、插入迭代器把"赋值"变成"插入"。
cpp
vector<int> v{1, 2, 3};
auto it = v.begin();
*it = 10; // 用法和指针一样
++it; // 走到下一个
begin()/end() 划出半开区间 [begin, end):end() 指向"最后一格的下一格",是个哨兵,不可解引用 。空容器就是 begin() == end(),表达起来零成本。
💡 全局版本
std::begin(v)/std::end(v)连裸数组都能伺候------数组没有成员函数,全局函数模板返回首指针和尾后指针,让数组和容器在算法面前完全平权。
四、各容器失效规则总表
❓ 各容器的迭代器什么时候失效?
✅ 汇总成一张表:
| 容器 | 插入 | 删除 |
|---|---|---|
| vector/string | 扩容→全部失效;否则插点及之后 | 删点及之后 |
| deque | 两端插→迭代器失效、引用有效;中间插→全失效 | 两端删→仅被删点;中间删→全失效 |
| list | 不失效 | 仅被删元素 |
| map/set | 不失效 | 仅被删元素 |
| unordered_* | rehash→迭代器失效、引用不失效;否则不失效 | 仅被删元素 |
⚠️ 本质 :迭代器失效就是"悬空指针"的泛化------它内部记着的内存已被释放或搬走,再解引用就是未定义行为。表中"引用是否幸存"常被追问:节点式容器(list、map、unordered)元素不搬家,引用大多活下来 ;连续内存(vector 扩容)整块搬家,全军覆没。
五、失效的三大应对
❓ 实际编码怎么避开失效坑?
✅ 三个惯用法:
① erase 接返回值------它返回"被删元素的下一个"的有效迭代器:
cpp
for (auto it = v.begin();
it != v.end(); )
if (*it % 2) it = v.erase(it);
else ++it;
② reserve 防扩容------提前开够容量,循环里 push_back 不再触发搬家。
③ 不缓存 end() ------循环条件里现取 v.end(),别在循环外存下来。
[C++20] 起还有一招更省的:std::erase_if 一个函数替代整段循环:
cpp
erase_if(v, [](int x) { return x % 2; });
④ erase-remove 惯用法 ------C++98 时代的经典:remove 只把要留的元素前移、返回新逻辑终点,真正删除交给 erase:
cpp
// 删掉所有 0
v.erase(
remove(v.begin(), v.end(), 0),
v.end());
// [C++20] 直接一行:
// erase(v, 0);
💡 为什么 remove 不顺手删?因为算法手里只有迭代器,够不着容器的 size------"真正缩短容器"只能由容器自己(erase)完成。这道题单独出现频率极高。
六、range-for:迭代器的语法糖
❓ 范围 for 和迭代器什么关系?
✅ range-for 就是迭代器循环的糖,编译器把它展开成:
cpp
for (auto x : v) use(x);
// 等价于:
// for (auto it = v.begin();
// it != v.end(); ++it)
// use(*it);
⚠️ 最大坑:循环体内增删容器。展开后就是"拿着迭代器循环",一 push_back 触发扩容、一 erase 就失效,直接未定义行为。要边遍历边改,退回显式迭代器循环 + erase 惯用法。
另一个高频坑是 auto 的三种拷贝语义:
cpp
for (auto x : v) x = 1; // ❌ 改副本
for (auto& x : v) x = 1; // ✅ 真改元素
for (const auto& x : v) // ✅ 只读免拷贝
cout << x;
七、const_iterator 与 cbegin
❓ 只读遍历用什么迭代器?
✅ const_iterator------"能读不能写"的迭代器,指向的元素视为 const:
cpp
const vector<int> cv{1, 2};
auto it = cv.begin();
// 推导出 const_iterator
// *it = 5; // ❌ 编译报错
想显式要 const 版本,用 cbegin() / cend();容器本身是 const 时,begin() 也自动返回 const_iterator。只读遍历优先 const------把"误写"挡在编译期。
八、面试高频追问
❓ Q1:为什么 std::sort 排不了 list?
✅ sort 要求随机访问迭代器(分区要 O(1) 跳转),list 只有双向迭代器,直接编译报错。list 自带成员 sort(),归并实现,O(n log n)。
❓ Q2:迭代器为什么设计成半开区间?
✅ 三个好处:begin == end 天然表达空区间;循环条件统一用 !=(不依赖 <,前向迭代器也能用);判断结束只需一次比较。这个设计叫"左闭右开",贯穿整个标准库。
❓ Q3:std::advance 的复杂度?
✅ 随机访问迭代器 O(1)(一次加法);其余 O(n)(逐步 ++)。std::distance 同理------所以"跨容器通吃"的代价藏在迭代器类别里。
❓ Q4:unordered_map 的迭代器是哪一类?
✅ 前向迭代器 ------能多遍 ++,但不能 --。所以它不能像 map 那样反向遍历。而 map/set 是双向的。这也是"无序族功能换性能"的体现之一。
❓ Q5:插入迭代器是什么?
✅ 把"赋值给迭代器"变成"插入进容器"的适配器:back_inserter 变 push_back、inserter 变 insert。配合 copy 等算法,往空容器里边构造边写,不用先开好大小。
❓ Q6:迭代器失效和悬空指针是一回事吗?
✅ 本质是一回事------都是"底层内存没了还去访问"。区别只是迭代器把指针包了一层:连续容器失效是整块搬家,节点容器失效往往只死被删的那个。调试期可开 _ITERATOR_DEBUG_LEVEL 让越界早炸。
九、总结速查表
| 考点 | 一句话结论 |
|---|---|
| 本质 | 泛化的指针,算法与容器的桥 |
| 五类 | 输入/输出/前向/双向/随机访问 |
| 空区间 | begin == end,右开哨兵 |
| vector 失效 | 扩容全失效;插删点及之后 |
| 节点容器 | 插入不失效;删除仅被删点 |
| unordered | rehash 废迭代器、不废引用 |
| erase 惯用法 | it = c.erase(it) |
| range-for | 迭代器循环的糖,体内勿增删 |
| 只读遍历 | const_iterator / cbegin |
一句话回顾
迭代器是泛化的指针 ,按能力分五级,类别决定容器能吃哪些算法;失效规则一张表讲清------节点容器插入不失效、vector 扩容全失效、unordered rehash 只废迭代器不废引用;range-for 是糖,循环体内别增删容器。
如果您觉得本篇内容对你有帮助,欢迎点赞 👍、收藏 ⭐、转发 📢。下期我们继续标准库篇,聊常用算法------sort/find/copy/accumulate 这些天天用的家伙各有什么讲究,敬请关注 👋