12.1 识别类和类之间的关系(12)
显而易见,这是一个析构函数的代理,它的任务是负责调用析构函数,然后根据参数值调用delete。将这个函数重命名为_Destructor_4011E0,重命名后,虚表结构是这个样子:
- .rdata:0040C0D0 vTable_40C0D0 dd offset _Destructor_4011E0
- .rdata:0040C0D4 pfnShowShtring dd offset ShowShtring
- .rdata:0040C0D8 pfnGetCChinese dd offset GetCChinese
- _Destructor_4011E0函数是虚表的第一项,我们可以回到main函数中来观察其参数传递的过程:
- .text:00401103 test esi, esi
- ; 当对象指针esi不为0时执行_Destructor_4011E0
- .text:00401105 jz short loc_40110F
- .text:00401107 mov edx, [esi] ; edx获得虚表
- .text:00401109 push 1 ; 传递参数值1
- .text:0040110B mov ecx, esi ; 传递this指针
- .text:0040110D call dword ptr [edx] ; 调用_Destructor_4011E0
- .text:0040110F
- .text:0040110F loc_40110F:
在main函数中调用虚表第一项时传递的值为1,那么在_Destructor_4011E0函数中,执行完析构函数后就会调用delete释放对象的内存空间。为什么要用这样一个参数来控制函数内释放空间的行为呢?为什么不能直接释放呢?
因为析构函数和释放堆空间是两回事,有的程序员喜欢自己维护析构函数,或者反复使用同一个堆对象,这时显式调用析构函数的同时不能释放堆空间,如下代码所示:
- void main(int argc, char* argv[]){
- CPerson *pPerson = new CChinese;
- pPerson->ShowSpeak();
- pPerson->~CPerson(); // 显式调用析构函数
-
- // 将堆内存中pPerson指向的地址作为CChinese的新对象的首地址,并调用CChinese的构造函数。这
- // 样可以重复使用同一个堆内存,以节约内存空间
- pPerson = new(pPerson)CChinese();
- delete pPerson;
- }
由于显式调用析构函数时不能马上释放堆内存,因此在析构函数的代理函数中通过一个参数来控制是否释放内存,以便于程序员自己管理析构函数的调用。这个代理函数的反汇编代码很简单,请读者自己上机验证。
在通过分析反汇编代码来识别类关系时,对于含有虚函数的类而言,利用IDA的交叉参考功能可简化分析识别过程。根据以上分析可知,具有虚函数,必然存在虚表指针。为了初始化虚表指针必然要准备构造函数,有了构造函数就可利用以上方法,顺藤摸瓜得到类关系,还原出对象模型。
思考题 大家在调试以上程序时会发现,比如CChinese的对象,在构造函数执行时虚表已经初始化完成了,在析构函数执行时,其虚表指针已经是子类的虚表了,为什么编译器还要在析构函数中再次将虚表设置为子类虚表呢?这是冗余操作吗?如果不这么做,会引发什么后果?答案见本章小结。