• c++虚表学习2


    前文

    1. 本文会让读者明白虚表原理,
    2. 理解类的大致内存结构。棱形继承下内存布局,和虚继承单一内存布局情况。
    3. 父类构造函数调用虚函数会怎么样
    4. 父类析构函数调用虚函数情况
    5. 为什么析构函数一定要是虚函数

    单继承

    我们首先参阅如下的代码:

    class Person {
    public:
    	int pFlag = 2;
    	Person() {
    		printf("Person \r\n");
    	}
    	virtual ~Person() {
    		printf(" ~Person \r\n");
    	}
    	virtual void vSayPerson() {
    		printf("Person \r\n");
    	}
    
    	void nSayPerson() {
    		printf("nSayPerson \r\n");
    	}
    };
    class XH :public Person {
    public:
    	int xhFlag = 4;
    	virtual ~XH() {
    		printf("~XH \r\n");
    	} 
    	XH() {
    		printf("XH \r\n");
    	}
    	virtual void vSayXH() {
    		printf("vSayXH \r\n");
    	}
    	virtual void vSayXH2() {
    		printf("vSayXH2 \r\n");
    	}
    	void nSayXH() {
    		printf("nSayXH \r\n");
    	}
    
    };
    int main()
    {
    	{
    		XH* pXh = new XH();
    		pXh->nSayPerson();
    		pXh->vSayPerson();
    		pXh->vSayXH2();
    		pXh->nSayXH();
    		delete pXh;
    	}
    	
    
    	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
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    在这里插入图片描述

    Debug编译后的汇编代码

    在这里插入图片描述
    我们跟进到构造函数中:

    在这里插入图片描述

    我们按照上面的顺序逐个分析

    1. 调用父类的构造函数。因为子类可能会用到父类的东西
    2. 给自己虚表赋值,注意虚表在内存首地址。
    3. 给自己变量赋值 。在上面你注意 [eax+8] 这个地址不是eax+4,证明中间还有其他东西。这里存放的是父类的局部变量
    4. 调用自身构造方法内的函数体

    我们首先构建出整个内存图:
    在这里插入图片描述
    构造函数要先调父类的初始化函数:
    因为子类会用到父类资源,比如子类获取父类的变量

    先初始化虚表指针在调用属性初始化和方法体:
    因为构造函数会有可能调用虚函数

    先初始化属性在调用方法体:
    方法体可能会获取属性

    我们首先虚表的赋值代码:
    在这里插入图片描述

    在这里插入图片描述

    我们接下来看下Person这个类的初始化函数

    在这里插入图片描述
    在这里插入图片描述
    你会差异的发现父类构造也会填入自己虚表,完成父类构造的后,子类又会覆盖写入这个虚表地址。
    这样会有什么关系和异常呢?假设子类重写父类的虚函数,在父类构造函数调用虚函数只会调用自己的函数而不是子类的。

    我们看下XH虚表地址的交叉引用信息
    在这里插入图片描述
    我们观察下XH析构函数:
    在这里插入图片描述
    为啥需要在自己析构函数中再次给自己的虚表赋值呢?为了解答这个答案我们首先才看~Person析构

    在这里插入图片描述
    在析构对象流程,首先释放子类的所有子类资源,在释放父类所有资源。因为子类资源被释放了,如果调用到父类时虚表没有还原父类的虚表,那么父类析构中有调用虚函数的可能会引起意外的异常。因为指向的函数是一个释放资源的子类函数。

    我们最后看看几个虚函数和非虚函数的调用

    	   pXh->nSayPerson();
    		pXh->vSayPerson();
    		
    
    • 1
    • 2
    • 3

    在这里插入图片描述
    首先我们要知道的是XH的虚表中第二项就是vSayPerson函数地址,第一项是析构函数代理函数地址。
    在这里插入图片描述
    现在你应该对虚函数的调用有一定的认识了吧。现在你应该举一反三回答出为啥析构函数为啥一定要是虚函数了。。。

    我们现在重写Person虚函数看看XH的虚表会怎么样.

    
    class XH :public Person {
    public:
    	int xhFlag = 4;
    	virtual ~XH() {
    		printf("~XH \r\n");
    	}
    	XH() {
    		printf("XH \r\n");
    	}
    	virtual void vSayXH() {
    		printf("vSayXH \r\n");
    	}
    	virtual void vSayXH2() {
    		printf("vSayXH2 \r\n");
    	}
    	void nSayXH() {
    		printf("nSayXH \r\n");
    	}
    	//重写
    	virtual void vSayPerson() {
    		printf("XH vSayPerson\r\n");
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述

    多继承

    class BaseClass {
    public:
    	int baseFlag = -1;
    	int fill[32] = {0};
    	BaseClass(int flag) {
    		baseFlag = flag;
    		printf("BaseClass \r\n");
    	}
    	virtual ~BaseClass() {
    		printf(" ~BaseClass \r\n");
    	}
    };
    class Person :  public  BaseClass {
    public:
    	int pFlag = 2;
    	Person() :BaseClass(2) {
    		printf("Person \r\n");
    	}
    	virtual ~Person() {
    		printf(" ~Person \r\n");
    	}
    	virtual void vSayPerson() {
    		printf("Person \r\n");
    	}
    
    	void nSayPerson() {
    		printf("nSayPerson \r\n");
    	}
    };
    
    class Female : public  BaseClass {
    public:
    	int fFlag = 3;
    	Female() :BaseClass(3) {
    		printf("Female \r\n");
    	}
    
    	virtual ~Female() {
    		printf(" ~Female \r\n");
    	}
    	virtual void vSayFemale() {
    		printf("vSayFemale \r\n");
    	}
    
    	void nSayFemale() {
    		printf("nSayFemale \r\n");
    	}
    };
    
    
    
    
    class XM :public  Person, public Female {
    public:
    	virtual void vSayFemale() {
    		printf("vSayFemale \r\n");
    	}
    
    	virtual ~XM() {
    		printf("~XM \r\n");
    	}
    	XM() {
    		printf("XM \r\n");
    	}
    	virtual void sayXm() {
    		printf("sayXm \r\n");
    	}
    };
    
    
    
    
    
    
    int main()
    {
    	XM* pXm = new XM();
    	//280
    	printf("%d \r\n", sizeof XM);
    	delete pXm;
    	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
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    在这里插入图片描述
    我们可以看到多继承下XM输出类大小是280字节,我们改用虚继承

    class BaseClass {
    public:
    	int baseFlag = -1;
    	int fill[32] = {0};
    	BaseClass(int flag) {
    		baseFlag = flag;
    		printf("BaseClass \r\n");
    	}
    	virtual ~BaseClass() {
    		printf(" ~BaseClass \r\n");
    	}
    };
    class Person : virtual public  BaseClass {
    public:
    	int pFlag = 2;
    	Person() :BaseClass(2) {
    		printf("Person \r\n");
    	}
    	virtual ~Person() {
    		printf(" ~Person \r\n");
    	}
    	virtual void vSayPerson() {
    		printf("Person \r\n");
    	}
    
    	void nSayPerson() {
    		printf("nSayPerson \r\n");
    	}
    };
    
    class Female :virtual  public  BaseClass {
    public:
    	int fFlag = 3;
    	Female() :BaseClass(3) {
    		printf("Female \r\n");
    	}
    
    	virtual ~Female() {
    		printf(" ~Female \r\n");
    	}
    	virtual void vSayFemale() {
    		printf("vSayFemale \r\n");
    	}
    
    	void nSayFemale() {
    		printf("nSayFemale \r\n");
    	}
    };
    
    
    
    
    class XM :public  Person, public Female {
    public:
    	virtual void vSayFemale() {
    		printf("vSayFemale \r\n");
    	}
    
    	virtual ~XM() {
    		printf("~XM \r\n");
    	}
    	XM():BaseClass(0x123) {
    		printf("XM \r\n");
    	}
    	virtual void sayXm() {
    		printf("sayXm \r\n");
    	}
    };
    
    
    
    
    
    
    int main()
    {
    	XM* pXm = new XM();
    	//280
    	printf("%d \r\n", sizeof XM);
    	delete pXm;
    	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
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    在这里插入图片描述

    普通多继承类内存视图

    可见虚继承减少部分内存,我们首先研究下分非虚继承下的XM内存结构

    在这里插入图片描述

    我们直接查看XM的构造函数

    在这里插入图片描述

    析构方法在这里插入图片描述

    多继承类内存视图

    我们知道虚继承可减少共同父类占用空间,比如本例中XM类会有两个BaseClass类,因此我们会想280减去一个Base类大小就是虚继承后的大小。具体数值为 144=280-136,但是我们通过允许后发现实际内存是160大小。

    //..其他代码略,这里是虚继承的代码
    int main()
    {
    	XM* pXm = new XM();
    
    	printf("sizeof XM %d \r\n", sizeof XM);
    	printf("sizeof Female %d \r\n", sizeof Female);
    	printf("sizeof Person %d \r\n", sizeof Person);
    	printf("sizeof BaseClass %d \r\n", sizeof BaseClass);
    	delete pXm;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述
    我们很明显发现FemalePerson大小变大了8字节.

    为了研究这个问题们修改以下代码首先构造一个Female

    
    int main()
    {
    	Female* pXm = new Female();
    
    	printf("sizeof XM %d \r\n", sizeof XM);
    	printf("sizeof Female %d \r\n", sizeof Female);
    	printf("sizeof Person %d \r\n", sizeof Person);
    	printf("sizeof BaseClass %d \r\n", sizeof BaseClass);
    	delete pXm;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们这里为方便理解直接给出虚继承内存结构:
    在这里插入图片描述
    IDA PRO 查看构造函数你会发现这个代码会有分支结构,有可能不会调用虚基类初始化函数
    在这里插入图片描述
    在这里插入图片描述
    我们最后再看看XM这个类的内存结构体:

    在这里插入图片描述
    在这里插入图片描述

  • 相关阅读:
    docker使用前的配置
    记一次中间件宕机以后持续请求导致应用OOM的排查思路(server.max-http-header-size属性配置不当的严重后果)
    快速掌握Golang单元测试与断言教程
    【PAT甲级 - C++题解】1137 Final Grading
    vue中属性的基本用法
    16.ReentrantLock全解读
    皕杰报表之隐藏处理
    【算法基础】基础算法(三)--(双指针算法、位运算、离散化、区间合并)
    exists与not extists详细解释
    Android编译系统apk并进行系统签名安装
  • 原文地址:https://blog.csdn.net/qfanmingyiq/article/details/126325438