C++虛函數(shù)探索筆記(3)——延伸思考:虛函數(shù)應(yīng)用的一些其他情形
關(guān)注問(wèn)題:
虛函數(shù)的作用
虛函數(shù)的實(shí)現(xiàn)原理
虛函數(shù)表在對(duì)象布局里的位置
虛函數(shù)的類(lèi)的sizeof
純虛函數(shù)的作用
多級(jí)繼承時(shí)的虛函數(shù)表內(nèi)容
虛函數(shù)如何執(zhí)行父類(lèi)代碼
多繼承時(shí)的虛函數(shù)表定位,以及對(duì)象布局
虛析構(gòu)函數(shù)的作用
虛函數(shù)在QT中的應(yīng)用
虛函數(shù)與inline修飾符,static修飾符
虛析構(gòu)函數(shù)
大家都知道,在C++里需要自己嚴(yán)格管理好資源的分配和回收。通常情況下,
在一個(gè)對(duì)象被析構(gòu)的時(shí)候,是要由其釋放其申請(qǐng)到的各種資源的。最常見(jiàn)的,當(dāng)
然就是內(nèi)存資源啦。
當(dāng)只有一個(gè)類(lèi)的時(shí)候,我們可以不用考慮太多,只要在析構(gòu)函數(shù)里檢查并釋
放所有申請(qǐng)到的資源即可。但是在這個(gè)類(lèi)繼承了一個(gè)抽象接口基類(lèi)時(shí),就有點(diǎn)點(diǎn)
不一樣了。讓我們看看類(lèi)的析構(gòu)過(guò)程:
在大多數(shù)的類(lèi)的使用時(shí),通常都是直接刪除該類(lèi)的實(shí)例對(duì)象,然后該類(lèi)的析
構(gòu)函數(shù)就會(huì)被調(diào)用,從而使得這個(gè)類(lèi)在析構(gòu)函數(shù)里執(zhí)行的資源釋放代碼被執(zhí)行到
。
如果這個(gè)類(lèi)繼承了其他類(lèi),那么編譯器還會(huì)在這個(gè)類(lèi)的析構(gòu)函數(shù)里自動(dòng)添加
對(duì)父類(lèi)的析構(gòu)函數(shù)的調(diào)用,從而將父類(lèi)里申請(qǐng)的資源也進(jìn)行釋放。如果偶多個(gè)父
類(lèi),也會(huì)依次調(diào)用各個(gè)析構(gòu)函數(shù)。
倘若繼承的是一個(gè)抽象接口類(lèi),并且在程序運(yùn)行期,可能通過(guò)一個(gè)基類(lèi)指針
將此對(duì)象釋放掉,那么致命而又隱藏的內(nèi)存泄露BUG就出現(xiàn)啦……因?yàn)樵噲D刪除的
是基對(duì)象,刪除時(shí)調(diào)用的是基類(lèi)的析構(gòu)函數(shù),而基類(lèi)的析構(gòu)函數(shù)當(dāng)然是不會(huì)去調(diào)
用子類(lèi)的析構(gòu)函數(shù)的羅!
讓我們看看下面的代碼,使用vs2008編譯并運(yùn)行的時(shí)候,將會(huì)在程序運(yùn)行結(jié)
束時(shí)報(bào)告內(nèi)存泄漏情況(如果要在linux下編譯測(cè)試,需要去掉第一行的include
,以及return前的_CrtDumpMemoryLeaks()函數(shù),然后使用linux下檢查內(nèi)存泄
露的工具進(jìn)行測(cè)試)。
//Source filename: Win32Con.cpp
#include
class parent
{
public:
parent() { }
/*virtual */ ~parent() { }
};
class child:public parent
{
public:
child()
{
p=new char[1000];
}
~child()
{
delete[] p;
}
char *p;
};
void free_child(parent *pp)
{
delete pp;
}
int main()
{
child *obj=new child();
free_child(obj);
_CrtDumpMemoryLeaks();
return 0;
}
在這段代碼里我們創(chuàng)建的是一個(gè)child類(lèi)型的對(duì)象,然后使用free_child
(parent*)函數(shù)來(lái)試圖釋放這個(gè)對(duì)象,這個(gè)時(shí)候,只會(huì)調(diào)用到parent::
~parent()這個(gè)析構(gòu)函數(shù),而不會(huì)調(diào)用到child::~child()!
如何解決這個(gè)問(wèn)題呢?
很簡(jiǎn)單的,只要在parent::~parent()前增加 virtual關(guān)鍵字,將其變成
一個(gè)虛函數(shù)。這樣,無(wú)論是以這個(gè)對(duì)象的父類(lèi)指針進(jìn)行刪除的時(shí)候,就會(huì)從虛函
數(shù)表里定位到子類(lèi)child的析構(gòu)函數(shù),這樣就能夠從子類(lèi)開(kāi)始一級(jí)一級(jí)的向上調(diào)用
析構(gòu)函數(shù),從而正確的將這個(gè)對(duì)象在各個(gè)繼承層次上申請(qǐng)的所有資源都釋放掉。
正因?yàn)檫@個(gè)原因,在很多C++編程原則的文章或者書(shū)里都會(huì)提到這樣的原則:
如果一個(gè)類(lèi)要被設(shè)計(jì)為可被繼承的基類(lèi),那么其析構(gòu)函數(shù)應(yīng)該被聲明為虛函
數(shù)。
虛函數(shù)在QT中的應(yīng)用
在QT里虛函數(shù)的應(yīng)用非常的廣泛,事實(shí)上,在大多數(shù)的C++類(lèi)庫(kù)里都不可避免
的要使用到虛函數(shù)。這里簡(jiǎn)單的列舉QT里使用虛函數(shù)的情況:
QT的事件機(jī)制
是使用了虛函數(shù)的,你因此才可以自定義事件處理函數(shù)。比如最核心的
QObject類(lèi)的定義里(在qobject.h里),我們可以看到如下的虛函數(shù)定義:
virtual bool event(QEvent *);
virtual bool eventFilter(QObject *, QEvent *);
然后,在QWidget類(lèi)繼承QObject類(lèi)后重新實(shí)現(xiàn)了上面的兩個(gè)虛函數(shù),完成很
多窗口控件類(lèi)的缺省事件處理。
當(dāng)你要編寫(xiě)自定義的QT控件的時(shí)候,對(duì)event虛函數(shù)的重新實(shí)現(xiàn)就更是重要啦
。
QT的信號(hào)和槽
QT的槽函數(shù)可以被聲明為虛函數(shù),所以雖然QT在實(shí)現(xiàn)信號(hào)和槽機(jī)制的時(shí)候可
能出于效率或者運(yùn)行代價(jià)的原因未采用虛函數(shù)機(jī)制,但是我們依然可以在必要的
時(shí)候使用虛函數(shù)來(lái)完成一些特定功能。比如為一些自定義控件類(lèi)抽象出來(lái)一個(gè)抽
象接口基類(lèi),在做信號(hào)和槽的連接的時(shí)候是對(duì)基類(lèi)指針進(jìn)行操作,而在基類(lèi)里的
槽定義為虛函數(shù),那么虛函數(shù)在此依然可以實(shí)現(xiàn)信號(hào)與槽的多態(tài)。
然而虛函數(shù)在調(diào)用的時(shí)候,一定要經(jīng)歷查表的步驟,是存在一定的運(yùn)行開(kāi)銷(xiāo)
的,對(duì)于一些非常頻繁的槽調(diào)用還是應(yīng)該考慮到使用虛函數(shù)產(chǎn)生的代價(jià)的。
其他
在虛函數(shù)上,static和inline這兩個(gè)關(guān)鍵詞與virtual顯得很不友好。
從語(yǔ)義上即可看出,static和virtual完全就是沖突的,所以如果你試圖為一
個(gè)虛函數(shù)增加一個(gè)static限定詞,那么你的C++編譯器就會(huì)很負(fù)責(zé)任的報(bào)告一個(gè)嚴(yán)
重錯(cuò)誤給你。
而inline的含義和虛函數(shù)其實(shí)也是非常沖突的,但是inline在語(yǔ)法上只是給
編譯器一個(gè)建議,而不是強(qiáng)制的語(yǔ)義限定,所以C++編譯器應(yīng)該會(huì)忽略掉inline關(guān)
鍵詞,繼續(xù)正常的編譯。