联想 C++ 二面,聊了一个半小时差点没撑住

二面,还是视频面试,这次面试官换了一个人,自我介绍说是做系统软件方向的高级工程师。

和一面相比节奏明显不一样,问题少但每道都很深,基本上每个话题都会顺着你的回答继续挖,感觉整场面试就是在不断被追问。项目聊了将近三十分钟,他对架构设计和遇到的问题非常感兴趣,问了很多"为什么这么做"和"有没有更好的方案"。后半段有一道系统设计题,没有手撕算法,但设计题聊了很久。

整体强度比一面高不少,面完感觉脑子空了。

1. 讲一下你对 C++ 对象内存布局的理解,虚继承是怎么解决菱形继承问题的?

答:一个普通 C++ 对象的内存布局,从低地址到高地址依次是:如果有虚函数,最开头是虚指针(vptr),然后是按声明顺序排列的成员变量,编译器可能在成员之间插入填充字节来满足对齐要求。

继承的情况下,派生类对象的内存里先放基类的部分,再放派生类自己的成员。如果基类有虚指针,派生类通常复用这个虚指针,指向派生类自己的虚函数表。

菱形继承的问题是:D 继承自 B 和 C,B 和 C 都继承自 A,那么 D 的对象里会有两份 A 的成员,一份来自 B,一份来自 C,访问 A 的成员时编译器不知道用哪份,产生歧义,而且内存里有重复数据。

虚继承解决这个问题的方式是:B 和 C 用虚继承的方式继承 A,这样 D 的对象里只有一份 A 的成员,叫做虚基类子对象,放在内存布局的某个固定位置。B 和 C 各自通过一个虚基类指针(vbptr)或者虚基类偏移表来找到这份共享的 A。代价是内存布局更复杂,访问虚基类成员需要多一次间接寻址,有一定性能开销。实际项目里菱形继承本身就是设计上的坏味道,能避免就避免。

2. 模板的实例化是什么时候发生的?模板的编译期计算你了解吗?

答:模板本身不是代码,是生成代码的模板。模板实例化发生在编译期,当编译器遇到具体的模板使用时,用实际的类型参数替换模板参数,生成对应的具体代码。每种不同的类型参数组合都会生成一份独立的代码,这叫做模板实例化。

这带来一个问题:模板的定义必须在使用它的编译单元里可见,所以模板通常定义在头文件里,不能像普通函数那样把声明放头文件、定义放源文件分开编译。如果模板定义在源文件里,其他编译单元看不到定义,就无法实例化,会产生链接错误。

模板的编译期计算,也叫模板元编程,是利用模板实例化在编译期做计算的技术。编译器在实例化模板时会递归展开,可以用这个特性在编译期计算常量、做类型推导、生成代码。C++11 之后有了 constexpr,很多编译期计算可以用更直观的方式写,不需要绕弯子用模板递归。C++17 的 if constexpr 可以在编译期做条件分支,根据类型特征选择不同的代码路径,比 SFINAE 更直观。

编译期计算的价值在于把运行时的开销转移到编译期,生成的代码更高效,同时可以在编译期做类型检查,把错误提前暴露。

3. 讲一下你理解的内存序,为什么在 x86 上很少出问题,但在 ARM 上容易出问题?

答:内存序描述的是多核 CPU 上,一个核对内存的写操作,什么时候对其他核可见,以及多个内存操作之间的顺序关系。

x86 架构采用的是强内存模型(TSO,全存储顺序),硬件层面保证了大部分操作的顺序性,写操作对其他核的可见顺序和程序顺序基本一致,只有 store-load 这一种重排可能发生。所以在 x86 上写多线程代码,即使没有显式的内存屏障,很多情况下也能正确工作,掩盖了潜在的问题。

ARM 架构采用的是弱内存模型,硬件允许更多种类的重排,load-load、load-store、store-load、store-store 都可能被重排,写操作对其他核的可见时机也更不确定。所以同一份代码在 x86 上跑没问题,移植到 ARM 上可能出现数据竞争导致的偶发性错误,而且很难复现。

正确的做法是不依赖具体架构的内存模型,用 C++ 标准提供的 std::atomic 和内存序来表达同步意图。memory_order_acquirememory_order_release 配对使用,可以建立跨线程的 happens-before 关系,编译器和 CPU 都会在必要时插入内存屏障指令,保证正确性,同时在不需要屏障的地方不插入,保证性能。

4. 你了解哪些 C++11 之后的新特性,实际项目里用过哪些,解决了什么问题?

答:用得比较多的有这几类。

智能指针,unique_ptrshared_ptr,解决了手动内存管理容易泄漏的问题,项目里基本用智能指针替代了裸指针,异常安全性也提高了很多。

移动语义和右值引用,对于持有大量资源的对象,移动代替复制,性能提升明显,尤其是往容器里放对象的时候。

auto 和范围 for 循环,减少了很多冗余的类型声明,代码更简洁,迭代器相关的代码写起来方便很多。

lambda 表达式,配合标准库算法用起来很顺手,也用于回调和异步任务,比写仿函数类简洁很多。

std::threadstd::mutex 这套多线程库,统一了跨平台的线程接口,不需要再用 pthread 或者 Windows API,配合 lock_guardunique_lock 做 RAII 风格的锁管理。

constexpr,把一些常量计算移到编译期,减少运行时开销,也让代码意图更清晰。

C++17 的 std:: 用来表达可能没有值的返回,比返回特殊值或者用输出参数更清晰。 用来做类型安全的联合体,替代了一些原来用继承做的多态场景。

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++八股文全集 文章被收录于专栏

本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。

全部评论

相关推荐

03-14 02:31
已编辑
中国计量大学 UE5
点赞 评论 收藏
分享
牛客57020934...:双非本投Seed吗,这种都是在9本9硕上还要挑一堆论文竞赛的岗
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

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