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

内联函数

优点
1️⃣ 消除函数调用开销
普通函数调用成本:
压栈 → 跳转 → 返回
内联:
直接执行
👉 高频函数(比如循环中)收益明显
2️⃣ 有利于编译器优化
内联后:
代码暴露给编译器
可以做:
- 常量折叠
- 死代码消除
- 循环展开
- 向量化(SIMD)
3️⃣ 类型安全(比宏好)
cpp
#define ADD(a,b) a+b // ❌ 有坑
inline int add(int a,int b) {} // ✅ 安全
缺点
1️⃣ 代码膨胀(非常关键)
每次调用 → 展开一份代码
👉 如果函数很大:
可执行文件变大
2️⃣ 可能导致 cache 变差
代码变多:
指令缓存(I-cache)压力变大
👉 反而变慢(高级点)
3️⃣ 不是一定会内联(重点)
inline 只是"建议",不是强制
编译器可能拒绝:
- 函数太大
- 有循环
- 有递归
- 有虚函数
4️⃣ 调试困难
没有函数调用栈
22、什么时候不会内联
1、虚函数(默认)
cpp
virtual void func();
因为在调用的时候才知道调用的是那个函数
2、递归函数
cpp
int f(int n) {
return f(n-1);
}
3、编译器觉得"展开不划算"
| 对比 | 宏 | inline |
|---|---|---|
| 类型检查 | ❌ | ✅ |
| 作用域 | ❌ | ✅ |
| 调试 | 难 | 较好 |
| 安全性 | 低 | 高 |