美团 C++开发 二面
1. 自我介绍
2. 说一下 C++ 里的多态,虚函数表是怎么工作的?
答案: 如果只答“多态就是父类指针指向子类对象”,会比较浅。 更完整的回答应该分成两部分:语言层面的多态,以及底层实现。
C++ 里常说的多态主要是运行时多态,也就是:
- 基类定义虚函数
- 子类重写虚函数
- 通过基类指针或引用调用时,实际执行子类版本
底层上通常依赖虚函数表 vtable 和 虚函数表指针 vptr。 对象里通常会有一个隐藏的 vptr,指向该类型对应的虚表。调用虚函数时,程序先通过对象找到虚表,再从虚表里取出对应函数地址完成动态分发。
多态的优点是扩展性好,适合做接口抽象、框架回调、策略替换。 代价主要有:
- 多一次间接调用
- 对象里通常要多一个 vptr
- 某些场景不利于内联优化
代码:
#include <iostream>using namespace std;class Base {public: virtual void run() { cout << "Base\n"; } virtual ~Base() = default;};class Derived : public Base {public: void run() override { cout << "Derived\n"; }};int main() { Base* p = new Derived(); p->run(); // 运行时多态 delete p;}
3. 项目拷打
4. 如果我有一堆正整数需要频繁插入和删除,并且还要知道第几大的数,应该用什么数据结构?为什么?
答案: 如果需要同时支持:
- 动态插入
- 动态删除
- 查询第 k 大
那比较合适的数据结构通常是:
- 带顺序统计信息的平衡二叉搜索树
- 或者离散化后用 树状数组 / 线段树
- 如果数据范围固定且不大,用桶或计数结构也可以
为什么普通堆不够? 因为堆擅长维护最值,但不擅长支持任意删除和第 k 大查询。 为什么 set / multiset 也不完全够? 因为标准库里的 set 虽然能维护有序,但不能直接高效查询“第 k 大”,除非你自己额外维护秩信息。
如果值域很大、数据动态变化很多,工程上更常见思路是:
- 用平衡树节点维护子树大小
- 插入删除时更新 size
- 查询第 k 大时根据右子树节点数递归判断
5. 如果值域已知而且很大,做第 k 大查询时为什么有时线段树比平衡树更合适?
答案: 如果值域已知,比如数字都落在 [1, 10^7] 或经过离散化后范围明确,那么线段树或树状数组会很有优势。
原因主要有:
- 它们天然适合做频次统计
- 插入删除本质上就是单点加减
- 第 k 大查询可以通过区间计数往下二分
- 复杂度稳定,不依赖随机数据分布
相比之下,平衡树更适合:
- 值域非常分散
- 不方便离散化
- 需要保留原始有序结构
线段树的代价则是:
- 值域过大时空间压力大
- 实现复杂度比直接用 STL 高
- 如果是动态开点,工程复杂度会进一步上升
6. 现在有四个连接请求,前面三个已经收到了,需要把第四个拼起来,要求高并发,应该怎么做?
答案: 如果一个完整请求被拆成多次到达,那么服务端不能假设每次 recv 就是一个完整包,而应该:
- 为每个连接维护独立接收缓冲区
- 收到数据后追加到缓冲区
- 根据协议头或长度字段判断一个完整包是否已经到齐
- 到齐后切包处理,剩余数据继续保留
高并发下比较关键的点有:
- 缓冲区不能频繁扩容和拷贝
- 协议设计最好有固定头部或长度字段
- 一个连接一个状态机,不要把多个连接数据混在一起
- 解析和业务处理尽量解耦,避免 IO 线程被重逻辑阻塞
比较典型的做法是:
- IO 线程负责收包和分包
- 完整消息投递到工作线程池
- 每个连接维护 read buffer 和 parse state
代码:
struct Conn { std::string buffer;};void onRead(Conn& c, const char* data, size_t len) { c.buffer.append(data, len); while (true) { if (c.buffer.size() < 4) return; // 假设前4字节表示包长 uint32_t msgLen = *reinterpret_cast<const uint32_t*>(c.buffer.data()); if (c.buffer.size() < 4 + msgLen) return; std::string msg = c.buffer.substr(4, msgLen); handleMessage(msg); c.buffer.erase(0, 4 + msgLen); }}
7. Reactor 和 Proactor 有什么区别?如果是高并发连接场景你会怎么选?
答案: 两者的核心区别在于:
- Reactor:内核通知“你可以读/写了”,真正的读写由应用自己做
- Proactor:应用先提交异步 IO 请求,内核完成后通知“已经做完了”
在 Linux 常见服务端开发里,更多接触到的是 epoll + 非阻塞 socket,这更偏 Reactor 模型。 原因很现实:
- 生态成熟
- 编程模型清晰
- 和线程池、状态机结合方便
- 在高并发连接场景里非常常见
8. 现在有一个很大的数需要得到相乘之后的结果,使用 int 肯定溢出,怎么做?思路以及时间复杂度?
答案: 最直接的思路就是字符串模拟大整数乘法。 可以把两个数字按字符存储,然后模拟我们手算乘法的过程:
- 从最低位开始逐位相乘
- 把乘积累加到结果数组的对应位置
- 最后统一处理进位
- 去掉前导零后输出结果
如果两个数长度分别是 n 和 m,那么这种做法的时间复杂度通常是:
- 时间复
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.