虚函数底层原理
虚函数、多态靠虚函数表(vtable)和虚指针(vptr)实现。编译时,编译器自动生成查表代码,运行时查表跳转。
以下以GCC/Linux平台为例(VS大体相同)。
对比
没有虚函数的普通类
1 | class Base{ |
-对象内存:只有成员变量int a。
-成员函数不在对象内,存放在代码段(.text),所有对象共用一份函数。
-静态绑定:编译期直接确定函数地址。
有虚函数(virtual)的类
1 | class Base{ |
1.对象内存分布
对象最开头,编译器自动插入了一个隐藏成员变量:虚指针(vptr)。
1 | Base 对象内存(8字节,64位) |
-每一个实例都有自己的vptr,但同一个类所有对象共用一个虚函数表。
2.什么是虚函数表vtable
vtable是一张函数指针表,存放该类的虚函数指针,普通函数不进入vtable。
Base的vtable结构:
1 | Base::_vtable |
3.对象初始化时vptr怎么赋值?
编译器会自动在构造函数开头插入代码,给vptr赋值。
1 | Base::Base() |
-这就是为什么构造函数中调用虚函数不会触发多态:
子类还没有构造,Base的vptr指向当前正在构建的类(Base)的虚函数表。
派生类继承并重写虚函数
1 | class Derived:public Base{ |
1.Derived对象分布
1 | Derived 对象: |
2.Derived的虚函数表
1 | Derived::_vtable: |
虚表下标不变,对应位置的函数指针换成子类版本。
3.多态调用完整流程:
1 | Base* p=new Derived; |
编译阶段(静态):
编译器知道 p 是Base*,通过Base的虚函数表确定虚函数在虚表中的下标。
编译器不会直接填函数地址,而是会生成一段查表代码。
汇编阶段 (运行时做的事,动态绑定):
伪汇编:
1 | ;1.取出p指向对象的vptr放入rdx |
运行时才根据实际对象的vptr找到对应虚表,再根据下标找到并调用对应子重写函数(动态绑定)。
如果子类新增自己的虚函数?
1 | class Derived{ |
子类的虚函数表:
1 | Derived::_vtable |
1.新增虚函数追加到虚表后。
2.若Base* p=new Derived(),不能直接调用fun_2,Base::_vtable内没有下标1的条目。
多继承下的虚表
1 | class C:public A,public B{ |
1 | C的对象: |
每个父类对应一张独立的虚表。
几个底层硬事实
1.vptr放在哪?
GCC:放在对象最起始地址;部分编译器可配置。
###2.虚函数表存在哪里
全局只读段.rodata,不属于对象,属于类。
3.大小开销
64位:一个vptr8字节
32位:多4字节
4.static静态函数不能是virtual
static没有this指针;而虚函数调用必须通过对象拿vptr。
5.构造函数不能是virtual
构造函数创建一个有效的vptr用来查表。若构造函数为虚函数,构造时要查虚表,但此时vptr未初始化,无法查表,故构造函数不能是virtual。
完整链路
1 | Base* p = new Derived(); |
