从汇编看c++中含有虚基类对象的析构
c++中,当继承结构中含有虚基类时,在构造对象时编译器会通过将一个标志位置1(表示调用虚基类构造函数),或者置0(表示不调用虚基类构造函数)来防止重复构造虚基类子对象。如下图菱形结构所示:
当构造类Bottom对象时,Bottom构造函数里面的c++伪码如下(单考虑标志位,不考虑其他):
//Bottom构造函数伪码 flag = 1;//标志位 if (flag) { 调用虚基类Top的构造函数 } flag = 0;//标志位清零 调用Left的构造函数 flag = 0;//标志位清零 调用Right的构造函数 其他操作 //Left构造函数伪码 if (flag) { 调用虚基类Top的构造函数 } 其他操作 //right构造函数伪码: if (flag) { 调用虚基类Top的构造函数 } 其他操作
编译器通过这种方式,保证虚基类的构造函数不会重复调用,那么析构的时候,是不是也是通过这种标志位的方式呢?下面来看c++源码:
class Top { private: int _top; public: Top(int top = 0) : _top(top) {} virtual ~Top() {} virtual int get1() { return 1; } }; class Left : virtual public Top { private: int _left; public: Left(int left = 0) : _left(left) {} virtual ~Left() {} virtual int get2() { return 2; } }; class Right : virtual public Top { private: int _right; public: Right(int right = 0) : _right(right) {} virtual ~Right() {} virtual int get3() { return 3; } }; class Bottom : public Left, public Right { private: int _bottom; public: Bottom(int bottom = 0) : _bottom(bottom) {} virtual ~Bottom() {} virtual int get4() { return 4; } }; int main() { Bottom b; }
上面类之间的继承关系是一个菱形继承,下面就来看一下析构的时候汇编码。
析构的时候,并不直接调用Bottom的析构函数,而是先调用的析构代理函数,其部分相关的汇编码如下:
lea ecx,[b];将对象b的首地址给寄存器ecx 012814BD call Bottom::`vbase destructor' (1281019h);调用析构代理函数
析构代理函数函数的汇编码如下(只列出相关部分)
mov dword ptr [ebp-8],ecx ;寄存器ecx里面存放对象b首地址(this指针,即对象b首地址),将ecx的值给ebp-8所代表的的内存 01281C73 mov ecx,dword ptr [this];将this指针给寄存器ecx 01281C76 add ecx,1Ch;ecx里面的值加28byte,调整this指针,所指位置如图1所示 01281C79 call Bottom::~Bottom (1281055h) ;调用类Bottom的析构函数 01281C7E mov ecx,dword ptr [this];this指针给寄存器ecx 01281C81 add ecx,1Ch;ecx里面的 值加28byte,调整this指针,所指位置如图4所示 01281C84 call Top::~Top (1281096h) ;调用虚基类Top的析构函数
在析构代理函数里面先调用了Bottom的析构函数,然后调用虚基类Top的析构函数
Bottom的析构函数汇编码如下(只列出相关部分):
mov dword ptr [ebp-14h],ecx ;寄存器ecx里面存有this指针,所指位置如上图1,将其值存到ebp-14h所代表的内存中 00C13C52 mov eax,dword ptr [ebp-14h];将ebp-14h所代表的内存里面的值(this指针)给寄存器eax 00C13C55 mov dword ptr [eax-1Ch],offset Bottom::`vftable' (0C16758h);虚表首地址给向上偏移this指针28byte处内存,即对象b首地址处(设置第一处虚表) 00C13C5C mov eax,dword ptr [ebp-14h];将ebp-14h所代表的内存里面的值(即对象b的首地址)给寄存器eax 00C13C5F mov dword ptr [eax-10h],offset Bottom::`vftable' (0C1674Ch);虚表首地址给向上偏移this指针16byte处内存,即父类Right子对象首地址(设置第二处虚表) 00C13C66 mov eax,dword ptr [ebp-14h];将ebp-14h所代表的内存里面的值(即this指针)给寄存器eax 00C13C69 mov ecx,dword ptr [eax-18h];将向上偏移this指针24byte处内存内容(即vbtable首地址)给寄存器ecx 00C13C6C mov edx,dword ptr [ecx+4];将偏移vbtable首地址4byte处内存内容(即vbtable指针偏移虚基类Top子对象首地址的偏移量)给寄存器edx 00C13C6F mov eax,dword ptr [ebp-14h];将ebp-14h所代表的内存里面的值(即对象b的首地址)给寄存器eax 00C13C72 mov dword ptr [eax+edx-18h],offset Bottom::`vftable' (0C16740h);eax为this指针,edx为刚获得的偏移量 eax+edx-18h ;这里将虚表首地址给该内存(设置第三处虚表) 00C13C7A mov dword ptr [ebp-4],0 00C13C81 mov ecx,dword ptr [ebp-14h] ;将ebp-14h所代表的内存里面的值(即对象b的首地址)给寄存器ecx 00C13C84 sub ecx,4 ;ecx里面的值减4,调整this指针,此时this指针所指位置如图2 00C13C87 call Right::~Right (0C11091h);调用父类Right子对象的析构函数 00C13C8C mov dword ptr [ebp-4],0FFFFFFFFh 00C13C93 mov ecx,dword ptr [ebp-14h];将ebp-14h所代表的内存里面的值(即this指针)给寄存器ecx 00C13C96 sub ecx,10h ;ecx里面的值减16,调正this指针,this指针所指位置如图3 00C13C99 call Left::~Left (0C110DCh);调用父类Left子对象的析构函数 01281C20 mov dword ptr [ebp-8],ecx;ecx里面存放this指针,所指位置如图2,将其存放到 ebp-8所代表的内存 01281C23 mov eax,dword ptr [this];将this指针给寄存器eax 01281C26 mov dword ptr [eax-0Ch],offset Right::`vftable' (1286874h);将虚表首地址给向上偏移this指针12byte处内存,设置第一处虚表 01281C2D mov eax,dword ptr [this];将this指针给寄存器eax 01281C30 mov ecx,dword ptr [eax-8] ;将向上偏移this指针8byte处内存内容(即vbtable首地址)给寄存器ecx 01281C33 mov edx,dword ptr [ecx+4];将偏移vbtable首地址4byte处内存内容(即vbtable指针偏移虚基类Top首地址的偏移量),给寄存器edx 01281C36 mov eax,dword ptr [this] ;将this指针给寄存器eax 01281C39 mov dword ptr [eax+edx-8],offset Right::`vftable' (1286814h) ;eax是this指针,edx是偏移量,因此eax+edx-8即使虚基类Top子对象的首地址 ;这里将虚表首地址给该内存,设置第二处虚表
图1 图2 图3
在Bottom的析构函数里面,首先调用了Right的析构函数,然后调用了Left的析构函数。并且在调用这些函数之前,设置好了相关的虚表。
下面是Right析构函数的汇编码(只列出相关部分):
01281C20 mov dword ptr [ebp-8],ecx;ecx里面存放this指针,所指位置如图2,将其存放到 ebp-8所代表的内存
01281C23 mov eax,dword ptr [this];将this指针给寄存器eax
01281C26 mov dword ptr [eax-0Ch],offset Right::`vftable' (1286874h);将虚表首地址给向上偏移this指针12byte处内存,设置第一处虚表
01281C2D mov eax,dword ptr [this];将this指针给寄存器eax
01281C30 mov ecx,dword ptr [eax-8] ;将向上偏移this指针8byte处内存内容(即vbtable首地址)给寄存器ecx
01281C33 mov edx,dword ptr [ecx+4];将偏移vbtable首地址4byte处内存内容(即vbtable指针偏移虚基类Top首地址的偏移量),给寄存器edx
01281C36 mov eax,dword ptr [this] ;将this指针给寄存器eax
01281C39 mov dword ptr [eax+edx-8],offset Right::`vftable' (1286814h) ;eax是this指针,edx是偏移量,因此eax+edx-8即使虚基类Top子对象的首地址
;这里将虚表首地址给该内存,设置第二处虚表
在Right函数中并没有调用虚基类Top的析构函数,函数只是一开始的时候,设置好了相关的虚表。
下面是Left函数析构函数的汇编码(值列出相关部分):
01281880 mov dword ptr [ebp-8],ecx;ecx里面存放的this指针,所指位置如图3,将其存放到 ebp-8所代表的内存里面 01281883 mov eax,dword ptr [this];将this指针给寄存器eax 01281886 mov dword ptr [eax-0Ch],offset Left::`vftable' (1286794h);将虚表首地址给向上偏移this指针12byte处内存,设置第一处虚表 0128188D mov eax,dword ptr [this] ;将this指针给寄存器eax 01281890 mov ecx,dword ptr [eax-8] ;将向上偏移this指针8byte处内存内容(即vbtable首地址)给寄存器ecx 01281893 mov edx,dword ptr [ecx+4];将偏移vbtable首地址4byte处内存内容(即vbtable指针偏移虚基类Top首地址的偏移量),给寄存器edx 01281896 mov eax,dword ptr [this];将this指针给寄存器eax 01281899 mov dword ptr [eax+edx-8],offset Left::`vftable' (1286788h);eax是this指针,edx是偏移量,因此eax+edx-8即使虚基类Top子对象的首地址 ;这里将虚表首地址给该内存,设置第二处虚表
在Left函数里面也没有调用虚基类Top的析构函数,函数只是一开始的时候,设置好了相关的虚表
最后是虚基类Top的析构函数(只列出相关部分):
012816D0 mov dword ptr [ebp-8],ecx;寄存器ecx里面存有this指针,所指位置如图1所示 ,将this指针的值存入ebp-8所代表的内存 012816D3 mov eax,dword ptr [this];将this指针给寄存器eax 012816D6 mov dword ptr [eax],offset Top::`vftable' (128677Ch);将虚表首地址给this指针所指向的内存
通过上面的汇编码,可以发现,菱形结构中的析构函数并没有使用构造函数中的标记来防止重复析构,而是将虚基类Top的析构函数放到最后调用。在调用Left和Right的析构函数时,根本不调用虚基类Top的析构函数。虚基类Top的析构含仅仅由析构代理函数调用。