第3章 技术面试:C/C++高频考点:指针与内存、虚函数、多态、STL
本章聚焦C/C++面试中最高频的三大模块:指针与内存管理、虚函数与多态、STL容器与算法。面试官常通过追问底层实现、内存布局和异常场景来考察你的“C++工程理解深度”。下面先给出考点全景总览,再逐一拆解典型问答。
| 考点类别 | 核心考察点 | 典型追问方向 |
|---|---|---|
| 指针与内存 | 智能指针原理、内存泄漏、野指针、RAII | weak_ptr如何解决循环引用?new/delete vs malloc/free |
| 虚函数与多态 | 虚表指针、动态绑定、纯虚函数、构造/析构调用虚函数 | 为什么构造函数不能是虚函数?析构函数为何要虚? |
| STL容器与算法 | 容器特性、迭代器失效、reserve vs resize、sort实现 | vector扩容机制、map底层红黑树 vs unordered_map哈希表 |
面试问答卡片
1. 请比较 shared_ptr 与 unique_ptr,它们如何管理内存?
面试官考察点: 智能指针底层原理、RAII思想、引用计数实现、性能与所有权语义。
回答框架: 首先指出两者都是RAII类,自动释放资源。接着说 unique_ptr 独占所有权,只可移动不可复制;shared_ptr 共享所有权,通过引用计数管理。然后提一句底层控制块实现。最后强调使用场景区别。
示例回答: “unique_ptr 独占所有权,只允许 move,性能接近裸指针;shared_ptr 通过控制块维护引用计数,支持复制,但额外代价是原子操作。我通常在明确唯一所有权时用 unique_ptr,多对象共享资源时用 shared_ptr,并用 weak_ptr 打破循环引用。”
可能追问: weak_ptr 如何解决循环引用?控制块默认存放在哪?自定义删除器如何传入?
2. 为什么 C++ 中析构函数通常要声明为虚函数?
面试官考察点: 多态对象析构的正确性、虚函数表机制、对象切片风险。
回答框架: 先说明基类指针指向派生类对象时,非虚析构只调用基类析构,导致派生类资源泄漏。然后指出虚析构会通过虚表动态绑定到正确析构函数。最后补充如果类不打算作为基类,则不用虚析构以避免增加虚表开销。
示例回答: “当通过基类指针删除派生类对象时,如果析构函数非虚,只会调用基类的析构,派生类成员可能泄漏。声明为 virtual 后,通过 vptr 找到正确析构函数,实现完整销毁。注意:如果类不会被继承,就没必要加虚析构,避免额外开销。”
可能追问: 构造函数为什么不能是虚函数?虚析构函数在继承链中的调用顺序?纯虚析构函数怎么定义?
3. vector 的 push_back 可能导致迭代器失效,具体什么情况?如何避免?
面试官考察点: STL容器内存重分配机制、迭代器失效类型、reserve 优化、移动语义。
回答框架: 先说明 vector 在空间满时会重新分配更大内存,导致原有迭代器、指针、引用全部失效。然后给出避免方法:提前 reserve 足够空间,或改用索引而非迭代器。最后补充插入/删除操作也会使部分迭代器失效。
示例回答: “push_back 当 size == capacity 时会 reallocate,原迭代器指向旧内存,全部失效。我一般用 reserve 预分配容量;如果必须用迭代器,改用索引或等到所有插入操作完成后再获取新迭代器。注意 erase 会返回下一个有效迭代器。”
可能追问: vector 的扩容因子是多少(通常 1.5 或 2)?resize 和 reserve 区别?其他容器迭代器失效情况如何?
项目案例:基于智能指针的多线程任务调度系统
| 项目名称 | TaskFlow — 基于 shared_ptr / weak_ptr 的任务依赖调度器 |
| 业务背景 | IOT边缘计算节点需并发执行多个传感器数据解析、推理与上传任务,任务间存在依赖(如解析完成后才能推理),需动态管理任务生命周期,避免内存泄漏。 |
| 规模指标 | 支撑 1000+ 任务节点并行调度,内存占用 < 200MB,任务平均延迟 < 50ms。 |
| 技术栈 | C++17、std::shared_ptr / weak_ptr、std::thread 池、std::function、std::unordered_map、std::vector。 |
| 核心难点 | 1) 任务间循环依赖导致 shared_ptr 循环引用,内存泄漏;2) 任务执行时动态添加新依赖,需避免迭代器失效;3) 线程安全地修改任务状态。 |
| 面试高频追问 | - 你如何发现循环引用?用了什么工具(如 valgrind)? - weak_ptr 的 lock() 返回空时如何处理? - 任务容器你用了 vector 还是 list?为什么? - 线程池如何避免任务对象析构时仍被线程执行? |
| 可迁移回答话术 | “我在项目中通过将父任务持有子任务的 weak_ptr,子任务持有父任务的 shared_ptr,打破了循环。同时用 shared_from_this() 确保任务对象在回调中生存期合理。对任务容器提前 reserve 避免动态扩容导致迭代器失效,再结合生产者-消费者队列保证线程安全。” |