面灵AI→

光子精密 C++ 面经:智能指针、vector 扩容与线程同步

时间
2026-10
来源
牛客网

《面试题目》

  1. 介绍一下三种不同的智能指针。
  2. 能否用 shared_ptr 去管理 Qt 对象?
  3. vector 在什么时候扩容、怎样扩容?
  4. vector 除了析构函数还有什么方法可以释放掉?
  5. 条件变量是怎么实现线程之间同步的?
  6. C++ 通用线程池是怎么实现的,大概的流程?
  7. lambda 表达式和普通函数在使用上有哪些需要注意的地方?
  8. TCP 和 UDP 的区别?
  9. TCP 怎样保证可靠性?
  10. 为什么常见关闭连接要四次交互?
  11. 死锁是什么?怎样避免?

《参考解析》

智能指针与 Qt 对象所有权:unique_ptr 独占所有权、不可拷贝只可移动、零额外开销,是默认选择;shared_ptr 共享所有权,靠控制块里的强引用计数决定何时析构,代价是原子操作与一块额外内存;weak_ptr 不增加强引用计数,用 lock() 提升后访问,专门打破循环引用。用 shared_ptr 管 Qt 对象则是典型踩坑:QObject 自带父子对象树,父对象析构时会 delete 所有子对象,再套一层 shared_ptr 就有两套所有权同时认为自己该释放——父子树删过一次、计数归零再删一次,直接崩溃。正确做法是二选一:放进对象树就用裸指针或 QPointer 观察,需要共享所有权就用 Qt 自己的 QSharedPointer;确实要让 shared_ptr 管理 QObject 时必须传一个不删对象的自定义删除器。另外不要从裸 this 构造 shared_ptr,否则会生成两个控制块导致二次释放。

vector 的扩容与释放:要区分 size 和 capacity。vector 元素数量达到 capacity() 时触发扩容,主流实现按 1.5 倍或 2 倍申请新内存(MSVC 1.5 倍,libstdc++ 2 倍),把旧元素移动或拷贝过去再释放旧内存——所以扩容是摊还 O(1),但单次扩容有 O(n) 拷贝,已知规模时先 reserve 可以彻底避免。释放内存的常见手段:clear() 只析构元素、不改 capacity;shrink_to_fit() 是请求(非强制)把 capacity 收到 size;C++11 前的老写法是 vector<T>(v).swap(v),用临时对象构造出正好大小的容器再交换;如果只想让某块大内存立刻归还,vector<T>().swap(v) 最干脆。另外要提两类隐蔽问题:erase 之后原迭代器与引用失效,循环删除要用返回值接住新迭代器;容器里存的是对象时,扩容会触发移动构造,所以类型最好满足 noexcept 的移动语义,否则 vector 会退化成拷贝。

条件变量与线程池:把「等待—通知—退出」三件事讲完整。条件变量必须和互斥量配合使用,标准写法是「加锁—wait(lock, predicate) 循环判断条件—被唤醒后重新检查」;用带谓词的 wait 可以自动处理虚假唤醒,用裸 wait 则必须自己写 while。常见错误是通知方没持锁就修改条件(导致丢唤醒)和忘记在退出时 notify_all(导致工作线程永久阻塞)。线程池的通用结构是:一个任务队列(std::queue<std::function<void()>>)加一把互斥量、一组工作线程、一个条件变量和一个停止标志;提交任务时加锁入队并 notify_one,工作线程循环「取任务—执行—回到等待」,析构时置停止标志并唤醒所有线程 join。更完整的版本会用 std::packaged_task 加 std::future 让提交方拿到返回值,用固定数量的线程绑定 CPU 核心数,并考虑任务优先级、异常捕获(线程里抛异常会直接终止进程)和队列容量上限。

lambda 与普通函数:差异集中在捕获与类型上。lambda 本质是编译器生成的匿名函数对象,捕获列表决定它的成员变量:按值捕获会把变量拷贝进去(默认 const,要改就加 mutable),按引用捕获不延长生命周期——异步执行或存进回调队列后原变量已销毁,就是悬垂引用,这是最常见的线上事故来源。[&] 和 [=] 要慎用,尤其是类成员函数里 [=] 捕获的是 this 指针而不是对象副本,对象析构后再调用同样崩。类型上,lambda 的闭包类型独一无二、没有名字,所以需要存储时只能转成 std::function(有类型擦除与可能的堆分配开销)或作为模板参数传递;普通函数指针虽然轻,但捕获不了上下文。另外 lambda 不能递归调用自身(除非用 std::function 加自引用),不能有模板参数(C++20 之前),这些在写比较器、回调、异步任务的场景里都会碰到。

TCP 三问要串成一条逻辑线:可靠性、连接管理、死锁避免。 TCP 与 UDP 的区别落到四点:面向连接 vs 无连接、可靠有序 vs 尽力而为、字节流(无消息边界)vs 数据报、头部与拥塞控制带来的开销差异。TCP 的可靠性由序号与确认、超时重传、快速重传、滑动窗口(流量控制)、拥塞控制(慢启动、拥塞避免、快重传、快恢复)和校验和共同保证。四次挥手的原因是 TCP 全双工:主动方发 FIN 只表示「我不再发数据」,被动方还要把剩余数据发完才发自己的 FIN,加上 ACK 与 FIN 通常不能合并(延迟确认与半关闭状态),所以是四次;TIME_WAIT 要等 2MSL 是为了让最后的 ACK 有机会重传、并让旧连接的迷路报文在网络中消散。死锁的四个必要条件与本篇前面提到的破坏手段一致,放到多线程语境里再补一条:优先使用 RAII 锁(lock_guard、unique_lock)加 std::lock 一次性锁定多个互斥量,避免手动加解锁漏掉某条异常路径。

这场面试的复盘建议:八股要「讲到原理,再落一个例子」。题面全是教科书级问题,说明面试官看的是表达深度而不是知识广度。作者自述「问题都会但答得笼统、没说出技术要点」,改进办法是把每道题压成四句话的模板:它是什么(定义)、怎么实现的(机制,如 shared_ptr 的控制块、vector 的 1.5/2 倍扩容、条件变量配互斥量)、边界与代价是什么(循环引用、迭代器失效、虚假唤醒与丢唤醒)、我遇到过的一个具体场景(哪次线上问题或项目里用它解决了什么)。练的时候对着手机录音,控制在 60 到 90 秒讲完一道题,超时说明还没提炼出主干。另外像「能否用 shared_ptr 管 Qt 对象」这类跨知识点问题,考察的是所有权意识,遇到完全没接触过的领域(本次的 Qt)可以坦白没学过,但要把已知的通用原理讲出来,并说明会怎么去查证,这比沉默或硬答更加分。