【C++ 面试真题】19. 聊聊 C++ 的迭代器

【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,靠的就是"裸指针也满足迭代器要求"这一条。


三、与指针的关系:像、是、超越

❓ 迭代器和指针到底什么关系?

✅ 三层:

  1. :裸指针满足随机访问迭代器的全部要求,指针就是迭代器,算法通吃;
  2. :迭代器支持 *->++!=,用法与指针一致;
  3. 超越:迭代器是类对象时能干指针干不了的事------调试模式带边界检查、流迭代器把流包装成序列、插入迭代器把"赋值"变成"插入"。
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 这些天天用的家伙各有什么讲究,敬请关注 👋

相关推荐
AI情绪识别开源1 小时前
检信ALLEMOTION VibrationAI 2.4.0 12维度情绪识别开源源代码
开发语言·python
Dovis(誓平步青云)1 小时前
DevEco Studio 6.1.1 Windows 安装实录:从下载校验到首次启动
android·开发语言·数据库·人工智能·windows·harmonyos
qeen871 小时前
【数据结构】红黑树的算法原理解析与实现
开发语言·数据结构·c++·算法·红黑树
黄敬峰2 小时前
TypeScript 工具类型一次讲透:Pick、Omit、Partial、Record 逐个击破
面试
码匠许师傅2 小时前
【C++ 面试真题】20. 聊聊 C++ 的标准库常用算法
c++·算法·面试
Logintern092 小时前
什么时候应该用多进程什么时候用多线程呢?
开发语言·python
long3162 小时前
枚举(Enums)
java·开发语言·数据库
ShiXZ2133 小时前
网络调试四剑客:ping / telnet / nc / netstat 速查指令集
运维·开发语言·网络·php
码农大叔的博客3 小时前
golang示例:switch
开发语言·后端·golang