【C++】多态结论证明

前言

上篇对多态底层原理的博客最后,谈到了几个总结性的知识点,本篇博客对其进行一些证明
话不多说,马上开始今天的学习

在这里插入图片描述

一. 子类的虚函数

上篇博客,我们说到
总结一下子类的虚表生成:a. 先将父类中的虚表内容拷贝一份到子类的虚表中。b. 如果子类重写了父类的中的某个虚函数,就用子类自己的函数覆盖虚表中父类的虚函数 c. 子类自己新增加的虚函数按其子类中的声明次序增加到子类虚表的最后
这里,我们说子类新增加的虚函数按其子类中的声明次序增加到子类虚表的最后

事实如此,但这个结论证明过程值得我们学习

接下来,我们尝试证明一下。
首先,我们看一下监视窗口有没有显现
我们定义这样一个父子类

class Base
{
    
    
public:
	virtual void Func1()
	{
    
    
		cout << "Base::Func1()" << endl;
	}

	virtual void Func2()
	{
    
    
		cout << "Base::Func2()" << endl;
	}


private:
	int _b = 1;
};

class Derive :public Base
{
    
    
public:
	virtual void Func1()
	{
    
    
		cout << "Derive::Func1()" << endl;
	}

	virtual void Func4()
	{
    
    
		cout << "Derive::Func4()" << endl;
	}
private:
	int _b = 2;
};

int main()
{
    
    
	Base b;
	Derive d;

	return 0;
}

父类有两个虚函数,子类重写第一个,并且自己加了一个虚函数

1. 调试窗口

接下来,我们打开监视窗口,看一下子类新增的虚函数有没有在子类虚表的后面
在这里插入图片描述

我们看到,父类的虚函数表中有Func1和Func2这两个虚函数,子类因为重写了Func1,所以子类的Func1,函数内容不同,函数地址也不同;Func2因为没有重写,所以还是父类的虚函数。这和我们上述所说的,子类虚表的形成符合。

但是监视窗口并没有显示子类新增加的虚函数,我们不妨用内存窗口再看一下

在这里插入图片描述
我们看到,前两个函数指针的地址都可以对应上,而在第三个位置,还有一个地址相近的地址,这会不会就是子类新增的虚函数的函数指针呢?仅看内存表我们也还无法得出结论。

2. 打印虚表

监视窗口和内存窗口,我们都无法看出子类新增的虚函数是否有进虚表的最后

因为是虚表存储的函数指针,我们可以尝试获取这些函数指针,然后调用函数,为此,我们在每个虚函数内写入对应的打印,当我们调用第三个地址,如果打印出子类新增虚函数的内容,那不就证明子类新增的虚函数会放在虚表的最后吗。

首先,我们编写一个函数用于打印虚表的内容,因为虚表是函数指针数组,我们可以将函数指针重命名一下,方面阅读。同时在虚表的第四个位置是空指针,所以终止条件就是访问到空指针
在这里插入图片描述

//函数指针
typedef void(*VF_PTR)();
//参数是函数指针数组
void Print(VF_PTR table[])
{
    
    
	//访问到空指针就终止循环,停止打印
	for (int i = 0; table[i]; i++)
	{
    
    
		printf("[%d]:%p\n", i, table[i]);
	}
}

接下来,我们就要思考如何获取虚表
我们想到,虚表是在对象的第一个位置,因为是指向函数指针数组的指针,本质还是指针,所以占用四个字节。所以我们可以取地址,再强转一下,使得指针的访问大小是4,再强转成函数指针数组,就可以匹配Print的参数了

(1). 第一种写法

//先取地址,再强转成访问4字节的int指针,解引用获取4个字节
//再强转成函数指针的指针,因为函数指针数组本质也是函数指针的指针
Print((VF_PTR*)(*(int*)(&d)));

在这里插入图片描述
但是该方法有局限性,因为int*访问范围是4字节,规定死了,但是在64位下,指针是8字节,所以不匹配
在这里插入图片描述
这边就无法运行

(2). 第二种写法

也可以
在这里插入图片描述

这里稍作解释第二种写法
因为Print参数是VF_PTR [ ] ,函数指针数组,本质也是函数指针的指针
所以我们也可以取对象的地址,强转成VF_PTR**,指针的指针,访问也是4字节,然后解引用获取4个字节,并且变成VF_PTR*,刚好匹配Print的参数

该方法就可以兼容64位,因为使用的是指针的访问范围
在这里插入图片描述

(3). 第三种写法

再其次,我们可以不用函数指针,直接使用void*,因为本质都是地址,所以可以替换,但需要使用一级指针替代函数指针,所以最终会变成三级指针,不推荐使用

void Print(void **table)
{
    
    
	for (int i = 0; table[i]; i++)
	{
    
    
		printf("[%d]:%p\n", i, table[i]);
	}
}

在这里插入图片描述
基本原理也同第二种方法,同样也兼容64位

3. 证明

获取虚表后,因为虚表内部是函数指针,所以我们可以通过这些函数指针,调用对应的函数

typedef void(*VF_PTR)();
void Print(VF_PTR table[])
{
    
    
	for (int i = 0; table[i]; i++)
	{
    
    
		printf("[%d]:%p\n", i, table[i]);
		//取出函数指针
		VF_PTR fun = table[i];
		//调用
		fun();
	}
}

在这里插入图片描述

我们看到,Func4确实是增加到子类虚表的最后

二. 虚表指针初始化

虚表是在编译的阶段产生的,我们一调试,虚表就已经存在
在这里插入图片描述
但是虚表指针此时并没有初始化,那么是在什么时候初始化的呢?

虚表指针的初始化其实是在初始化列表进行的
在这里插入图片描述

三. 虚表的存储

我们在上篇博客也说了,虚表是存储在常量区,也就是代码段。接下来,我们简单证明一下

在这里插入图片描述

我们创建各个位置的变量,并将其地址进行打印,发现常量区的数据的地址和虚表的位置最接近,所以虚表是在常量区

结束语

如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_72563041/article/details/129959800