关于虚函数表(Virtual Function Table)的存储方式

前言

在C++中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类会把这些函数声明为虚函数(Virtual Function)

在使用基类引用或指针调用一个虚成员函数时会执行动态绑定,因此只有在运行时才能知道调用的是哪个版本的虚函数,被调用的函数是与绑定到指针或对象上的对象的动态类型相匹配的那一个,所以虚函数也常用于实现多态性

多态性(Polymorphism)源自希腊语,含义为“多种形式”,在C++中多态性分两种,分别是静态多态(利用函数重构原理实现)和动态多态(利用虚函数覆盖实现)

 

虚函数表与虚函数表指针的基本概念

若类中包含虚函数,那么在声明其对象时就会产生一个虚函数表指针(Virtual Function Table Pointer)和一个虚函数表(Virtual Function Table),这个指针指向虚函数表的首地址,表内按函数声明的顺序存放虚函数的地址

 

虚函数表:虚基类表是一块存储本类及基类所有的虚函数地址的空间,它由系统自动分配,不占用类的存储空间
虚函数表指针:虚函数表指针是一个指向虚函数表首地址的指针,存在类中,占用类的存储空间,虚函数表指针是提供给系统用于查找虚函数表,对于使用者来说可以忽略其实现原理;一般来说,一个类的对象只会有一个虚函数表指针,或是本类的,或是从基类继承下来的

 

派生类中的虚函数表与虚函数表指针

当基类中声明了虚函数,那么派生类中再声明虚函数时,便不会产生新的虚函数表与虚函数表指针,而是继承基类的,并将在派生类中声明的虚函数地址存储到虚函数表中(按声明顺序存储在虚函数表最后一个成员的后面)

当派生类中再次声明一个与基类虚函数同名同参的函数时,该函数被默认为虚函数(当然,再次使用virtual关键字声明也无妨),此时派生类的虚函数地址会覆盖虚函数表中基类虚函数的地址

# 实际上,若虚函数在单独一个类中使用并没有太大的意义,它主要利用覆盖的特性实现动态多态

   

虚函数表的存储方式

协助工具

在探究虚函数表前,需要一些工具来协助:VS2019(Visual Studio2019)

设置断点

在VS2019程序编辑窗口,点击最左侧添加断点

VS2019添加断点

 

查看空间分配

VS2019提供了以下三种方式查看对象空间:

调试过程中
第一种:在菜单栏选择 调试-窗口-局部变量 调出局部变量窗口 

VS2019局部变量窗口

 

第二种:选中变量 鼠标右击-快速监视 调出快速监视窗口

VS2019快速监视窗口

 

非调试过程中
第三种:VS2019提供给用户显示C++对象在内存中的布局选项:/d1reportSingleClassLayout
使用方法如下:
在菜单栏选择  工具-命令行-开发者命令提示(C) 调出命令提示窗口
进入后切换至需要显示布局的cpp文件所在目录,输入以下命令即可:

cl <FileName>.cpp /d1reportSingleClassLayout<ClassName>

 

存储方式

1. 单一类

单一类内含虚函数,声明对象时会产生一个虚函数表和虚函数表指针,虚函数按声明顺序存储在虚函数表中,虚函数表指针指向虚函数表首地址,存在类对象的空间中

Example:

#include <iostream>
using namespace std;

class Single {
public:
	virtual void fun1(void) { cout << "fun1" << endl; }
	virtual void fun2(void) { cout << "fun2" << endl; }
	void fun(void) { cout << "fun" << endl; }
};

int main(void)
{
	Single a;
    cout << sizeof(a) << endl;
	return 0;
}

由于此处文件名为SingleClass.cpp,class名为Single,所以在命令提示窗口输入以下指令:

cl SingleClass.cpp /d1reportSingleClassLayoutSingle

class Single的内存布局如下

Single对象的大小是4byte,其里面的成员只有vfptr(Vritual Function Table Pointer),它所指向的vftable(Vritual Function Table)只有两个成员,它们按照声明的顺序存储,而fun并没有声明为virtual,所以它的地址并不会存储在虚函数表中

具体空间分布

 

2. 派生类

若基类不含虚函数,那么规则跟单一类相同;若基类含虚函数,那么在声明对象时,基类已经产生一个虚函数表和一个虚函数表指针,派生类直接继承基类的虚函数表和虚函数表指针

派生类声明的虚函数按声明顺序存储在继承的虚函数表后面,若派生类成员函数存在与基类虚函数同名同参的函数,那么默认为虚函数,且其地址会覆盖虚函数表中的基类虚函数地址

Example:

#include <iostream>
using namespace std;

class Base {
public:
	virtual void fun(int mem = 0) { cout << "Base::fun" << mem << endl; }
	virtual void fun2(void) { cout << "Base::fun2" << endl; }
};

class Derived{
public:
    virtual void fun(int mem = 1) { cout << "Derivde::fun" << mem << endl; }
	virtual void fun3(void) { cout << "Derived::fun3" << endl; }
};

int main(void)
{
	Derived a;
    cout << sizeof(a) << endl;
    // Base *b = &a;
    // b->fun();
	return 0;
}

class Derived内存分布

Derived对象的大小同是4byte,里面的成员只有{base class Base}的vfptr,这说明在派生类Derived中并没有再次产生一个虚函数表指针,而是继承了基类的虚函数表指针,它所指向的vftable有三个成员,fun3是Derived类的虚函数,而Base类的fun地址被Derived类的fun地址所覆盖,所以在调用函数时,系统从虚表中获取的函数地址是&Derived::fun

通过快速监测查看对象地址及内存分布

这里虚函数表成员只显示了两个,本人猜测是因为在局部变量监测时,对象a中的vfptr被认为是Base类的虚函数表指针,由于Base类只有两个虚函数,所以它只显示了两个;从虚函数表的描述中也不难看出成员不止有两个

为了检验正确性,在Qt上也做了相同的监测

具体空间分布(以VS2019检测地址为例)

关于派生类虚函数参数默认值

在这个例子中,主函数有两行注释,其内容是用基类的指针指向派生类的对象,再利用这个指针调用虚函数fun;根据虚函数覆盖原理,此时虚函数表中存储的是Derived::fun的地址,那么调用fun函数就是调用Derived::fun,但是这里并无传参,而是采用默认参数,那么系统调用的默认参数会是类Base中的mem=0,还是类Derived中的mem=1呢?

实际上,由于默认参数是存储在数据段的,通过类Base指针去调用虚函数fun时,系统并不会因为虚函数表中的fun被Derived::fun所覆盖而认为调用的是Derived::fun,系统仍然会认为调用的是Base::fun,所以其默认参数也是选用类Base中的mem=0

这里要清晰一个概念:无论虚函数怎么覆盖,它只是覆盖掉虚函数表里面的函数地址而已,对函数本身是不会有影响的

运行结果

 

3. 虚继承中的虚函数

虚函数表与虚基类表其实是两个完全不同的表,它们互不相干,内容存储格式也完全不同,虚基类表是按类信息为单位存储的(虚基类表指针与直接继承类的偏移、虚基类各成员与直接继承类的偏移)

所以即使存在虚继承关系,类的对象中存有虚基类表指针,当有虚函数存在时,仍然会产生一个虚函数指针和虚函数表

既然如此,为什么还要单独说明这种情况呢?
这是因为某些编译器会进行优化,这些编译器编译时为了尽量缩小其所占资源,会将虚基类表与虚函数表合成一个vtable(Vritual Table),那么自然的,也不需要分虚基类表指针与虚函数表指针了,它们合成一个vptr(Vritual Table Pointer)
比如:Ubuntu16.04 g++编译器、Qt Creator4.11.1

Example:

#include <iostream>
using namespace std;

class Indirect{
public:
    void fun(void) { cout << "Indirect" << endl; }
};

class Base :virtual public Indirect{};

class Derived{
public:
    virtual void fun(int mem = 1) { cout << "Derivde::fun" << mem << endl; }
};

int main(void)
{
	Derived a;
    cout << sizeof(a) << endl;
	return 0;
}

VS2019 C++编译器编译结果

Qt Creator4.11.1 编译结果

 由上面两图我们可以发现,在VS2019上编译,类Derived中含有两个指针,分别是虚函数表指针vfptr与虚基类表指针vbptr,所占空间也是8byte(32bit中一个指针的大小为4byte);而在Qt Creator 4.11.1中,类Derived只有一个指针vptr

 

利用指针调用虚函数表中的函数方法实例

#include <iostream>
using namespace std;

class Base {
private:
    virtual void show() {
        cout << "show" << endl;
    }
    virtual void printf() {
        cout << "printf" << endl;
    }
};

int main()
{
    cout << "sizeof(Base):" << sizeof(Base) << endl;
    Base b;
    void* vftpr = (void*)(*((unsigned int*)&b));            //获取虚函数表地址

    void* vfptr1 = (void*)(*((unsigned int*)vftpr));        //获取虚函数表中第一个函数show的地址
    void* vfptr2 = (void*)(*(((unsigned int*)vftpr) + 1));  //获取虚函数表中第二个函数printf的地址

    typedef void (*Fun)();      //定义返回值为void,函数参数为void的函数指针类型
    Fun show = (Fun)vfptr1;
    Fun printf = (Fun)vfptr2;
    show();
    printf();
    return 0;
}

运行结果

 

# 本人才疏学浅,本文主要是本人学习上的一种记录及思考,若其中有所不足欢迎大家指出,期待与大家交流

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值