美团 C++开发 一面

1. 自我介绍

2. 介绍一下项目

3. 说一下智能指针,追问用法以及使用场景

答案:智能指针的核心价值是把资源释放时机和对象生命周期绑定起来,减少手动 new/delete 带来的泄漏、悬挂指针和异常路径遗漏问题。C++ 里最常见的是这几个:

  • std::unique_ptr:独占所有权,不能拷贝,可以移动
  • std::shared_ptr:共享所有权,靠引用计数管理
  • std::weak_ptr:弱引用,不增加引用计数,通常用来观察 shared_ptr
  • std::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 名字,重点是讲容器分类、底层结构和选型理由。常见容器大概可以分成几类:

  • 顺序容器:vectordequelist
  • 关联容器:mapset
  • 无序关联容器:unordered_mapunordered_set
  • 适配器:stackqueuepriority_queue

底层实现上常见结论是:

  • vector:动态连续数组
  • deque:分段连续存储
  • list:双向链表
  • map/set:红黑树
  • unordered_map/set:哈希表

比较时一般从这几个角度讲:

  • 是否连续存储
  • 是否支持随机访问
  • 插入删除复杂度
  • 查找复杂度
  • 迭代器稳定性
  • 内存局部性

工程上最常见的经验其实是:如果没有非常明确的理由,优先考虑 vectorunordered_map,因为它们在大多数真实业务里更符合性能直觉。

6. 迭代器失效是怎么回事?不同容器为什么表现不一样?

答案:这题很适合接在容器后面,因为它比单纯问容器分类更能看出你是不是真的用过 STL。迭代器失效,本质上是容器内部结构发生变化后,原来持有的位置、地址或节点关系已经不再可靠

不同容器表现不一样,核心原因在于底层结构不同:

  • vector 扩容后可能整块搬迁,所以原来的迭代器、指针、引用都可能失效
  • deque 因为是分段存储,插入删除后规则比 vector 更复杂
  • list 节点独立分配,插入删除通常只影响被删节点本身,其他迭代器更稳定
  • unordered_map rehash 后,桶结构变化,迭代器通常会失效

代码:

#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++ 常考面试题总结 文章被收录于专栏

本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务