多态的底层依靠虚函数表(vtable)+虚函数指针(vptr)实现:
当类含有虚函数时,编译器会给这个类生成一张虚函数表;该类的每个对象内部会新增一个虚指针,指向本类的虚函数表。
调用虚函数时,运行时通过对象的vptr找到虚表,再查表调用函数,而不是编译期直接绑定函数地址,以此实现运行时动态绑定。
有ai后学习东西确实方便很多。
两个核心概念
- 虚函数表 vtable
- 属于类 ,同一个类所有对象共用一张虚表,存放在只读数据段。
- 表里面存储:本类虚函数的函数地址。
- 子类继承父类虚函数:
- 如果 ** 重写 (override)** 虚函数:虚表中对应位置,替换成子类函数地址;(我觉得这个是关键)
- 如果没有重写:保留父类虚函数地址。
- 虚函数指针 vptr
- 属于对象 ,每个对象的内存布局最开头会隐藏增加一个指针
vptr。 - 对象构造时,编译器自动给
vptr赋值,指向当前类的虚函数表。 注意:构造函数不能是虚函数 ,对象构造过程中vptr 还没初始化完成。
- 属于对象 ,每个对象的内存布局最开头会隐藏增加一个指针
多态生效的 3 个必要条件
- 存在继承关系;
- 父类中有虚函数 ,子类重写该虚函数;
- 使用父类指针 / 引用指向子类对象。
cpp
class Base{
public:
virtual void func(){ cout << "Base" << endl; } // 虚函数
};
class Son : public Base{
public:
void func() override { cout << "Son" << endl; } // 重写
};
int main(){
Base* p = new Son(); // 父类指针指向子类对象
p->func(); // 运行时多态,输出 Son ✔
delete p;
return 0;
}
如果不用指针 / 引用,直接Base b = Son();,会发生对象切片,无法触发多态。
这句话什么意思?
对象切片(Object Slicing)通俗讲解
Base b = Son();这句话的含义:在栈上创建一个Son临时对象,然后把这个临时对象里属于Base父类那一部分数据拷贝出来,赋值给Base类型变量b;子类独有的成员直接被切掉丢掉,只剩下父类部分。 所以最后得到的b,本质就是一个纯 Base 对象,不再是 Son 对象,自然无法触发多态。
先看内存结构
cpp
class Base{
public:
virtual void func(){ cout << "Base" << endl; }
int base_val = 1;
};
class Son : public Base{
public:
void func() override { cout << "Son" << endl; }
int son_val = 2; // 子类独有的成员
};
Son 对象内存布局:
[vptr_Son][base_val][son_val]
包含:子类虚指针 + 父类成员 + 子类新增成员
执行 Base b = Son(); 的拷贝过程:
只拷贝父类那一段:[vptr_Base][base_val]
son_val 直接被切掉(sliced) ;并且 b 的虚指针 vptr 被设置成指向 Base 的虚函数表,不再指向 Son 的虚表。
最终 b 就是一个 Base 对象,不是 Son。此时调用 b.func(),直接调用 Base::func,不会走多态。
对比:用指针 / 引用为什么就不会切片?
cpp
Son s;
Base* p = &s; // 指针,只是保存s对象的地址,不会拷贝对象
Base& ref = s; // 引用,也是s对象的别名,不会拷贝对象
- 指针 / 引用不会复制对象内存,不会产生新对象;
- 原始的
Son对象完整保留在内存中,它内部的**vptr_Son**仍然指向 Son 虚表; - 所以
p->func()/ref.func()在运行时查表,调用 Son::func,触发多态。
调用过程
p->func()执行步骤:
- 运行时取出对象
*p内存首地址的**vptr**; - 通过
vptr找到对应的虚函数表 vtable; - 在虚表中找到**
func对应的函数地址**; - 调用该地址函数。
关键点:函数地址是运行时才确定,叫动态绑定;普通成员函数编译期就确定地址,静态绑定。
运行时,对象实际类型不同,则会得到不同的虚函数表。(类首先需要包含虚函数)
虚析构函数 父类析构建议加**virtual**。
如果父类指针指向子类对象,delete p;时,只有父类析构是虚函数,才会先调用子类析构,防止子类内存泄漏。
多重继承虚表:一个对象会有多个 vptr ,每继承一个带虚函数的父类就多一张虚表。
虚函数带来的代价:
- 对象内存增加一个指针大小(8 字节,64 位);
- 每次调用虚函数要查表,有微小运行开销。
纯虚函数:virtual void func() = 0;,这个类是抽象类,不能实例化对象 ,虚表中对应位置存**nullptr**。
常见坑
- 子类重写时,函数名 / 参数 / 返回值必须匹配,否则不是重写,只是函数隐藏;C++11
override关键字用来校验。 - 静态成员函数不能是 virtual,静态函数不属于对象,没有 this 指针,无法通过 vptr 查表。
虚函数表内存布局
环境:64 位系统,指针占 8 字节
cpp
class Base {
public:
virtual void func1();
virtual void func2();
int base_a = 10;
};
class Son : public Base {
public:
void func1() override; // 重写func1
virtual void func3(); // 新增虚函数
int son_b = 20;
};
Base 类对象内存布局
cpp
Base obj;
obj内存:
+----------------+
| vptr_Base | 8字节 → 指向Base的虚函数表
+----------------+
| base_a = 10 | 4字节
+----------------+
Base虚函数表 vtable_Base(只读数据段,属于类,所有对象共用)
+----------------+
| &Base::func1 | 虚函数地址
+----------------+
| &Base::func2 | 虚函数地址
+----------------+
| nullptr | 虚表结束标记(部分编译器)
+----------------+
Son 子类对象内存布局(单继承)
cpp
Son son_obj;
son_obj内存:
+----------------+
| vptr_Son | 8字节 → 指向Son虚函数表
+----------------+
| base_a = 10 | 4字节(继承父类成员)
+----------------+
| son_b = 20 | 4字节(子类新增成员)
+----------------+
Son虚函数表 vtable_Sase:
+----------------+
| &Son::func1 | ✅ 重写:替换成子类func1地址
+----------------+
| &Base::func2 | ❗没重写:保留父类func2地址
+----------------+
| &Son::func3 | ✅ 子类新增虚函数,追加在后面
+----------------+
| nullptr |
+----------------+
重点:单继承规则
- 子类重写哪个虚函数,虚表对应位置 就替换成子类函数地址;
- 没有重写的虚函数,保留父类地址;
- 子类新增虚函数,追加到虚表尾部。
演示对象切片 Base b = son_obj;
cpp
Son son_obj 原始内存:
[vptr_Son][base_a][son_b]
Base b = son_obj; // 调用Base拷贝构造,只拷贝父类部分
b 的内存:
+----------------+
| vptr_Base | ⚠️ vptr被重置!指向Base虚表
+----------------+
| base_a =10 |
+----------------+
// son_b 直接被丢弃,切掉了
所以b.func1(),查表找到Base::func1,不会调用 Son 的版本,多态失效。
多继承场景
普通多继承(两个父类都带虚函数)
cpp
class A{
public:
virtual void fa();
int a_val = 1;
};
class B{
public:
virtual void fb();
int b_val = 2;
};
// C同时继承A、B
class C : public A, public B{
public:
void fa() override; // 重写A的虚函数
virtual void fc(); // C新增虚函数
int c_val = 3;
};
对象 C 的内存布局
cpp
C obj;
+----------------+
| vptr_A | 属于A的虚指针
+----------------+
| a_val = 1 | A的成员变量
+----------------+
| vptr_B | 属于B的虚指针(第二个!多继承核心:几个带虚函数父类,就几个vptr)
+----------------+
| b_val = 2 | B的成员变量
+----------------+
| c_val = 3 | C自己新增成员
+----------------+
两张虚函数表
vtable_A(A 的虚表)
cpp
& C::fa // 重写A::fa,替换成C版本
& C::fc // C新增虚函数,追加到第一个父类A虚表末尾
nullptr
vtable_B(B 的虚表)
cpp
& B::fb // fb没有重写,保留B原始函数地址
nullptr
关键要点
- 每继承一个拥有虚函数 的父类,子类对象就会多出一个**
vptr**; - 子类新增虚函数,只会追加到第一个父类 的虚表后面;
- 父类指针指向子类对象:
cpp
C c;
A* pa = &c; // pa指向c对象起始地址,vptr_A位置
B* pb = &c; // pb 指向c对象里面vptr_B的地址!指针会自动偏移!
pb = &c这里编译器自动做地址偏移,跳到 B 子对象起始位置。
如果强转反过来,C* pc = (C*)pb;,不安全,需要小心指针偏移问题。
菱形继承(钻石继承)------ 多继承经典坑
cpp
结构:
Base
/ \
A B
\ /
C
代码:
cpp
class Base{
public:
virtual void func();
int base_val = 10;
};
class A : public Base{
public:
int a_val = 1;
};
class B : public Base{
public:
int b_val = 2;
};
class C : public A, public B{
public:
int c_val = 3;
};
C 对象内存布局(普通菱形继承,没有虚继承)
cpp
C obj
+----------------+
| vptr_A_Base // A继承来的Base虚指针
+----------------+
| base_val = 10 // A带来的Base成员副本【第一份Base】
+----------------+
| a_val = 1
+----------------+
| vptr_B_Base // B继承来的Base虚指针
+----------------+
| base_val = 10 // B带来的Base成员副本【第二份Base】
+----------------+
| b_val = 2
+----------------+
| c_val =3
+----------------+
两个大问题:
-
数据冗余 :
Base的成员变量base_val存了两份,浪费内存; -
二义性
cppC c; c.base_val; // 编译报错!不知道是A里面那份还是B里面那份Base的base_val即使你写
c.A::base_val可以指定访问,但冗余问题还在。
虚继承 virtual inheritance ------ 解决菱形继承
修改继承方式:A、B 虚继承 Base
cpp
class Base{
public:
virtual void func();
int base_val = 10;
};
class A : virtual public Base{ // virtual 虚继承
public:
int a_val = 1;
};
class B : virtual public Base{ // virtual 虚继承
public:
int b_val = 2;
};
class C : public A, public B{
public:
int c_val =3;
};
virtual public Base:虚继承,不是把 Base 拷贝进 A、B 里面,而是 A、B 记录一个偏移量,运行时去找唯一的 Base
虚继承 ≠ 虚函数。虚继承解决菱形继承,虚函数实现多态,两套完全独立机制。
C 对象内存布局
cpp
C对象
├─A子对象:
│ vbptr_A ←【虚基类指针】,不是虚函数指针vptr!
│ A自己的成员
├─B子对象:
│ vbptr_B ←【虚基类指针】
│ B自己的成员
├─C自己成员
│
└─【唯一一份Base】放在整个对象末尾!
vptr_Base
base_val = 10
vbptr(虚基类指针)指向【虚基类表 vbtable】
虚基类表里存:当前这个子对象到公共 Base 的内存偏移量
vbptr_A指向 A 的虚基类表:记录一个数字,比如offset=24,意思:从 A 的起始地址向后偏移 24 字节,找到 Basevbptr_B指向 B 的虚基类表:记录offset=16,意思:从 B 的起始地址向后偏移 16 字节,找到 Base
访问 c.base_val 的过程
- 编译器发现 base_val 属于虚基类 Base;
- 通过 A 或者 B 的**
vbptr拿到虚基类表**; - 查表得到偏移量;
- 当前地址 + 偏移 → 定位到唯一那一份 Base,读取 base_val。
✅ 现在:Base 只有一份!
c.base_val不再有二义性。
虚继承 + 虚函数,混合在一起(容易混淆的地方)
Base 里面带虚函数,所以公共 Base 子对象自己有一个 vptr_Base
❗A 和 B 里面的**
vbptr是虚基类指针**,不是虚函数指针 vptr
- vptr:虚函数指针,用来找虚函数表,实现多态
- vbptr:虚基类指针,用来找虚基类表,找公共基类(虚继承专用)
举个直观类比(方便理解)
普通继承:A 买了一份 Base,B 也单独买一份 Base。C 继承 A、B,手里拿到两份 Base。
虚继承:A、B 都不把 Base 买下来,只各自拿一张纸条(vbptr,虚基类表),纸条上写着 Base 存放的地址偏移。C 继承 A、B 之后,只创建一份 Base 放在最后,A 和 B 的纸条都指向这同一个 Base。
虚继承的开销?
- 每个虚继承的子类,多一个
vbptr****指针(8 字节 64 位); - 访问虚基类成员时,多一次查表 + 偏移计算,运行时开销。
所以虚继承一般只用来解决菱形继承,不推荐随便使用。
我想知道,上面的情况下,a还可以被new出来吗?
可以!A 类依然可以 new A() 创建对象,不受虚继承 virtual public Base 的影响。 虚继承只是修改继承体系中子类的内存布局 ,A 本身仍然是一个正常可实例化的类。
cpp
A* pa = new A(); 完全合法,看看 new A() 的内存布局
当我们单独创建一个 A 对象:
cpp
A 对象(new A出来的)
+----------------+
| vbptr_A // A的虚基类指针
+----------------+
| a_val = 1 // A自己成员
+----------------+
| vptr_Base // Base虚指针(Base实例放在末尾)
+----------------+
| base_val = 10 // Base成员
+----------------+
- 单独 new A 的时候,编译器会自动生成一份 Base放在 A 对象尾部;
vbptr_A虚基类表里面记录偏移,找到这个 Base;- 虚继承的含义是:当 A 和 B 同时被 C 继承的时候,共享 Base;单独实例 A,A 自己带一份 Base。
一句话理解虚继承:virtual public Base 不是说 A 不能单独存在;它只是一个标记:如果未来有人同时继承 A、B,就共用一份 Base;如果单独用 A,A 自己正常带一份 Base。
其实,虚继承和虚函数,菱形继承混在一起让我有点晕了,先这样,后面再梳理梳理。