详解虚函数的实现过程之初探虚表(1)

空对象它有一字节的大小,在没有任何成员变量但是却有虚函数的对象里,它的大小是四个字节,这是为什么呢?

在这里插入图片描述
因为含有虚函数的对象里,对象的起始地址往后四个字节其实是 一个指针,它指向了一个数组,这个数组的元素是 指针,这些数组的元素指向的地方就是虚函数实现的地方,我们称这个数组叫做 虚表。而指向这个虚表的指针我们成为 虚表指针。这个对象为什么是4个字节的大小,也就是含有这个虚表指针。

对于开发者而言,虚表指针都是隐藏的,在常规的开发过程中,我们感受不到它们的存在,要想感知它们的存在,看看底层的汇编代码即可,或者通过c的形式,来实现虚函数的间接调用。

对象中的虚表指针和虚表的关系如下所示:
在这里插入图片描述
虚表指针的初始化过程:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
40108D~401090把虚表指针放在this指针所指的地方(也就是对象的起始地址),也就是对象开始的前四个字节存放的是虚表指针。

对象的虚表指针初始化是通过编译器在构造函数内插入代码来完成的,在用户没有编写构造函数时,由于必须初始化虚表指针,因此编译器会提供默认的构造函数,以完成虚表指针的初始化。
(这里也就印证了我们之前所发博客 (提供默认构造函数的第一点))

注意:
a.由于虚表信息在编译后会被链接到对应的执行文件中,因此所获得的虚表地址是一个相对固定的地址。
b.虚表中虚函数的地址的排列顺序依据虚函数在类中的声明顺序而定,先声明的虚函数的地址会被排列在虚表中靠前的位置。即第一个被声明的虚函数的地址在虚表的首地址处

虚表的元素所指向的函数是如何被进行调用的呢?
解释:
在虚表指针的初始化过程中,对象执行了构造函数后,就得到了虚表指针,当其它代码访问到这个对象的虚函数时,会根据对象的首地址,取出对应虚表元素。当函数被调用时,会间接访问虚表,得到对应 的虚函数首地址,并调用执行(记住,虚表指针是放在对象里面的,对象,对象,对象

虚表间接寻址寻址访问的情况只有在使用对象的指针或引用来调用虚函数的时候才会出现;如果直接使用对象来调用自身的虚函数时,没必要查表访问,因为已经明确调用的是自身成员函数,根本没有构成多态问题。

接下来我们看看如何调用自身类中的虚函数的:
在这里插入图片描述
在这里插入图片描述
它直接通过对象去调用了自身的成员函数,因此编译器使用了直接调用函数的方式,并未去访问虚表指针,并没有间接获取虚表指针。

仔细分析虚表指针后发现,编译器隐藏了虚表指针的初始化实现代码,当类中出现虚函数时,必须在构造函数中对虚表指针执行初始化操作,而没有虚函数的类对象在构造时,不会进行初始化虚表指针的操作。因此,在分析构造函数时,又增加了一个特性:虚表指针初始化。
如果排除开发者伪造编译器生成的代码来误导分析人员的可能,我们就可以给出一个结论:对于单线继承的类结构,在其某个成员函数中,将this的地址初始化虚表首指针时,那么我们可以断定,这个成员函数就是构造函数。

下面来分析一下含有虚表的对象析构函数
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
两者对虚表操作几乎相同,都是将虚表指针设置成当前对象所属类中的虚表首地址。两者看似相同,其实差别很大。
构造函数中完成的是初始化虚表指针的工作,执行前**虚表指针并没有指向虚表地址,**而执行析构函数时,其对象的虚表指针已经指向了某个虚表指针。

大家是否觉得这里是不是很没有必要呢?这里实际上是还原虚表指针,让其指向自身的虚表首地址,防止在析构函数中调用虚函数时取到非自身的虚表,从而导致函数调用错误
这里也就是前面博客所提到的,在构造函数和析构函数里面会进行虚表覆盖。不会产生多态。)

总结:
如何识别虚函数:
a.类中隐式定义了一个数据成员
b.该数据成员在首地址处,占4字节
c.构造函数会将此数据成员初始化为某个数组的首地址
d.这个地址属于数据区,是一个相对固定的地址
e.在这个数组内,每个数组成员都是函数指针
f.仔细观察这些函数,它们被调用时,第一个参数必然是this指针(要注意调用约定)
g.在这些函数内部,很有可能会对this指针使用相对间接的访问方式

虚函数系列:
详解虚函数的实现过程之初探虚表(1)
详解虚函数的实现过程之单继承(2)
详解虚函数的实现过程之多重继承(3)
详解虚函数的实现过程之虚基类(4)
详解虚函数的实现过程之菱形继承(5)

猜你喜欢

转载自blog.csdn.net/CSNN2019/article/details/111318544