c++ | 有趣的動(dòng)態(tài)轉(zhuǎn)換之 delete 崩潰探究兼談基類(lèi)虛析構(gòu)的重要性
前言
在《有趣的動(dòng)態(tài)轉(zhuǎn)換》 這篇文章中,運(yùn)行 測(cè)試代碼3 會(huì)崩潰。本文試圖揭示崩潰的原因。
錯(cuò)誤更正
在開(kāi)始之前,需要更正《C++ 虛函數(shù)簡(jiǎn)介》中的一個(gè)錯(cuò)誤。關(guān)于 CBase 和 CDerived 的虛表內(nèi)容,析構(gòu)函數(shù)的位置并不是直接存儲(chǔ)了虛函數(shù)的地址,而是存儲(chǔ)了一段編譯器生成的函數(shù),該函數(shù)內(nèi)部會(huì)調(diào)用對(duì)應(yīng)的析構(gòu)函數(shù)。

所以正確的虛表應(yīng)該是下面這樣的:

注意:
debug版默認(rèn)會(huì)引入另外一層間接層,而release?版不會(huì)。
錯(cuò)誤回顧
回顧一下 測(cè)試代碼3 運(yùn)行后的錯(cuò)誤提示,如下圖:

這是一個(gè)棧平衡被破壞的錯(cuò)誤。在 vs 中單步調(diào)試可以知道是在執(zhí)行 delete(pBaseA); 的時(shí)候?qū)е碌腻e(cuò)誤。奇怪的是,在崩潰之前,還輸出了一個(gè) NewB::PerfectFunctionName。光看源碼,看不出什么問(wèn)題了,需要查看反匯編代碼了。
delete 的反匯編代碼

根據(jù)上圖中的解釋?zhuān)瑘?zhí)行 delete (pBaseA); 會(huì)輸出 NewB::PerfectFunctionName 已經(jīng)很清楚了。但是為什么會(huì)崩潰呢?不知道有沒(méi)有小伙伴兒注意到那個(gè)奇怪的 push 1。函數(shù) NewB::PerfectFunctionName() 是沒(méi)有參數(shù)的,而這里的 push 1 卻向棧上壓入了一個(gè)參數(shù),所以棧就不平衡了。
至此,執(zhí)行 delete (pBaseA); 會(huì)輸出 NewB::PerfectFunctionName 并且崩潰的來(lái)龍去脈應(yīng)該已經(jīng)清楚了。但是那個(gè) push 1 到底是什么呢?
奇怪的 push 1
為了弄清這個(gè) push 1 的來(lái)歷與作用,我把 delete pBaseA 改成了 delete((BaseB*)pBaseA);,這樣代碼會(huì)按正常的邏輯執(zhí)行。也就是會(huì)執(zhí)行到 NewB::'vector deleting destructor'。查看對(duì)應(yīng)的反匯編代碼,如下圖:

從圖中高亮的三句反匯編語(yǔ)句可知:NewB::vector deleting destructor 需要一個(gè)參數(shù)。該參數(shù)是一個(gè)標(biāo)記,如果為 1,則調(diào)用 operator delete 釋放內(nèi)存,否則不釋放內(nèi)存。
從整個(gè)反匯編代碼可知,NewB::vector deleting destructor 會(huì)先執(zhí)行 NewB::~NewB(),然后根據(jù)外部傳入的標(biāo)記來(lái)決定是否調(diào)用 operator delete 釋放內(nèi)存。
至此,理清了 push 1 的用途,那什么時(shí)候會(huì) push 0 呢?
不知道有沒(méi)有小伙伴兒顯式調(diào)用過(guò)析構(gòu)函數(shù),像下面這樣。

如果查看 pBaseB->~BaseB() 的反匯編代碼,一切都會(huì)真相大白。如下圖:

為什么多態(tài)基類(lèi)的析構(gòu)函數(shù)要是虛的?
相信有經(jīng)驗(yàn)的 C++ 開(kāi)發(fā)人員一定聽(tīng)過(guò)類(lèi)似的忠告:帶有多態(tài)性質(zhì)的基類(lèi)應(yīng)該聲明一個(gè)虛析構(gòu)函數(shù)。如果類(lèi)帶有任何虛函數(shù),它就該擁有一個(gè)虛析構(gòu)函數(shù)。
如果析構(gòu)函數(shù)不是虛函數(shù)呢?會(huì)有什么問(wèn)題嗎?稍微改動(dòng)一下測(cè)試代碼,如下:

運(yùn)行結(jié)果如下圖:

只有基類(lèi)的析構(gòu)函數(shù)被調(diào)用,子類(lèi)的析構(gòu)函數(shù)并沒(méi)有被調(diào)用!為什么會(huì)這樣呢?真相就在反匯編代碼里:

從上圖可知,如果要 delete 的類(lèi)型的析構(gòu)函數(shù)是非虛的,那么 vs 中帶的編譯器在生成匯編代碼時(shí),會(huì)直接調(diào)用對(duì)應(yīng)類(lèi)型的 scalar deleting destructor,不存在多態(tài)行為!這會(huì)導(dǎo)致子類(lèi)的析構(gòu)函數(shù)沒(méi)有被調(diào)用!
總結(jié)
如果一個(gè)類(lèi)會(huì)被當(dāng)成基類(lèi)使用,請(qǐng)確保其析構(gòu)函數(shù)是虛函數(shù)。
在生成
delete (pBaseA);這條語(yǔ)句的匯編代碼時(shí),編譯器是根據(jù)pBaseA的靜態(tài)類(lèi)型確定虛析構(gòu)函數(shù)在虛表中的位置的。而不是根據(jù)pBaseA實(shí)際指向的類(lèi)型。delete pBaseA會(huì)先執(zhí)行pBaseA指向的類(lèi)型的析構(gòu)函數(shù),然后再調(diào)用operator delete釋放對(duì)應(yīng)的內(nèi)存。可以顯式調(diào)用一個(gè)類(lèi)的析構(gòu)函數(shù)。當(dāng)然,析構(gòu)函數(shù)的訪問(wèn)級(jí)別必須是
public的。
參考資料
vs反匯編代碼《effective c++》
