1. 程式人生 > >C++中解構函式為虛擬函式時呼叫發生了什麼變化

C++中解構函式為虛擬函式時呼叫發生了什麼變化

昨天去XX公司面試,面試官問了一個關於C++類解構函式為虛擬函式時,如果是父類的指標用子類來new,如果發生析構時,解構函式是virtual與不是virtual有什麼區別。當時答的不好,回來總結了一下,在機器上實現了一遍,終於搞明白了。記錄下來,以後遇到這種情況自己一定不要犯錯了

一、先看第一種最簡單的情況吧,教科書上教的,解構函式不是virtual,正常定義一個子類物件

class student
{
public:
	int *m_pInt;
	student()
	{
		m_pInt = new int[10];   //1
		memset(m_pInt, 0, 10*4);
	}

	~student()
	{				//3
		delete []m_pInt;
	}

};

class GradeOneStue:public student
{
public:
	int m_iNum;
	GradeOneStue()			
	{				//2
		m_iNum = 1;
	}

	~GradeOneStue()
	{				//4
		m_iNum = 0;
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	GradeOneStue gd;
	return 0;
}
這時構造順序是先1後2,下面是反彙編程式碼
GradeOneStue()			
00411470  push        ebp  
00411471  mov         ebp,esp 
00411473  sub         esp,0CCh 
00411479  push        ebx  
0041147A  push        esi  
0041147B  push        edi  
0041147C  push        ecx  
0041147D  lea         edi,[ebp-0CCh] 
00411483  mov         ecx,33h 
00411488  mov         eax,0CCCCCCCCh 
0041148D  rep stos    dword ptr es:[edi] 
0041148F  pop         ecx  
00411490  mov         dword ptr [ebp-8],ecx 
00411493  mov         ecx,dword ptr [this] 
00411496  call        student::student (411109h) 
{ //2 m_iNum = 1; 0041149B mov eax,dword ptr [this] 0041149E mov dword ptr [eax+4],1 }
可以看到在執行m_iNum = 1前先呼叫了父類的建構函式。

再來看看析構時的順序(教科書上寫的是先呼叫子類的解構函式,在呼叫父類的,與構造過程相反)

~GradeOneStue()
	{					//4
00411550  push        ebp  
00411551  mov         ebp,esp 
00411553  sub         esp,0CCh 
00411559  push        ebx  
0041155A  push        esi  
0041155B  push        edi  
0041155C  push        ecx  
0041155D  lea         edi,[ebp-0CCh] 
00411563  mov         ecx,33h 
00411568  mov         eax,0CCCCCCCCh 
0041156D  rep stos    dword ptr es:[edi] 
0041156F  pop         ecx  
00411570  mov         dword ptr [ebp-8],ecx 
		m_iNum = 0;
00411573  mov         eax,dword ptr [this] 
00411576  mov         dword ptr [eax+4],0 
	}
0041157D  mov         ecx,dword ptr [this] 
00411580  call        student::~student (41102Dh)
可以看到順序和教科書上一樣。

二、解構函式是virtual,正常定義一個子類物件

建構函式順序就略過了,看析構彙編程式碼

virtual ~GradeOneStue()
	{					//4
004116D0  push        ebp  
004116D1  mov         ebp,esp 
...
004116F3  mov         eax,dword ptr [this] 
004116F6  mov         dword ptr [eax],offset GradeOneStue::`vftable' (415640h) 
		m_iNum = 0;
004116FC  mov         eax,dword ptr [this] 
004116FF  mov         dword ptr [eax+8],0 
	}
00411706  mov         ecx,dword ptr [this] 
00411709  call        student::~student (411091h) ...
可以看到,解構函式最後還是呼叫了父類的析構(即使是虛擬函式)。

三、用一個父類指標去new一個子類物件,解構函式不是virtual,構造過程略過

class student
{
public:
	int *m_pInt;
	student()
	{
		m_pInt = new int[10];   //1
		memset(m_pInt, 0, 10*4);
	}

	~student()
	{							//3
		delete []m_pInt;
	}

};

class GradeOneStue:public student
{
public:
	int m_iNum;
	GradeOneStue()			
	{					//2
		m_iNum = 1;
	}

	~GradeOneStue()
	{					//4
		m_iNum = 0;
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	student *pStu = new GradeOneStue();
	delete pStu;
	return 0;
}
看delete pStu處的彙編程式碼
delete pStu;
00413726  mov         eax,dword ptr [ebp-14h] 
00413729  mov         dword ptr [ebp-0E0h],eax 
0041372F  mov         ecx,dword ptr [ebp-0E0h] 
00413735  mov         dword ptr [ebp-0ECh],ecx 
0041373B  cmp         dword ptr [ebp-0ECh],0 
00413742  je          wmain+0C9h (413759h) 
00413744  push        1    
00413746  mov         ecx,dword ptr [ebp-0ECh] 
0041374C  call        student::`scalar deleting destructor' (4111E5h) 
00413751  mov         dword ptr [ebp-10Ch],eax 
00413757  jmp         wmain+0D3h (413763h) 
00413759  mov         dword ptr [ebp-10Ch],0 
	return 0;
看到只調用了父類的解構函式,此時子類的解構函式沒有被呼叫,此時子類的解構函式中雖然有呼叫父類解構函式的程式碼,但是這裡直接呼叫的是父類的解構函式,所以這是如果子類中解構函式有釋放資源的程式碼,這裡會造成這部分資源不被釋放,有可能造成記憶體洩露

四、用一個父類指標去new一個子類物件,解構函式是virtual,構造過程略過

這裡直接看delete pStu的彙編程式碼

delete pStu;
004114E6  mov         eax,dword ptr [ebp-14h] 
004114E9  mov         dword ptr [ebp-0E0h],eax 
004114EF  mov         ecx,dword ptr [ebp-0E0h] 
004114F5  mov         dword ptr [ebp-0ECh],ecx 
004114FB  cmp         dword ptr [ebp-0ECh],0 
00411502  je          wmain+0D9h (411529h) 
00411504  mov         esi,esp 
00411506  push        1    
00411508  mov         edx,dword ptr [ebp-0ECh]    //edx等於pStu,指向new出來的子類物件
0041150E  mov         eax,dword ptr [edx]         //將edx指向的dword傳給eax,eax現在是儲存的虛擬函式表指向的地址
00411510  mov         ecx,dword ptr [ebp-0ECh] 
00411516  mov         edx,dword ptr [eax]         //將虛擬函式表指向的第一個函式的地址傳給edx,也就是唯一的一個虛解構函式的地址
00411518  call        edx                         //呼叫解構函式,這個函式是子類的,這個地址構造時寫入虛擬函式表
由於子類的虛構函式最後會呼叫父類的解構函式(不管是否為virtual,子類解構函式最後都會呼叫父類解構函式),所以最終父類的解構函式會得到執行。(這時呼叫的解構函式相當於pStu->vpTable->解構函式(),這個解構函式是構造時寫入的,由於構造時使用子類型別去new,此時虛擬函式表中解構函式的地址是子類的解構函式地址)

最後的結論:

1、無論父類與子類的解構函式是否是virtual,子類的解構函式都會呼叫父類的解構函式

2、如果父類與子類的解構函式不為virtual,用一個父類指標指向一個用子類型別new的物件,delete時,直接呼叫父類的解構函式,這是在編譯時刻就決定的。如果子類解構函式中有釋放資源的程式碼,這是會發生資源洩漏。

3、如果父類與子類的解構函式是virtual,用一個父類指標指向一個用子類型別new的物件,delete時,這時由於是通過虛擬函式表呼叫解構函式,而虛擬函式表中的地址是構造時寫入的,是子類的解構函式的地址,由於結論第一條,所以子類與父類的解構函式都會得到呼叫,不會發生資源洩漏。

寫的不對的地方,歡迎拍磚吐舌頭