京东 C++ 一面,聊了一个小时,问得又广又杂
投的是京东基础架构部门的 C++ 后端开发岗,一面是电话面试,面试官语速很快,上来先聊了十分钟背景,然后直接进技术。
整体风格是广度优先,覆盖面很宽,C++ 基础、内存管理、并发、网络、项目都有,但每道题不会问得特别深,感觉是在快速扫描知识面。项目聊了将近二十分钟,他对项目里用到的技术选型很感兴趣,问了不少为什么这么选。最后有一道手撕,难度不高,时间也够。
总时长刚好一个小时,整体节奏很快,没有太多思考时间。
1. 自我介绍,介绍一下你的实习经历和做过的项目
2. 你提到用过消息队列,为什么要引入 MQ,解决了什么问题,选型上为什么选这个而不是其他的?
答:引入 MQ 主要解决三个问题:解耦、异步、削峰。
解耦方面,原来服务 A 直接调用服务 B,B 挂了 A 也受影响,引入 MQ 后 A 只管发消息,B 什么时候消费和 A 无关,两者独立。
异步方面,有些操作不需要同步等待结果,比如发短信、发邮件、更新统计数据,放到 MQ 里异步处理,主流程响应速度更快。
削峰方面,突发流量先进队列缓冲,消费者按自己的处理能力消费,保护下游不被打垮。
选型上,如果对吞吐量要求高、消息量大,选 Kafka,它的顺序写和零拷贝让吞吐量极高,但消息延迟相对高一点,适合日志收集、数据管道。如果对消息可靠性和功能要求高,比如需要消息路由、死信队列、延迟消息,选 RabbitMQ 或者 RocketMQ。RocketMQ 是阿里开源的,在电商场景下经过大规模验证,支持事务消息,适合业务消息场景。
3. 讲一下 C++ 的智能指针,shared_ptr 的引用计数是怎么实现的,循环引用怎么解决?
答:C++ 有三种智能指针。unique_ptr 独占所有权,不可复制只能移动,零开销,是最常用的。shared_ptr 共享所有权,内部维护引用计数,复制时计数加一,析构时减一,减到零释放资源。weak_ptr 不增加引用计数,用于打破循环引用。
shared_ptr 的引用计数实现:shared_ptr 内部有两个指针,一个指向管理的对象,一个指向控制块。控制块里存放引用计数(强引用计数)和弱引用计数,以及自定义删除器。引用计数的增减是原子操作,保证多线程安全。make_shared 把对象和控制块分配在同一块内存,减少一次内存分配,也提高缓存局部性。
循环引用问题:A 持有 B 的 shared_ptr,B 持有 A 的 shared_ptr,两者引用计数永远不为零,内存泄漏。解决方法是把其中一个改为 weak_ptr,weak_ptr 不增加引用计数,使用时调用 lock() 升级为 shared_ptr,如果对象已销毁返回空指针。典型场景是父子节点互相引用,子节点持有父节点的 weak_ptr,父节点持有子节点的 shared_ptr。
4. 讲一下 C++ 的内存布局,栈和堆的区别,内存泄漏怎么排查?
答:C++ 程序的内存布局从低地址到高地址大致是:代码段(存放程序指令)、数据段(已初始化的全局变量和静态变量)、BSS 段(未初始化的全局变量,程序启动时清零)、堆(动态分配的内存,向高地址增长)、栈(函数调用栈,向低地址增长)。
栈和堆的区别:栈由编译器自动管理,函数调用时分配,函数返回时释放,速度快,但空间有限(通常几 MB),不能存放大对象。堆由程序员手动管理(new/delete 或 malloc/free),空间大,但分配释放有开销,容易出现内存泄漏和碎片化。
内存泄漏排查:
代码审查:检查每个 new 是否有对应的 delete,异常路径是否会跳过 delete,优先用智能指针代替裸指针。
Valgrind:valgrind --leak-check=full ./program,能详细报告泄漏位置和分配时的调用栈,但运行速度慢很多。
AddressSanitizer:编译时加 -fsanitize=address,运行时检测泄漏、越界、use-after-free,性能开销比 Valgrind 小,开发阶段可以长期开着。
监控内存占用:线上环境通过 /proc/pid/status 的 VmRSS 字段监控进程内存,持续增长说明有泄漏。
5. 讲一下多线程中的锁,std::mutex 和 std::shared_mutex 的区别,什么时候用读写锁?
答:std::mutex 是互斥锁,同一时刻只有一个线程可以持有,不区分读写,所有操作都互斥。
std::shared_mutex 是读写锁,支持两种模式:共享模式(读锁)允许多个线程同时持有,独占模式(写锁)同一时刻只有一个线程可以持有,且持有写锁时不允许任何读锁。
使用方式:读操作用 std::shared_lock<std::shared_mutex>,写操作用 std::unique_lock<std::shared_mutex>,都是 RAII 风格,自动释放。
什么时候用读写锁:读多写少的场景,比如配置信息、缓存数据,大量线程频繁读取,偶尔有线程更新。用普通互斥锁的话,读操作之间也互斥,并发度低。用读写锁,读操作可以并发,只有写操作才需要独占,提高并发度。
注意事项:读写锁本身比互斥锁重,如果读写比例不悬殊,或者临界区很短,读写锁的开销可能反而更大,需要实际测量。另外要注意写饥饿问题,如果读请求持续不断,写请求可能一直等不到机会,不同实现的策略不同。
6. 讲一下 std::atomic 和 volatile 的区别,原子操作是怎么实现的?
答:volatile 告诉编译器不要优化对该变量的读写,每次都从内存读取,不缓存在寄存器里。它解决的是编译器优化问题,不解决多线程可见性问题,不提供原子性,不防止 CPU 指令重排。在嵌入式里用于内存映射寄存器,在 PC 端多线程场景基本没有正确的使用场景。
std::atomic 提供原子操作,保证操作的不可分割性,同时提供内存序保证,防止编译器和 CPU 重排。多线程场景下共享变量应该用 std::atomic 而不是 volatile。
原子操作的实现:在 x86 架构上,简单的读写操作天然是原子的(对齐的 32/64 位读写)。复合操作(比如 fetch_add)用 前缀指令实现, 前缀让 CPU 在执行这条指令期间锁住总线或缓存行,保证原子性。在 ARM 等弱内存序架构上,用 / 指令实现 LL/SC(Load-Linked/Store-Condit
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。