美团 C++开发 一面
1. 自我介绍
2. 介绍一下项目
3. 说一下智能指针,追问用法以及使用场景
答案:智能指针的核心价值是把资源释放时机和对象生命周期绑定起来,减少手动 new/delete 带来的泄漏、悬挂指针和异常路径遗漏问题。C++ 里最常见的是这几个:
std::unique_ptr:独占所有权,不能拷贝,可以移动std::shared_ptr:共享所有权,靠引用计数管理std::weak_ptr:弱引用,不增加引用计数,通常用来观察shared_ptrstd::auto_ptr:已经废弃,不建议再提
使用场景上,一般这样区分:
- 明确唯一拥有关系时,优先用
unique_ptr - 多个对象确实要共享同一资源时,用
shared_ptr - 需要打破循环引用,或者只是临时观察对象存活状态时,用
weak_ptr
工程上比较好的习惯是:
- 能不用
shared_ptr就尽量不用 - 不要把所有指针都机械地替换成智能指针
- 智能指针管理的是所有权,不是访问权限
代码:
#include <memory>
class Widget {};
int main() {
std::unique_ptr<Widget> p1 = std::make_unique<Widget>();
std::shared_ptr<Widget> p2 = std::make_shared<Widget>();
std::weak_ptr<Widget> wp = p2;
if (auto sp = wp.lock()) {
// 对象还活着,可以安全使用
}
}
4. 如果 shared_ptr 发生循环引用怎么办?enable_shared_from_this 又是解决什么问题的?
答案:这是智能指针里非常高频的追问。shared_ptr 的引用计数机制只能处理“没有环”的所有权结构,一旦两个对象互相持有 shared_ptr,它们的引用计数都不会归零,对象就不会释放。
典型解决方案是:
- 其中一边改成
weak_ptr - 明确谁拥有谁,谁只是观察者
- 不要为了图省事把成员全写成
shared_ptr
enable_shared_from_this 解决的是另一个常见问题:如果一个对象本身已经被 shared_ptr 管理,但你在对象内部又想安全地拿到“指向自己的 shared_ptr”,不能直接 shared_ptr<this>,否则会构造出新的控制块,最终可能 double free。正确方式是继承 std::enable_shared_from_this<T>,然后通过 shared_from_this() 拿到和外部共享同一控制块的智能指针。
代码:
#include <memory>
class B;
class A {
public:
std::shared_ptr<B> b;
};
class B {
public:
std::weak_ptr<A> a; // 用 weak_ptr 打破环
};
class Session : public std::enable_shared_from_this<Session> {
public:
std::shared_ptr<Session> getSelf() {
return shared_from_this();
}
};
5. 说一下 C++ 中的容器,追问底层实现以及容器间的对比
答案:这题不要只是背 STL 名字,重点是讲容器分类、底层结构和选型理由。常见容器大概可以分成几类:
- 顺序容器:
vector、deque、list - 关联容器:
map、set - 无序关联容器:
unordered_map、unordered_set - 适配器:
stack、queue、priority_queue
底层实现上常见结论是:
vector:动态连续数组deque:分段连续存储list:双向链表map/set:红黑树unordered_map/set:哈希表
比较时一般从这几个角度讲:
- 是否连续存储
- 是否支持随机访问
- 插入删除复杂度
- 查找复杂度
- 迭代器稳定性
- 内存局部性
工程上最常见的经验其实是:如果没有非常明确的理由,优先考虑 vector 或 unordered_map,因为它们在大多数真实业务里更符合性能直觉。
6. 迭代器失效是怎么回事?不同容器为什么表现不一样?
答案:这题很适合接在容器后面,因为它比单纯问容器分类更能看出你是不是真的用过 STL。迭代器失效,本质上是容器内部结构发生变化后,原来持有的位置、地址或节点关系已经不再可靠。
不同容器表现不一样,核心原因在于底层结构不同:
vector扩容后可能整块搬迁,所以原来的迭代器、指针、引用都可能失效deque因为是分段存储,插入删除后规则比vector更复杂list节点独立分配,插入删除通常只影响被删节点本身,其他迭代器更稳定unordered_maprehash 后,桶结构变化,迭代器通常会失效
代码:
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4); // 可能触发扩容
// it 这里可能已经失效
}
7. lambda 表达式
答案:Lambda 本质上是一个匿名函数对象,编译器会为它生成一个闭包类型。它最常见的价值是让一些“就地定义、短小逻辑”的代码更简洁,特别适合:
- 回调
- 算法配合
- 局部策略封装
- Qt 或异步框架里的短逻辑处理
一个 lambda 主要包括几部分:
- 捕获列表
- 参数列表
- 返回值
- 函数体
最值得讲的点通常是捕获方式:
- 值捕获:复制一份外部变量
- 引用捕获:直接引用外部变量
[=]、[&]:默认捕获mutable:允许修改值捕获的副本
高分点一般在于补一句:lambda 不是“只是语法糖”,它背后是一个有 operator() 的对象,所以能和模板、泛型算法、函数对象体系很好地结合。
代码:
#include <iostream>
int main() {
int x = 10;
auto f = [x](int y) { return x + y; };
std::cout << f(5) << std::endl;
}
8. lambda 和 std::function 的关系是什么?std::function 为什么有额外开销?
答案:这题放在 lambda 后面很合适,因为很多人只会写 lambda,但说不清它和 std::function 的关系。lambda 本质上是一个具体类型的函数对象,而 std::function 是一个通用可调用对象包装器,它可以装:
- 普通函数
- lambda
- 仿函数
bind的结果- 成员函数适配后的对象
std::function 的优势是统一接口、使用方便,但代价通常也更高,原因主要有:
- 需要做类型擦除
- 可能发生额外的对象封装
- 某些情况下可能触发堆分配
- 内联优化机会可能变差
所以如果你在高频调用、性能敏感路径里,通常更推荐:
- 直接用模板接收可调用对象
- 或者直接保存具体 lambda 类型
- 只有在确实需要统一抽象时再用
std::function
代码:
#include <functional>
#include <iostream>
void run(const std::function<int(int)>& f) {
std::cout << f(10) << std::endl;
}
int main() {
int base = 5;
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.
查看12道真题和解析