虚函数、多态靠虚函数表(vtable)和虚指针(vptr)实现。编译时,编译器自动生成查表代码,运行时查表跳转。
以下以GCC/Linux平台为例(VS大体相同)。

对比

没有虚函数的普通类

1
2
3
4
5
class Base{
public:
void fun(){}
int a;
};

-对象内存:只有成员变量int a。
-成员函数不在对象内,存放在代码段(.text),所有对象共用一份函数。
-静态绑定:编译期直接确定函数地址。

有虚函数(virtual)的类

1
2
3
4
5
6
7
8
9
class Base{
public:
Base()
{
a=10;
}
virtual fun(){}
int a;
};

1.对象内存分布
对象最开头,编译器自动插入了一个隐藏成员变量:虚指针(vptr)。

1
2
3
Base 对象内存(8字节,64位)
0x00|vptr (8字节,指针,指向虚函数表首位)
0x08|a (int,4字节,对齐后占8)

-每一个实例都有自己的vptr,但同一个类所有对象共用一个虚函数表。

2.什么是虚函数表vtable
vtable是一张函数指针表,存放该类的虚函数指针,普通函数不进入vtable。
Base的vtable结构:

1
2
3
Base::_vtable
[0]-> &Base::fun
[1]-> nullptr //末尾哨兵

3.对象初始化时vptr怎么赋值?
编译器会自动在构造函数开头插入代码,给vptr赋值。

1
2
3
4
5
Base::Base()
{
this->vptr = &Base::_vtable;//编译器自动添加
a=10;
}

-这就是为什么构造函数中调用虚函数不会触发多态:
子类还没有构造,Base的vptr指向当前正在构建的类(Base)的虚函数表。

派生类继承并重写虚函数

1
2
3
4
class Derived:public Base{
public:
void fun() override{...}
};

1.Derived对象分布

1
2
3
Derived 对象:
0x00 | vptr -> Derived::_vtable
0x08 | a //继承Base的成员

2.Derived的虚函数表

1
2
3
Derived::_vtable:
[0]->&Derived::fun
[1]->nullptr

虚表下标不变,对应位置的函数指针换成子类版本。

3.多态调用完整流程:

1
2
Base* p=new Derived;
p->fun();

编译阶段(静态):
编译器知道 p 是Base*,通过Base的虚函数表确定虚函数在虚表中的下标。
编译器不会直接填函数地址,而是会生成一段查表代码。

汇编阶段 (运行时做的事,动态绑定):
伪汇编:

1
2
3
4
5
6
;1.取出p指向对象的vptr放入rdx
mov rdx,[p]
;2.通过rdx拿到Derived的虚表[0](Derived::fun的地址)
mov rax,[rdx+0]
;3.跳转执行
call rax

运行时才根据实际对象的vptr找到对应虚表,再根据下标找到并调用对应子重写函数(动态绑定)。

如果子类新增自己的虚函数?

1
2
3
4
5
6
class Derived{
public:
virtual void fun_1(){}
virtual void fun_2(){}
int a;
};

子类的虚函数表:

1
2
3
4
Derived::_vtable
[0]->&Derived::fun_1
[1]->&Derived::fun_2
[2]->nullptr

1.新增虚函数追加到虚表后。
2.若Base* p=new Derived(),不能直接调用fun_2,Base::_vtable内没有下标1的条目。

多继承下的虚表

1
2
3
4
class C:public A,public B{
public:
int a;
};
1
2
3
4
C的对象:
0x00 | vptr_A->C::C_vtable_A
0x08 | vptr_B->C::C_vtable_B
0x16 | a

每个父类对应一张独立的虚表。

几个底层硬事实

1.vptr放在哪?

GCC:放在对象最起始地址;部分编译器可配置。

###2.虚函数表存在哪里
全局只读段.rodata,不属于对象,属于类。

3.大小开销

64位:一个vptr8字节
32位:多4字节

4.static静态函数不能是virtual

static没有this指针;而虚函数调用必须通过对象拿vptr。

5.构造函数不能是virtual

构造函数创建一个有效的vptr用来查表。若构造函数为虚函数,构造时要查虚表,但此时vptr未初始化,无法查表,故构造函数不能是virtual。

完整链路

1
2
3
4
5
6
7
8
9
Base* p = new Derived();
↓
p保存堆上Derived对象地址
↓
对象首地址 → vptr → Derived虚函数表
↓
虚表[0] = &Derived::fun
↓
call 该地址 → 执行子类函数