腾讯 微信客户端-C++ 一面
1、TCP 和 UDP 在微信客户端里一般分别用在哪些场景
答案:像聊天消息、图片消息、文件传输、登录态同步这类场景,核心诉求是可靠、有序、不能乱不能丢,所以更适合基于 TCP 的长连接体系。哪怕中间有重传和拥塞控制带来的额外开销,也必须保证最终一致和消息正确到达。
而音视频通话这类实时互动场景,重点通常不是每一帧都绝对到达,而是延迟要低、抖动要小、卡了能尽快恢复,所以更适合 UDP 或基于 UDP 进一步封装的传输协议。因为对实时媒体来说,晚到的数据往往比丢掉的数据更没价值。
所以在微信这类客户端里,两者是按业务目标拆分的。强一致消息链路偏 TCP,实时媒体链路偏 UDP。
2、了解 QUIC 协议吗,它和 TCP 的核心差别是什么
答案:QUIC 可以理解成是在 UDP 之上实现的一套可靠传输协议,它把连接管理、重传、拥塞控制这些原本更多在内核 TCP 里的事情,搬到了用户态协议层里。HTTP/3 底层就是基于 QUIC。
它跟 TCP 一个很大的区别,是它天然把传输层和 TLS 更紧地结合在一起,建连时延更低,而且多路复用时不会像 TCP 那样因为一个流丢包把所有流都卡住,也就是常说的队头阻塞问题在连接内不同流之间被缓解了。
QUIC 还支持连接迁移,在移动端网络切换时更自然一些,比如 Wi-Fi 切到移动网络时,连接不一定要像 TCP 那样重新建立。
3、如果 A 给 B 发微信消息,B 十分钟后才收到,已知链路日志很全,你会怎么排查
答案:我会直接按链路分段看时间戳。先看 A 侧消息生成时间、发送时间、真正写入 socket 的时间,再看服务端接收、入队、路由、转发、下发的时间,最后看 B 侧收到数据包的时间、进入协议栈的时间、应用层处理时间、消息最终展示时间。
如果日志已经细到函数级别,那就很好定位了。比如 B 侧 recv 很早就收到了,那问题就不在网络,而更可能在客户端上层处理链路,比如消息分发线程卡住、事件循环没及时调度、数据库写入阻塞、UI 刷新链路积压。如果 recv 本身就是十分钟后才发生,那再往下看 socket 读事件为什么没被及时处理,是 epoll 事件没起来,线程没被调度,还是连接状态本身有异常。
排查时会把延迟切成三段:A 到服务端、服务端内部、服务端到 B。先确认延迟落在哪一段,再继续往下追。
4、说一个你熟悉一点的数据结构
答案:我会讲哈希表。哈希表的核心是把 key 经过哈希函数映射到桶位置上。映射时常见做法是取模,如果桶大小设计成 2 的幂,也可以用位运算加速。
冲突处理一般有开放定址和链地址法两种。开放定址对缓存更友好,但装载因子高了以后探查会越来越差,还容易出现扎堆。链地址法删除方便,扩容思路也清晰,但链太长时查找退化明显,而且节点分散,cache locality 不好。
如果继续往实现里看,性能差异很多时候不只在复杂度,还在内存布局、探查策略和缓存命中率上。
5、unordered_map 扩容和 rehash 的时候,哪些东西会失效
答案:unordered_map 底层是哈希桶,rehash 的时候元素会根据新的桶数量重新分布,所以原来的桶位置基本都会变化。这个过程中,迭代器通常会失效,因为它依赖的是原来的桶组织结构。
工程里比较稳妥的做法是,只要发生了 rehash,就不要继续使用之前缓存下来的迭代器。尤其是在边遍历边插入的代码里,这类问题很隐蔽。
如果代码依赖元素地址或者引用,也应该谨慎处理,避免在容器结构变化后继续使用旧状态。
6、红黑树和 AVL 树有什么区别,为什么很多库更偏向红黑树
AVL 树追求的是更严格的平衡,所以查找高度通常更理想;红黑树平衡要求没那么严格,但插入删除时旋转和调整成本往往更低。
红黑树在频繁插入删除的综合场景下更稳,维护成本更低。AVL 树在查找密集、更新较少时会更有优势,但如果操作模式里改动很多,它保持严格平衡的代价就会更明显。
所以两者差异主要在平衡严格程度和更新代价上。AVL 更追求查找效率,红黑树更适合动态更新场景。
7、如果哈希桶上链太长了,会带来什么问题,怎么优化
链太长最直接的问题就是查找退化,平均 O(1) 会慢慢往线性靠。而且链表节点离散分布在堆上,CPU 访问时要频繁跳指针,cache miss 会很多,实际性能往往比纸面复杂度更差。
优化思路一方面是从哈希函数和扩容策略入手,让 key 分布尽量均匀,避免热点桶太集中;另一方面是改桶内结构,比如链过长以后换成树结构,或者改成更适合缓存的开放定址实现。
8、你做过什么项目
答案:我做过一个基于 Qt、protobuf、ASIO、SQLite 的跨端离线消息同步客户端。这个项目的重点在消息链路和本地数据一致性上。客户端和服务端之间维持长连接,消息体用 protobuf 编解码,本地用 SQLite 做会话索引和消息落盘,支持断线重连、离线补拉、消息去重和已读状态同步。
我主要做了几块:一块是消息收发链路,把网络线程、协议解析、业务分发和 UI 通知拆开,避免主线程被 IO 逻辑拖住;一块是本地存储层,处理消息顺序、重复插入和批量写入;还有一块是弱网重连和补偿机制,比如连接恢复后根据序列号补消息,而不是简单全量拉取。
这个项目里比较难的是消息顺序和幂等处理,因为客户端本地已经有一部分历史消息,重连之后还会收到服务端补发的数据,需要把展示顺序、落库顺序和去重规则统一起来。
9、智能指针怎么理解,shared_ptr 的控制块里一般有什么
答案:智能指针本质上是在做资源所有权管理,把什么时候释放这件事从手动 delete 变成更明确的语义控制。
unique_ptr 是独占所有权,一个资源只归一个对象管理。shared_ptr 是共享所有权,多个地方共同持有同一个对象,底层一般会有一个控制块去维护强引用计数、弱引用计数,还有删除器之类的信息。强引用计数归零时,对象本身会释放;弱引用归零后,控制块才会最终销毁。weak_ptr 不拥有对象,只是观察者,用来解决循环引用和旁路访问问题。
代码:
#include <iostream>
#include <memory>
using namespace std;
struct Node {
~Node() { cout << "destroy Node\n"; }
};
int main() {
shared_ptr<Node> p1 = make_shared<Node>();
weak_ptr<Node> wp = p1;
{
shared_ptr<Node> p2 = p1;
cout << p1.use_count() << endl;
}
cout << p1.use_count() << endl;
p1.reset();
if (auto sp = wp.lock()) {
cout << "alive\n";
} else {
cout << "expired\n";
}
}
10、内存泄漏一般怎么排查
答案:内存泄漏排查要
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.