一、什么是多态必问 多态是同一接口不同实现在 C 中主要有两种1️⃣ 编译时多态静态多态函数重载overload模板template2️⃣ 运行时多态动态多态⭐重点依赖虚函数virtual继承指针/引用调用二、经典面试题1、虚函数实现原理问虚函数是如何实现多态的核心答案通过虚函数表vtable 虚指针vptr内存模型每个带虚函数的类class Base { public: virtual void func() {} };对象内部对象 ├── vptr → 指向虚函数表虚函数表vtable: func → Base::func派生类vtable: func → Derived::func调用时Base* p new Derived(); p-func(); // 动态绑定实际执行通过 vptr 找到 Derived::func()2、为什么需要 virtual如果不加 virtualBase* p new Derived(); p-func(); // 调用 Base 版本加 virtualvirtual void func(); 运行时绑定Run-time binding3、虚析构函数1、为什么基类析构函数要是虚函数错误写法class Base { public: ~Base() {} };调用Base* p new Derived(); delete p; // ❌ 未调用 Derived 析构正确写法class Base { public: virtual ~Base() {} };调用结果先调用自己子类的析构函数然后再调用父类的析构函数先调用 Derived::~Derived() 再调用 Base::~Base()4、纯虚函数 vs 抽象类纯虚函数virtual void func() 0;特点不能实例化子类必须实现抽象类class Base { public: virtual void func() 0; };作用定义接口实现多态5虚函数可以是私有的吗可以但可以在类内调用可以被派生类覆盖但通常通过基类接口调用6构造函数可以是虚函数吗 ❌ 不可以原因构造时对象还没完整建立vptr 还没初始化7多态什么时候失效1、通过对象调用Derived d; Base b d; b.func(); // ❌ 对象切片2、构造函数中调用虚函数Base::Base() { func(); // ❌ 调用 Base 版本 }8多态 vs 函数重载类型多态重载时间运行时编译时机制虚函数函数签名是否继承是否9、override 和 finaloverride推荐void func() override;作用防止函数签名写错finalclass Base final {};或void func() final;作用阻止继承 / 重写10、多态的本质深度理解多态的本质“运行时根据对象类型选择函数”技术实现指针 → vtable → 函数地址11、虚函数底层原理当我们写Base* p new Derived();p-func();对象内存结构对象里会多一个隐藏指针 [ vptr ] → 虚函数表vtable [ 成员变量 ]虚函数表vtableDerived::vtable: func → Derived::func调用过程关键p-func();实际执行1. 取 p 指向对象的 vptr2. 通过 vptr 找到 vtable3. 找到 func 的函数地址4. 间接调用该函数汇编本质伪mov rax, [p] ; 取对象地址mov rax, [rax] ; 取 vptrmov rax, [raxoffset] ; 找 funccall rax ; 间接调用关键理解普通函数调用直接跳转快虚函数调用多了一层“查表”慢一点12、为什么虚函数比普通函数慢核心原因1️⃣ 多一次间接寻址普通函数call func虚函数call [vtable offset]2️⃣ CPU Cache 不友好虚函数函数地址是“运行时才知道” 可能导致指令预测失败cache miss3️⃣ 无法内联inline编译器无法提前确定调用对象p-func(); // ❌ 不能 inline 影响性能 一句话总结性能问题虚函数 动态分发 → 慢于静态绑定13、模板 vs 虚函数面试超级高频虚函数运行时多态class Base { public: virtual void func() {} }; 特点 运行时决定 有 vtable 有开销模板编译时多态templatetypename T void func(T obj) { obj.func(); } 编译器会 为每个类型生成一份代码维度虚函数模板多态类型运行时多态编译时多态性能有开销vtable接近0开销可内联灵活性高运行时切换低编译期确定代码体积稳定可能膨胀使用场景接口/框架高性能计算虚函数适合运行时多态支持统一接口模板适合编译时多态性能更高但会产生代码膨胀。那个更好?虚函数和模板没有绝对优劣取决于使用场景- 如果需要运行时动态选择行为如插件、接口、多态容器使用虚函数- 如果追求性能、类型在编译期已确定使用模板或CRTP更优。虚函数用于运行时多态适合需要动态行为切换的场景模板用于编译时多态适合高性能和类型已知的场景。在性能敏感的系统中通常优先选择模板或CRTP来避免虚函数的开销。虚函数运行时决定调用哪个函数本质动态绑定runtime polymorphism模板编译时生成具体代码本质静态多态compile-time polymorphism14、为什么很多高性能系统不用虚函数在这些领域游戏引擎自动驾驶感知模块图像处理你这个方向高频交易原因1️⃣ 追求极致性能虚函数额外一次间接调用2️⃣ 避免 cache miss高性能系统更喜欢连续内存 可预测执行3️⃣ 使用模板 / CRTPtemplatetypename Derived class Base { void interface() { static_castDerived*(this)-impl(); } }; 这叫CRTP静态多态15、CRTP高级面试加分点templatetypename Derived class Base { public: void interface() { static_castDerived*(this)-impl(); } }; class Derived : public BaseDerived { public: void impl() { cout Derived endl; } };编译器在编译期就确定调用点说明无虚函数✔无运行时开销✔可内联✔编译期绑定✔优点 零运行时开销 可内联 无 vtable缺点类型必须在编译期确定写法复杂不适合动态扩展16、虚函数表在内存中到底长什么样对象 [ vptr ] [ data1 ] [ data2 ] vtable: [ func1地址 ] [ func2地址 ]多继承会怎样class A { virtual void f(); }; class B { virtual void g(); }; class C : public A, public B {};C 对象[ A 的 vptr ] [ B 的 vptr ] 这会导致多个 vptr复杂地址计算17、虚函数的“隐藏成本”面试很加分1️⃣ 对象体积变大每个对象多一个 vptr2️⃣ 破坏内存布局连续性影响cacheSIMDvectorization3️⃣ 难以优化编译器无法确定调用目标18、虚函数 vs CRTP vs std::function方式本质时间性能灵活性虚函数运行时多态运行时中高CRTP编译时多态编译时高0开销中std::function函数对象封装运行时较低最高方式调用路径能否内联虚函数vtable❌CRTP直接调用✅std::function间接调用❌场景使用1️⃣ 虚函数适合框架 / 插件 / 动态行为例如Qt游戏引擎UI系统2️⃣ CRTP适合高性能算法 / 底层库例如3D视觉SLAM点云处理3️⃣ std::function适合业务层 / 回调 / 异步例如事件驱动多线程任务19、为什么不用虚函数而用 CRTPCRTP 是编译期多态不需要虚函数表可以避免运行时开销同时支持内联优化在高性能场景如算法、图像处理中更优。20、为什么要“尽量避免虚函数”虚函数的问题不是“不能用”而是1. 运行时查表vtable2. 破坏内联3. cache 不友好4. 不利于SIMD/批处理优化这些是高频计算场景性能 灵活性替代方案方案本质性能适用模板编译期多态⭐⭐⭐⭐⭐核心计算CRTP模板封装⭐⭐⭐⭐⭐库设计std::function函数封装⭐⭐⭐回调方案1模板最常用⭐⭐⭐⭐⭐1️⃣ 传统虚函数写法class Filter { public: virtual void process(std::vectorfloat data) 0; };2️⃣ 模板版本推荐templatetypename T void process(std::vectorfloat data, T algo) { algo.process(data); }使用class GaussianFilter { public: void process(std::vectorfloat data) { // 处理逻辑 } }; GaussianFilter f; process(data, f);优势✔ 编译期绑定✔ 可内联✔ 无 vtable✔ 支持优化SIMD方案2CRTP更底层templatetypename Derived class FilterBase { public: void process(std::vectorfloat data) { static_castDerived*(this)-processImpl(data); } }; class GaussianFilter : public FilterBaseGaussianFilter { public: void processImpl(std::vectorfloat data) { // 实现 } };使用GaussianFilter f; f.process(data); 本质编译期已经确定调用函数 等价直接调用 GaussianFilter::processImpl 优势零开销完全内联适合高性能库SIMD 模板顶级优化templatetypename T void process_simd(T* data, int n) { for (int i 0; i n; i 8) { // SIMD处理 } } 优势 ✔ 向量化 ✔ cache友好 ✔ 高吞吐21、重写和重的区别重写是子类对父类虚函数的重新实现属于运行时多态重载 → API设计问题接口设计重载是在同一作用域中函数名相同但参数不同属于编译时多态。重写 → 架构设计问题多态与扩展重写用于改变行为重载用于扩展接口。重写Override子类重新实现父类的虚函数class Base { public: virtual void func() { cout Base endl; } }; class Derived : public Base { public: void func() override { // 重写 cout Derived endl; } };特点必须是虚函数virtual函数签名必须一致运行时多态动态绑定用override关键字推荐重载Overload同一个作用域内函数名相同但参数不同void func(int x) {} void func(double x) {} // 重载 void func(int x, int y) {} // 重载特点同一个函数名参数不同个数 / 类型编译期决定调用静态绑定与继承无关对比项重写 Override重载 Overload发生位置子类 vs 父类同一个类是否需要继承✅ 需要❌ 不需要是否需要 virtual✅ 需要❌ 不需要函数签名必须相同必须不同绑定方式运行时动态编译时静态作用多态接口扩展22、什么是内联优缺点内联函数是指在调用处直接展开函数代码而不是进行函数调用类似宏但更安全有类型检查有作用域普通函数内联函数优点1️⃣ 消除函数调用开销普通函数调用成本压栈 → 跳转 → 返回内联直接执行 高频函数比如循环中收益明显2️⃣ 有利于编译器优化内联后代码暴露给编译器可以做常量折叠死代码消除循环展开向量化SIMD3️⃣ 类型安全比宏好#define ADD(a,b) ab // ❌ 有坑 inline int add(int a,int b) {} // ✅ 安全缺点1️⃣ 代码膨胀非常关键每次调用 → 展开一份代码 如果函数很大可执行文件变大2️⃣ 可能导致 cache 变差代码变多指令缓存I-cache压力变大 反而变慢高级点3️⃣ 不是一定会内联重点inline 只是“建议”不是强制编译器可能拒绝函数太大有循环有递归有虚函数4️⃣ 调试困难没有函数调用栈22、什么时候不会内联1、虚函数默认virtual void func();因为在调用的时候才知道调用的是那个函数2、递归函数int f(int n) { return f(n-1); }3、编译器觉得“展开不划算”对比宏inline类型检查❌✅作用域❌✅调试难较好安全性低高