上篇对多态底层原理的博客最后,谈到了几个总结性的知识点,本篇博客对其进行一些证明
话不多说,马上开始今天的学习
上篇博客,我们说到
总结一下子类的虚表生成: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;
}
父类有两个虚函数,子类重写第一个,并且自己加了一个虚函数
接下来,我们打开监视窗口,看一下子类新增的虚函数有没有在子类虚表的后面
我们看到,父类的虚函数表中有Func1和Func2这两个虚函数,子类因为重写了Func1,所以子类的Func1,
函数内容不同,函数地址也不同
;Func2因为没有重写,所以还是父类的虚函数
。这和我们上述所说的,子类虚表的形成符合。
但是监视窗口并没有显示子类新增加的虚函数
,我们不妨用内存窗口
再看一下
我们看到,前两个函数指针的地址都可以对应上,而在第三个位置,还有一个地址相近的地址,这会不会就是子类新增的虚函数的函数指针呢?仅看内存表我们也还无法得出结论。
监视窗口和内存窗口,我们都无法看出子类新增的虚函数是否有进虚表的最后
因为是虚表存储的函数指针,我们可以尝试
获取这些函数指针
,然后调用函数
,为此,我们在每个虚函数内写入对应的打印
,当我们调用第三个地址,如果打印出子类新增虚函数的内容,那不就证明子类新增的虚函数会放在虚表的最后吗。
首先,我们编写一个函数用于打印虚表的内容,因为虚表是函数指针数组,我们可以将函数指针重命名一下,方面阅读。同时在虚表的第四个位置是空指针,所以终止条件就是访问到空指针
//函数指针
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的参数了
//先取地址,再强转成访问4字节的int指针,解引用获取4个字节
//再强转成函数指针的指针,因为函数指针数组本质也是函数指针的指针
Print((VF_PTR*)(*(int*)(&d)));
但是该方法有局限性,因为int*访问范围是4字节,规定死了,但是在64位下,指针是8字节,所以不匹配
这边就无法运行
这里稍作解释第二种写法
因为Print参数是VF_PTR [ ]
,函数指针数组,本质也是函数指针的指针
所以我们也可以取对象的地址,强转成VF_PTR**
,指针的指针,访问也是4字节
,然后解引用获取4个字节,并且变成VF_PTR*
,刚好匹配Print的参数
再其次,我们可以不用函数指针,直接使用void*,因为本质都是地址,所以可以替换,但需要使用一级指针替代函数指针,所以最终会变成三级指针,不推荐使用
void Print(void **table)
{
for (int i = 0; table[i]; i++)
{
printf("[%d]:%p\n", i, table[i]);
}
}
获取虚表后,因为虚表内部是函数指针,所以我们可以通过这些函数指针,调用对应的函数
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确实是增加到子类虚表的最后
我们在上篇博客也说了,
虚表是存储在常量区,也就是代码段
。接下来,我们简单证明一下
我们创建各个位置的变量,并将其地址进行打印,发现常量区的数据的地址和虚表的位置最接近,所以
虚表是在常量区
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。