金山 C++研发一面:编译链接、智能指针与虚函数模型
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 请做一下自我介绍
- 一个 C++ 源文件从编写到生成可执行文件,中间会经过哪些阶段?
- const 成员函数为什么可以被常量对象调用?mutable 会带来什么影响?
- 指针、引用和智能指针分别应该表达什么语义?
- shared_ptr 的控制块通常包含哪些内容?为什么不同实例之间线程安全不等于同一个变量线程安全?
- weak_ptr::lock() 和 expired() 有什么区别?为什么不能用 expired() 代替 lock()?
- 虚函数调用通常如何实现?它对对象布局和性能有什么影响?
- 纯虚函数和抽象类有什么关系?纯虚析构函数为什么还要提供实现?
- 虚继承主要解决什么问题?为什么它会增加对象模型复杂度?
- 多继承中出现同名成员时,如何消除调用歧义?
《参考解析》
预处理的四个阶段是怎么分工的:预处理只做文本操作——展开头文件、替换宏、按条件编译裁剪,产物还是源码;编译把源码变成汇编,模板实例化、重载决议、优化都在这里完成;汇编把汇编翻译成目标文件,此时符号未解析、地址是占位的;链接负责符号解析与重定位,把多个目标文件和库合成可执行文件。排错时可以按阶段对号入座:undefined reference 是符号解析失败(目标文件没参与链接、签名对不上、静态库顺序错),multiple definition 通常是函数定义写进了头文件,运行时找不到动态库则属于搜索路径问题,跟链接期无关。
const 成员函数与 mutable 的边界:const 成员函数做的事情是把隐式 this 从 X* 变成 const X*,所以常量对象才调用得了它。mutable 在这个承诺上开一个口子,合理用途是缓存、计数、懒加载这类不改变对象逻辑状态的东西。两点要记住:别拿 mutable 当绕开 const 改业务状态的后门;mutable 只解决编译期限制,不提供任何线程安全,多线程共用一个懒加载缓存照样要加锁。
智能指针表达的其实是三种所有权关系:引用表达「对象一定存在、我不负责它的生死、绑定关系不变」,适合函数参数和返回值。裸指针适合表达「可能为空」或「只是观察,不拥有」——用后者时必须在接口约定里写清谁释放。unique_ptr 是独占所有权,语义最窄最便宜,默认首选;shared_ptr 是共享所有权,引入原子计数、控制块和「谁最后释放」的运行时不确定性;weak_ptr 表达「我观察但我不参与决定」。「为了自动管理就全上 shared_ptr」是反模式——它把生命周期从设计问题变成了运行时问题。
「引用计数线程安全」的边界:控制块里通常有强计数、弱计数(两者都归零才回收控制块)、删除器。强计数减到 0 销毁被管理对象,弱计数也归零才释放控制块——这正是 weak_ptr 能判断对象是否存活的基础。线程安全的部分只是多个 shared_ptr 实例共享同一控制块时计数的增减;同一个 shared_ptr 变量被多线程无同步读写仍是数据竞争,因为要动的除了计数还有指针本身,得加锁或用 C++20 的 std::atomic<std::shared_ptr<T>>。另外,引用计数保护的只是控制块,不等于被管理对象内部的成员访问是线程安全的;也绝不要拿同一个裸指针分别构造两个 shared_ptr,那会产生两个控制块,最终 double free。
为什么必须用 lock() 而不是 expired():expired() 只是一次快照,回答「这一瞬间强计数是否为 0」,返回 false 之后别的线程完全可能在你访问对象之前释放掉最后一个 shared_ptr,于是 if (!wp.expired()) use(*wp); 是典型的检查后使用竞态。lock() 则是原子地尝试提升为强引用:成功返回非空 shared_ptr(持有它期间对象一定不会销毁),失败返回空。所以正确写法只有 if (auto sp = wp.lock()) { sp->use(); };只判断存活、不访问对象时才可以用 expired()。
虚函数模型与几个高频追问:虚函数靠对象里的虚表指针加虚表分派,覆写就是替换虚表槽位,代价是对象多一个指针、调用多一次间接跳转(类型明确时编译器可以去虚化)。构造函数不能是虚函数,因为此时虚表指针还没就位;基类析构函数必须是虚函数,否则用基类指针 delete 派生类对象只会走基类析构、派生类资源泄漏。纯虚析构函数照样让类变成抽象类,但必须提供实现,因为派生类析构会沿继承链调它。虚继承解决的菱形继承中公共基类被复制多份的问题,代价是共享的虚基类子对象位置无法静态确定,编译器要插虚基类表指针、构造顺序也变成虚基类优先且只构造一次。多继承的同名成员会直接报二义性,用 Base1::print() 显式限定,或在派生类里重写一个同名函数盖住两者。