12.1 识别类和类之间的关系(6)
我们来修改代码清单12-5中的构造函数和析构函数的实现过程,通过调试来分析其执行过程,如代码清单12-7所示。
代码清单12-7 构造函数和析构函数中调用虚函数的流程
- // 修改代码清单12-5后的示例,在构造函数与析构函数中添加虚函数调用
- class CPerson{ // 基类—"人"类
- public:
- CPerson(){
- ShowSpeak(); // 添加虚函数调用
- }
- virtual ~CPerson(){
- ShowSpeak(); // 添加虚函数调用
- }
- virtual void ShowSpeak(){
- printf("Speak No\r\n");
- }
- };
- // main函数实现过程
- void main(int argc, char* argv[]){
- CChinese Chinese;
- }
-
- // C++(www.cppentry.com)源码与汇编代码对比分析
- // Chinese 构造函数调用过程分析
- CChinese(){}
- 00401139 pop ecx ; 还原this指针
- 0040113A mov dword ptr [ebp-4],ecx
- 0040113D mov ecx,dword ptr [ebp-4] ; 传入当前this指针,将其作为父类的this指针
- 00401140 call @ILT+30(CPerson::CPerson) (00401023) ; 调用父类构造函数
- ; 执行父类构造函数后,将虚表设置为子类的虚表
- 00401145 mov eax,dword ptr [ebp-4] ; 获取this指针,这个指针也是虚表指针
- 00401148 mov dword ptr [eax],offset CChinese::'vftable' (0042201c)
- ; 设置虚表指针为子类的虚表
- 0040114E mov eax,dword ptr [ebp-4] ; 将返回值设置为this指针
- // 父类构造函数分析
- CPerson(){}
- 00401199 pop ecx ; 还原this指针,此时指针为子类对象的首地址
- 0040119A mov dword ptr [ebp-4],ecx
- 0040119D mov eax,dword ptr [ebp-4] ; 取出子类的虚表指针,设置为父类虚表
- 004011A0 mov dword ptr [eax],offset CPerson::'vftable' (00422028)
- ShowSpeak();
- 004011A6 mov ecx,dword ptr [ebp-4] ; 虚表是父类的,可以直接调用父类虚函数
- 004011A9 call @ILT+15(CPerson::ShowSpeak) (00401014)
- 004011C1 ret
-
- // Chinese 析构函数调用过程分析
- virtual ~CChinese(){}
- 00401309 pop ecx ; 还原this指针
- 0040130A mov dword ptr [ebp-4],ecx
- 0040130D mov eax,dword ptr [ebp-4] ; 再次设置子类的虚表
- 00401310 mov dword ptr [eax],offset CChinese::'vftable' (0042201c)
- 00401316 mov ecx,dword ptr [ebp-4] ; 调用父类的析构函数
- 00401319 call @ILT+20(CPerson::~CPerson) (00401019)
- // 父类析构函数分析
- virtual ~CPerson(){
- 004012B9 pop ecx
- 004012BA mov dword ptr [ebp-4],ecx
- 004012BD mov eax,dword ptr [ebp-4]
- ; 由于当前虚表指针指向了子类虚表,需要重新修改为父类虚表,以防止调用子类的虚函数
- 004012C0 mov dword ptr [eax],offset CPerson::'vftable' (00422028)
- ShowSpeak();
- 004012C6 mov ecx,dword ptr [ebp-4] ; 虚表是父类的,可以直接调用父类虚函数
- 004012C9 call @ILT+15(CPerson::ShowSpeak) (00401014)
- }
- 004012DE ret
在代码清单12-7的子类构造函数代码中,首先调用了父类的构造函数,然后设置虚表指针为当前类的虚表首地址。而析构函数中的顺序却与构造函数相反,首先设置虚表指针为当前类的虚表首地址,然后再调用父类的析构函数。其构造和析构的过程描述如下:
通过上面的分析可知构造和析构的顺序如下:
构造:基类→基类的派生类→……→当前类
析构:当前类→基类的派生类→ ……→基类
在代码清单12-5中,析构函数被定义为虚函数。为什么要将析构函数定义为虚函数呢?由于可以使用父类指针保存子类对象的首地址,因此当使用父类指针指向子类堆对象时,就会出问题。当使用delete释放对象的空间时,如果析构函数没有被定义为虚函数,那么编译器将会按指针的类型调用父类的析构函数,从而引发错误。而使用了虚析构函数后,会访问虚表并调用对象的析构函数。两种析构函数的调用过程如以下代码所示。
- // 没有声明为虚析构函数
- CPerson * pPerson = new CChinese;
- delete pPerson; // 部分代码分析略
- mov ecx,dword ptr [ebp-1Ch] ; 直接调用父类的析构函数
- call @ILT+10(CPerson::'scalar deleting destructor') (0040100f)
-
- // 声明为虚析构函数
- CPerson * pPerson = new CChinese;
- delete pPerson; // 部分代码分析略
- mov ecx,dword ptr [ebp-1Ch] ; 获取pPerson并保存到ecx中
- mov edx,dword ptr [ecx] ; 取得虚表指针
- mov ecx,dword ptr [ebp-1Ch] ; 传递this指针
- call dword ptr [edx] ; 间接调用虚析构函数