C++中解構函式為虛擬函式時呼叫發生了什麼變化
阿新 • • 發佈:2019-01-30
昨天去XX公司面試,面試官問了一個關於C++類解構函式為虛擬函式時,如果是父類的指標用子類來new,如果發生析構時,解構函式是virtual與不是virtual有什麼區別。當時答的不好,回來總結了一下,在機器上實現了一遍,終於搞明白了。記錄下來,以後遇到這種情況自己一定不要犯錯了
一、先看第一種最簡單的情況吧,教科書上教的,解構函式不是virtual,正常定義一個子類物件
這時構造順序是先1後2,下面是反彙編程式碼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; }
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時,這時由於是通過虛擬函式表呼叫解構函式,而虛擬函式表中的地址是構造時寫入的,是子類的解構函式的地址,由於結論第一條,所以子類與父類的解構函式都會得到呼叫,不會發生資源洩漏。
寫的不對的地方,歡迎拍磚