C++ 多态之虚函数表
创始人
2024-02-18 04:12:46

虚函数表概述

C++ 的多态,使用动态绑定的技术,技术的核心是虚函数表(简称虚表),每个包含了虚函数的类都包含一个虚表,虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象共用一个虚表。

1.虚表指针

每个包含虚函数的类,都有一个虚表指针,虚表指针在数据结构的第一个,对象在创建时就有了这个指针,并且指向了该类的虚表。

#include 
using namespace std;
class TBase {void fun1(){};void fun2(){};void fun3(){};int  x;
};
class VTBase {virtual void fun1(){};virtual void fun2(){};virtual void fun3(){};int          x;
};
int main()
{TBase tbase;VTBase vtbase;std::cout << sizeof(tbase) << endl;std::cout << sizeof(vtbase) << endl;    
}
  • 如果是x64编译,默认是8字节对齐 输出为4,16
  • 如果是x86编译,默认是4字节对齐 输出为4,8

我们知道函数是不会占用类的空间的,所以第一个Tbase类大小始终为4。含有虚函数的类,无论有多少个虚函数,都会增加一个虚表指针的大小(x64:8,x86:4)

另外可以通过offsetof来看偏移,以x64编译器为例子:

cout << offsetof(VTBase, x) << endl;//输出值为8

x偏移为8,是我们结构体中可见的首个变量,所以前8个字节为虚函数表指针的大小,也是该类的第一个成员变量。

在这里插入图片描述

1.1剖析虚表指针的调用

这里还是以X64编译器举例

  • 为了方便看代码,我们首先
    typedef long long Vtpr_type;typedef void (*VFunc)(void); //定义一个函数指针类型变量类型 VFunc
  • 还是以VTBase类举例子,我们已知前8个字节为虚函数表指针,所以通过变量指向该地址
	VTBase* v_base = new VTBase();//将对象的首地址输出Vtpr_type* vptr = nullptr;//vptr是虚函数类的第一个成员变量,也是虚函数表的指针memcpy(&vptr, v_base, sizeof(Vtpr_type));
  • 虚函数的地址存储在了虚函数表中,虚函数表地址内的值,为虚函数的指针,我们依次获取虚函数表中的值,并执行
	VFunc funa = (VFunc)(*vptr);VFunc funb = (VFunc)(*(vptr + 1));VFunc func = (VFunc)(*(vptr + 2));funa();//VTBase::a()funb();//VTBase::b()func();//VTBase::c()

完整代码

#include 
using namespace std;
namespace {class VTBase {public:virtual void a() { cout << "VTBase::a()" << endl; }virtual void b() { cout << "VTBase::b()" << endl; }virtual void c() { cout << "VTBase::c()" << endl; }int          x, y;};typedef long long Vtpr_type;typedef void (*VFunc)(void); //定义一个函数指针类型变量类型 VFunc} // namespaceint main()
{//offsetofVTBase vtbase;std::cout << sizeof(vtbase) << endl;cout << offsetof(VTBase, x) << endl;cout << offsetof(VTBase, y) << endl;/*x偏移为8,y偏移为12所以前8个字节为虚函数表的大小,也是该类的第一个成员变量*/VTBase* v_base = new VTBase();//将对象的首地址输出Vtpr_type* vptr = nullptr;//vptr是虚函数类的第一个成员变量,也是虚函数表的指针memcpy(&vptr, v_base, sizeof(Vtpr_type));//虚函数的地址存储在了虚函数表中,虚函数表地址内的值,为虚函数的指针VFunc funa = (VFunc)(*vptr);VFunc funb = (VFunc)(*(vptr + 1));VFunc func = (VFunc)(*(vptr + 2));funa();funb();func();delete v_base;v_base = nullptr;return 0;
}

1.2多个类共用一份虚函数表释义

void TestVtprValue() {VTBase* v_base1 = new VTBase();VTBase* v_base2 = new VTBase();Vtpr_type* vptr1   = nullptr;Vtpr_type* vptr2   = nullptr;memcpy(&vptr1, v_base1, sizeof(Vtpr_type));memcpy(&vptr2, v_base2, sizeof(Vtpr_type));cout << vptr1 << endl;cout << vptr2 << endl;}

在这里插入图片描述

2.继承中的虚函数表

我们先定义一个派生类VTBaseExt,并且重写其中某一个虚函数

class VTBaseExt : public VTBase {public:virtual void b() { cout << "VTBaseExt::b()" << endl; }
};
void TestVtbExt()
{VTBase*    v_base    = new VTBase();Vtpr_type* vptr_base = nullptr;memcpy(&vptr_base, v_base, sizeof(Vtpr_type));cout << vptr_base << endl;VTBaseExt* v_base_ext = new VTBaseExt();Vtpr_type* vptr_ext   = nullptr;memcpy(&vptr_ext, v_base_ext, sizeof(Vtpr_type));cout << vptr_ext << endl;VFunc funa = (VFunc)(*(vptr_ext));VFunc funb = (VFunc)(*(vptr_ext + 1));VFunc func = (VFunc)(*(vptr_ext + 2));funa();//VTBase::a()funb();//VTBaseExt::b()func();//VTBase::c()
}

此时执行,我们发现VTBase、VTBaseExt虚表大小均为4,但虚表数组中储存的第二个值发生了变化
在这里插入图片描述

2.1剖析派生类调用基类的虚函数是否为同一个

从上图可知是同一个
在这里插入图片描述
获取虚函数表数组存储的函数地址

void TestVtbExt()
{VTBase*    v_base    = new VTBase();Vtpr_type* vptr_base = nullptr;memcpy(&vptr_base, v_base, sizeof(Vtpr_type));cout << vptr_base << endl;VTBaseExt* v_base_ext = new VTBaseExt();Vtpr_type* vptr_ext   = nullptr;memcpy(&vptr_ext, v_base_ext, sizeof(Vtpr_type));cout << vptr_ext << endl;//获取虚函数表数组地址for (int i = 0; i < 3; i++) {cout << *(vptr_base + i) << " " << *(vptr_ext + i) << endl;}
}

从图中可以看出来,只有被重写过的第二组是不一样的,其余的函数地址都没有变化
在这里插入图片描述

2.2基类指针指向派生类的虚函数表

调用代码释义:

void TestVtbPointer() {//基类指针指向自己VTBase*    v_base    = new VTBase();Vtpr_type* vptr_base = nullptr;memcpy(&vptr_base, v_base, sizeof(Vtpr_type));cout << vptr_base << endl;//派生类指针指向派生类VTBaseExt* v_base_ext = new VTBaseExt();Vtpr_type* vptr_ext   = nullptr;memcpy(&vptr_ext, v_base_ext, sizeof(Vtpr_type));cout << vptr_ext << endl;//基类指针指向派生类VTBase*    v_base_to_ext = new VTBaseExt();Vtpr_type* vptr_to_ext   = nullptr;memcpy(&vptr_to_ext, v_base_to_ext, sizeof(Vtpr_type));cout << vptr_to_ext << endl;
}

从结果输出来看,最终的基类指针指向派生类对象,虚函数表的指针还是指向了派生类的虚函数表
在这里插入图片描述

3.虚函数表总结

动态绑定的最终实现就是查虚函数表,每个对象的类对应了自己的虚函数表,虚函数表存储了与之对应的虚函数地址。

3.1动态绑定示意图

  • 假设含有虚函数的基类VTBase ;
  • VTBase 派生类VTBaseExt,重写了func_2;
  • VTBaseExt派生类VTBaseFinal ,新增了func_4;
class VTBase {
public:virtual void func_1() { cout << __FUNCTION__ << endl; }virtual void func_2() { cout << __FUNCTION__ << endl; }virtual void func_3() { cout << __FUNCTION__ << endl; }int          x;
};
class VTBaseExt : public VTBase {
public:virtual void func_2() { cout << "VTBaseExt:: " << __FUNCTION__ << endl; }
};
class VTBaseFinal : public VTBaseExt {
public:virtual void func_4() { cout << "VTBaseFinal:: " << __FUNCTION__ << endl; }
};

在这里插入图片描述

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...