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成员)而普通函数,静态成员是不在类中的.
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
按照上面的说法,我们可以通过Base的实例来得到虚函数表。 下面是实际例程:
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << "虚函数表的指针的地址:" <<(int*)(&b)<< endl;
cout << "虚函数表的地址,同时也是 第一个虚函数地址:" << (int*)*(int*)(&b) << endl;
// Invoke the first virtual function
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
虚函数表地址:0012FED4
虚函数表 — 第一个函数地址:0044F148
Base::f
虚函数表的地址是一样的,也就是说一个类维护一张虚函数表,每个对象拥有一个虚函数表指针指向虚函数表
通过这个示例,我们可以看到,我们可以通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int* 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:
(Fun)*((int*)*(int*)(&b)+0); // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()
//测试虚函数表和虚函数指针
//基类每个对象都有独立的一个虚函数表指针,指向同一个虚函数表
//一个类只有一张虚函数表,所有对象共享
//派生类的虚函数表从基类继承过来,在虚函数表上修改虚函数指针
//派生类新增的虚函数不会在虚函数表中
#include <iostream>
using namespace std;
typedef void (*Func)();
class CBase
{
public:
static int m_gNum;
int m_nNum;
char m_c;
CBase():m_nNum(0), m_c(0){
cout << "call CBase()." << endl;
}
virtual void func1(){
cout << "call CBase::func1()" << endl;
}
virtual void func2(){
cout << "call CBase::func2()" << endl;
}
virtual ~CBase(){
cout << "call ~CBase()." << endl;
}
};
class CDerive : public CBase
{
public:
int m_nCount;
CDerive():CBase(), m_nCount(0){
cout << "call CDeriv()" << endl;
}
virtual void func2(){
cout << "call CDerive::func2()" << endl;
}
virtual void func3(){
cout << "call CDerive ::func3()" << endl;
}
virtual ~CDerive(){
cout << "call ~CDerive" << endl;
}
};
int main(void)
{
CBase base1, base2;
CDerive deriv1;
CBase *base3 = new CDerive();
CBase *base4 = new CBase();
cout << "base1:" << &base1 << endl;
cout << "base1:" << (int*)*(int*)&base1 << endl;
cout << "base2:" << &base2 << endl;
cout << "base2:" << (int*)*(int*)&base2 << endl;
cout << "deriv1:" << &deriv1 << endl;
cout << "deriv1:" << (int*)*(int*)&deriv1 << endl;
cout << "base3:" << base3 << endl;
cout << "base3:" << (int*)*(int*)base3 << endl;
cout << "base4:" << base4 << endl;
cout << "base4:" << (int*)*(int*)base4 << endl;
Func p = (Func)*((int*)*(int*)&base1);
p();
p = (Func)*((int*)*(int*)&deriv1);
p();
p = (Func)*((int*)*(int*)&deriv1 + 1);
p();
p = (Func)*((int*)*(int*)&deriv1 + 2);
p();//程序崩溃,调用了析构函数,不会调用新增的虚函数
return 0;
}