腾讯 微信客户端-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++ 常考面试题总结 文章被收录于专栏

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

全部评论
我感觉你很适合我们团队,愿意试试多多吗,看我住叶,进度随时帮看 。面试官就坐我旁边
点赞 回复 分享
发布于 昨天 20:19 上海

相关推荐

评论
点赞
2
分享

创作者周榜

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