转自:https://i-blog.csdnimg.cn/blog_migrate/731336c58f3ea5671bbcdfdf202a6460.png
1.虚函数
·虚表是怎么实现的?虚表存放在哪里?
·虚表中的数据是在什么时候确定的?
·对象中的虚表指针又在什么时候赋值的?
我们很难通过 C++语言本身来找到答案。 C++标准给编译器实现者定义了语法规范,但是被并没有定义如何实现这些语法规范,不同的编译器实现者可能有不同的实现方法,可以肯定的是他们的编译器必须符合这些语法规范。汇编语言作为最接近机器语言的计算机语言,可以为我们揭示一些隐藏在编译器内部的细节。接下来本来就试图通过对 C++源码进行反汇编的方式来解答这些疑惑。
二、分析
这里我选用 WinXP 和 VS2008 作为我们这次分析的平台。我们建立一个最简单的 Win32 控制台程序,并定义两个简单的类:
接下来我们可以直接编译这些 C++源码就可以得到相应的汇编代码。 通过分析这些汇编代码我们就找到许多有用的信息。我们可以找到这样的汇编代码:
以上的汇编代码定义了两个数据段, 而这两个数据段中的内容恰好就是类的虚表。 至此虚表的"庐山真面目"完全展示在我们的面前。 根据这些信息,我们可以推理出很多有用的结论:
·拥有虚函数的类会有一个虚表,而且这个虚表存放在类定义模块的数据段中。模块的数据段通常存放定义在该模块的全局数据和静态数据,这样我们可以把虚表看作是模块的全局数据或者静态数据
·类的虚表会被这个类的所有对象所共享。类的对象可以有很多,但是他们的虚表指针都指向同一个虚表,从这个意义上说,我们可以把虚表简单理解为类的静态数据成员。值得注意的是,虽然虚表是共享的,但是虚表指针并不是,类的每一个对象有一个属于它自己的虚表指针。
·虚表中存放的是虚函数的地址。
另外一个大的疑惑就是对象的虚表指针是在什么时候被赋值的? 我们都知道,类的对象是通过构造函数来完成初始化,但是我们从来没有在构造函数中初始化虚表指针, 那么编译器在幕后又做了哪些事情呢? 我们依然还是通过反汇编来找到答案。 在这个控制台程序的 main 函数中我们构建一个类对象:
类的非静态成员函数调用时,编译器会传入一个"隐藏"的参数。 这个参数就是通常我们说的"this"指针,它的值就是对象的地址。 在上面的代码中,寄存器 ECX 保存的就是这个"
this" 指 针 , 同 时 它 的 值 又 赋 给 了 寄 存 器 EAX。"??_7CD-szBase@@6B@"就是上面提到的虚表,同时它也代表了虚表的地址。
接下来,虚表的地址被赋给了由寄存器 EAX 指定的内存中。由此可见,虚表的地址被存放在对象的起始位置,即对象的第一个数据成员就是它的虚表指针。 同时我们还可以注意到,虚表指针的初始化确实发生在构造函数的调用过程中, 但是在执行构造函数体之前,即进入到构造函数的"{"和"}"之前。 为了更好的理解这一问题, 我们可以把构造函数的调用过程细分为两个阶段,即:
1.进入到构造函数体之间。在这个阶段如果存在虚函数的话,虚表指针被初始化。如果存在构造函数的初始化列表的话,初始化列表也会被执行。
2.进入到构造函数体内。这一阶段是我们通常意义上说的构造函数
简单的搞个基类Base{void fun();virtual void print(){...};public:int a;static b;}
定义一个对象 B b;调试状态下就可以看到b包含了什么
类中只有虚表指针和普通成员(包括const成员)而普通函数,静态成员是不在类中的.