• C++ 多态之虚函数表


    虚函数表概述

    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;    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 如果是x64编译,默认是8字节对齐 输出为4,16
    • 如果是x86编译,默认是4字节对齐 输出为4,8

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

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

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

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

    在这里插入图片描述

    1.1剖析虚表指针的调用

    这里还是以X64编译器举例

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

    完整代码

    #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
    
    } // namespace
    
    int main()
    {
    	//offsetof
    	VTBase 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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    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;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    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()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    此时执行,我们发现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;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

    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; }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

  • 相关阅读:
    Cron 表达式详解及最新版本使用
    手把手怎么把照片修复高清,p图小白也能轻松上手
    机器学习——奇异值分解(未完)
    2022年了!还在用定时器实现动画?赶紧试试requestAnimationFrame吧!
    数学建模笔记-第九讲-分类模型-逻辑回归
    Linux 系统性能瓶颈分析(超详细)
    算法训练第五十六天
    自动化运维:Ansible脚本之playbook剧本
    Spring MVC组件之HandlerAdapter
    Java回顾-比较器Comparable/Comparator
  • 原文地址:https://blog.csdn.net/u013052326/article/details/128052236