C++ 虚函数全解:从基础原理到高级特性(多重继承 / 菱形继承 / CRTP 对比)
前言
虚函数是 C++运行时多态的核心,也是面试、底层开发、嵌入式 C++ 必考重难点。本文从零梳理:定义、特点、底层原理、覆盖与隐藏、final、纯虚函数、虚析构、构造虚函数、默认参数坑、多重继承内存模型、菱形继承,最后对比 CRTP 静态多态,全篇适合直接背诵 + 面试默写。
一、什么是虚函数
虚函数由 virtual 关键字修饰,是 C++ 实现运行时多态的核心机制。
通俗理解用父类指针 / 引用当入口,程序运行时,实际自动执行子类重写的函数版本。
1.1 虚函数三大特点
-
多态绑定运行时按照对象实际类型调用对应函数,而不是指针的声明类型。理解:继承父类接口行为,但是实现自己独有逻辑。
-
继承性派生类可以使用 override 显式重写基类虚函数。
-
多态调用必须通过基类指针 / 基类引用才能触发多态。
cpp
Animal *p = new Dog();
// 父类指针指向子类对象,触发多态
二、虚函数实现原理
2.1 虚函数表 vtable + 虚指针 vptr
- 虚函数表 vtable
- 由编译器自动生成
- 每个有虚函数的类,独有一张虚表
- 存放当前类所有虚函数的地址
- 所有同类型对象共用同一张虚表
- 虚指针 vptr
- 每个对象独有一个 vptr
- 指向当前类的虚函数表
- 普通对象:只有成员变量
- 含虚函数对象:成员变量 + vptr 指针
示例:
cpp
A a1, a2, a3; // 3个对象,共用A同一张虚表
B b1, b2; // 2个对象,共用B同一张虚表
2.2 虚函数调用过程
- 通过对象找到自身 vptr
- 通过 vptr 找到所属类的 vtable
- 在虚表中取出虚函数地址
- 跳转执行函数
三、虚函数关键特性
3.1 覆盖 override 和 隐藏 hiding
- 覆盖(Override):有 virtual,运行时动态绑定,父类指针调子类实现,真正多态
- 隐藏(Hiding):无 virtual,编译期静态绑定,父类调子类、子类调子类,互不干扰,不触发多态
3.2 final 关键字 C++11
- 修饰虚函数:禁止子类重写
cpp
class Base {
public:
virtual void foo() final {}
};
class Derived : public Base {
// 编译报错,无法重写final虚函数
void foo() {}
};
- 修饰类:禁止被继承
cpp
class Base final{};
class Derived : public Base{}; // 编译报错
3.3 纯虚函数与抽象类
纯虚函数语法
cpp
virtual double area() = 0;
只有声明、无函数体。
抽象类包含至少一个纯虚函数的类:
- 不能实例化对象
- 只能作为接口模板被继承
- 子类必须全部重写纯虚函数,否则子类也变成抽象类
表格
| 类型 | 语法 | 是否抽象类 | 能否实例化 | 子类是否必须重写 |
|---|---|---|---|---|
| 普通虚函数 | virtual void f(){} | 否 | 可以 | 可选,不重写用父类 |
| 纯虚函数 | virtual void f() = 0; | 是 | 不可以 | 必须重写 |
四、虚函数性能分析
4.1 开销来源
空间开销
- 每个对象增加一个 vptr 4/8 字节
- 每个类单独占用一张虚函数表空间
时间开销
- 运行时间接调用,多 1~2 次寻址
- 破坏编译器优化,难以函数内联 inline
4.2 性能优化建议
- 性能关键路径少用虚函数
- 无多态需求不要定义 virtual
- 追求高性能多态改用 CRTP 编译期静态多态
五、虚函数高级技术
5.1 虚析构函数
核心结论:基类析构必须加 virtual,否则基类指针 delete 子类对象时,只调用基类析构,子类析构不执行,造成内存泄漏。
cpp
class Base {
public:
virtual ~Base() {} // 必须虚析构
};
class Derived : public Base {
~Derived() override {}
};
Base* p = new Derived();
delete p;
// 正确:先析构子类,再析构父类
5.2 构造函数中的虚函数
构造函数执行阶段,虚函数机制还没完全建立,只会调用当前自己类的函数版本,不会触发多态。
5.3 虚函数默认参数
- 虚函数:运行时动态绑定
- 默认参数:编译期静态绑定,编译阶段直接替换值
坑点:调用的是子类函数,用的却是父类默认参数。开发规范:虚函数尽量不要设置默认参数。
六、虚函数与对象内存模型
6.1 多重继承下的虚函数
场景:Derived 同时继承 Base1、Base2,两个基类都有虚函数。
内存布局:
plaintext
Derived 对象内存:
┌───────────────────────────┐
│ vptr1 → Base1 vtable │
├───────────────────────────┤
│ vptr2 → Base2 vtable │
└───────────────────────────┘
口诀:对象里有几个带虚函数的基类,就有几个 vptr;逻辑上每个基类对应一张独立虚函数表。
6.2 菱形继承 虚继承
继承结构:
plaintext
Base
/ \
Derived1 Derived2
\ /
Final
普通继承问题:Final 会包含两份 Base 子对象,造成冗余和二义性。解决:使用 virtual 虚继承,保证最终类只保留一份共享 Base。
如果 Derived1、Derived2 都重写了 Base 的虚函数,Final 必须自己重写该虚函数,否则编译器不知道选用哪一个父类版本,产生歧义报错。
七、CRTP 奇异递归模板模式
7.1 核心概念
CRTP 是编译期静态多态,代替虚函数,无虚表、无运行时开销。
写法:子类把自己当做模板参数传给父类。
cpp
运行
template <typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->impl();
}
};
class Derived : public Base<Derived> {
public:
void impl() {
// 子类具体实现
}
};
7.2 CRTP 与虚函数对比
表格
| 特性 | 虚函数 | CRTP |
|---|---|---|
| 绑定时机 | 运行时动态绑定 | 编译期静态绑定 |
| 运行开销 | 有 vptr、虚表、间接调用 | 零开销 |
| 多态类型 | 运行时多态 | 编译期多态 |
| 适用场景 | 框架、动态类型切换 | 高性能、底层复用 |
八、总结
- 虚函数依靠 vptr + vtable 实现运行时多态;每个类一张虚表,每个对象一个虚指针。
- override 是覆盖多态,无 virtual 是名字隐藏,不触发多态。
- final 可禁止重写、禁止继承;纯虚函数构成抽象类,只能做接口。
- 基类析构必须虚析构,防止内存泄漏。
- 构造函数内虚函数无效;虚函数不要用默认参数,静态绑定会挖坑。
- 多重继承有几个带虚函数基类,就有几个 vptr。
- 菱形继承用虚继承解决冗余二义性,最终类必须重写冲突虚函数。
- 要动态多态用虚函数,要高性能无开销多态用 CRTP。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)