12.2 多重继承(3)
在代码清单12-10中,在转换CBed指针时,会调整首地址并跳过第一个父类所占用的空间。这样一来,当使用父类CBed的指针访问CBed中实现的虚函数时,就不会错误地寻址到继承自CSofa类的成员变量。
了解了多重继承中子类的构造函数,以及父类指针的转换过程后,接下来通过分析代码清单12-11来学习多重继承中子类对象的析构过程。
代码清单12-11 多重继承的类对象析构函数—Debug版
- ; 子类析构函数的实现过程
- virtual ~CSofaBed(){ // 沙发床类的虚析构函数
- ; 部分代码略
- 0040170E pop ecx ; 还原this指针
- 0040170F mov dword ptr [ebp-10h],ecx
- 00401712 mov eax,dword ptr [ebp-10h]
- ; 将两个虚表指针设置为各个父类的虚表首地址
- 00401715 mov dword ptr [eax],offset CSofaBed::'vftable' (00426198)
- 0040171B mov ecx,dword ptr [ebp-10h]
- 0040171E mov dword ptr [ecx+8],offset CSofaBed::'vftable' (0042501c)
- 00401725 mov dword ptr [ebp-4],0
- ; 执行子类虚函数内的代码
- printf("virtual ~CSofaBed()\r\n");
- }
- ; 比较对象地址,与子类对象转为父类指针相似
- 00401739 cmp dword ptr [ebp-10h],0 ; 当this==NULL时不需调整
- 0040173D je CSofaBed::~CSofaBed+6Ah (0040174a)
- 0040173F mov edx,dword ptr [ebp-10h]
- 00401742 add edx,8
- 00401745 mov dword ptr [ebp-14h],edx ; 将调整后的this指针保存到[ebp-14h]
- 00401748 jmp CSofaBed::~CSofaBed+71h (00401751)
- 0040174A mov dword ptr [ebp-14h],0
- 00401751 mov ecx,dword ptr [ebp-14h]
- ; 调用父类CBed的析构函数
- 00401754 call @ILT+75(CBed::~CBed) (00401050)
- 00401759 mov dword ptr [ebp-4],0FFFFFFFFh
- 00401760 mov ecx,dword ptr [ebp-10h]
- ; 无需转换this指针,直接调用父类CSofa的析构函数
- 00401763 call @ILT+125(CSofa::~CSofa) (00401082)
- 00401768 mov ecx,dword ptr [ebp-0Ch]
- 0040176B mov dword ptr fs:[0],ecx
- ; 部分代码略
- 00401782 ret
代码清单12-11演示了对象SofaBed的析构过程。由于具有多个同级父类(多个同时继承的父类),因此在子类中产生了多个虚表指针。在对父类进行析构时,需要设置this指针,用于调用父类的析构函数。由于具有多个父类,当在析构的过程中调用各个父类的析构函数时,传递的首地址将有所不同,编译器会根据每个父类在对象中占用的空间位置,对应地传入各个父类部分的首地址作为this指针。
在Debug版下,由于侧重调试功能,因此使用了两个临时变量来分别保存两个this指针,它们对应的地址分别为两个虚表指针的首地址。在Release版下,虽然会进行优化,但原理不变,子类析构函数调用父类的析构函数时,仍然会传入在对象中父类对应的地址,当做this指针。
前面讲解了多重继承中子类对象的生成与销毁过程,以及在内存中的分布情况,对比单继承类,两者特征总结如下:
单继承类
在类对象占用的内存空间中,只保存一份虚表指针。
由于只有一个虚表指针,对应的也只有一个虚表。
虚表中各项保存了类中各虚函数的首地址。
构造时先构造父类,再构造自身,并且只调用一次父类构造函数。
析构时先析构自身,再析构父类,并且只调用一次父类析构函数。
多重继承类
在类对象所占用的内存空间中,根据继承父类的个数保存对应的虚表指针。
根据所保存的虚表指针的个数,对应产生相应个数的虚表。
转换父类指针时,需要跳转到对象的首地址。
构造时需要调用多个父类构造函数。
构造时先构造继承列表中第一个父类,然后依次调用到最后一个继承的父类构造函数。
析构时先析构自身,然后以与构造函数相反的顺序调用所有父类的析构函数。
当对象作为成员时,整个类对象的内存结构和多重继承很相似。当类中无虚函数时,整个类对象内存结构和多重继承完全一样,可酌情还原;当父类或成员对象存在虚函数时,通过观察虚表指针的位置和构造函数、析构函数中填写虚表指针的数目及目标地址,来还原继承或成员关系。
在对象模型的还原过程中,可根据以上特性识别出继承关系。对于有虚函数的情况,可利用虚表的初始化,使用IDA中的引用参考进行识别还原。引用参考的使用请回顾第11章的相关内容。