首先从带有虚函数的基类派生讲起。
class Base { public: Base(){base = 1; cout<<"Base!\n";} virtual ~Base(){cout<<"~Base!\n";} virtual void say(){cout<<"hello Base\n";} int base; }; class Derive : public Base { public: Derive(){derive = 2;} virtual ~Derive(){cout<<"~Derive!\n";} virtual void say(){cout<<"hello Derive\n";} int derive; }; int _tmain(int argc, _TCHAR* argv[]) { Base *pd=new Derive; pd->say(); delete pd; return 0; }
这是一个很简单的派生的例子,首先从内存角度看下,期中0x003B94B0为指针pd的值
可以看到最开始的四个字节的值为0x00417700, 后面是两个int型值分别为1和2,代表着base和derive的值,这两个值是在构造函数里进行赋值的,现在主要看下0x00417700这个值代表着什么?
0x00417700这个内存位置的值是0x004112c6,在继续跟下0x004112c6这个内存位置有什么东西
004112C6 jmp Derive::`scalar deleting destructor' (414980h)
可以看到这个地方是跳到一个好像是Derive的析构函数处,其实进去之后会发现其实他是一个代理函数,在此函数中先调用Derive的析构函数,然后在调用operator delete 释放内存。可以通过反汇编代码来看下:
Derive::`scalar deleting destructor':
00414980 push ebp
。。。。
0041499F pop ecx
004149A0 mov dword ptr [ebp-8],ecx
004149A3 mov ecx,dword ptr [this]
004149A6 call Derive::~Derive (4112B2h)
004149AB mov eax,dword ptr [ebp+8]
004149AE and eax,1
004149B1 je Derive::`scalar deleting destructor'+3Fh (4149BFh)
004149B3 mov eax,dword ptr [this]
004149B6 push eax
004149B7 call operator delete (4110D2h)
中间的省略掉了,无关紧要,主要是将开辟的一段栈空间的内容初始化为0xcccccccc,在红色处可以看到Derive的析构函数的调用。现在在回到开头处,用一张图示意下
可以看到Derive类型的对象的内存布局,首先是一个虚表指针,然后是基类的成员变量最后才是自己的成员变量
现在来观看下这个指向虚表的指针式什么时候安插到对象的头部的,我们通过派生类的构造函数来看下:
Derive()
004116A0 push ebp
。。。。。。
004116BF pop ecx
004116C0 mov dword ptr [ebp-8],ecx //ecx为this的值
004116C3 mov ecx,dword ptr [this]
004116C6 call Base::Base (41115Eh)
004116CB mov eax,dword ptr [this]
004116CE mov dword ptr [eax],offset Derive::`vftable' (417700h)
通过观看构造函数的反汇编代码可以看到,在派生类的构造函数中先是调用基类Base的构造函数,现在我们跟到基类的构造函数中去:
Base():
004117B0 push ebp
。。。。。
004117CF pop ecx
004117D0 mov dword ptr [ebp-8],ecx
004117D3 mov eax,dword ptr [this]
004117D6 mov dword ptr [eax],offset Base::`vftable' (417718h)
004117DC mov eax,dword ptr [this]
004117DF mov dword ptr [eax+4],1
004117E6 push offset string "Base!\n" (417708h)
004117EB mov eax,dword ptr [__imp_std::cout (41B328h)]
004117F0 push eax
004117F1 call std::operator<<<std::char_traits<char> > (4111A4h)
通过基类的构造函数可以看到,他将offset Base::`vftable' (417718h)基类的虚表地址赋给了对象的头四个字节,然后才是执行基类构造函数中的代码,现在我们可以看到pd头部的四个字节的值不是417700h而是417718h,奇怪了别急,后面会看到417700h这个值是赋给pd头部的四个字节。基类的构造函数执行完毕之后回到派生类的构造函数,此事可以看到紧接着就是两个赋值语句
004116CB mov eax,dword ptr [this]
004116CE mov dword ptr [eax],offset Derive::`vftable' (417700h)
首先是将this指针的值赋给eax,然后将offset Derive::`vftable' (417700h)派生类的虚表的指针赋给eax所指向的地址的头四个字节,也就是赋给pd所指的派生类对象的头四个字节,现在到此也就分析完了其内存布局,以及虚表指针的赋值过程。