虛解構函式(√)、純虛解構函式(√)、虛建構函式(X)

發表於2016-12-03

一. 虛解構函式

我們知道,為了能夠正確的呼叫物件的解構函式,一般要求具有層次結構的頂級類定義其解構函式為虛擬函式。因為在delete一個抽象類指標時候,必須要通過虛擬函式找到真正的解構函式。

如:

這是正確的用法,會發生動態繫結,它會先呼叫Derived的解構函式,然後是Base的解構函式

如果解構函式不加virtual,delete pb只會執行Base的解構函式,而不是真正的Derived解構函式。因為不是virtual函式,所以呼叫的函式依賴於指向靜態型別,即Base

二. 純虛解構函式

現在的問題是,我們想把Base做出抽象類,不能直接構造物件,需要在其中定義一個純虛擬函式。如果其中沒有其他合適的函式,可以把解構函式定義為純虛的,即將前面的CObject定義改成:

可是,這段程式碼不能通過編譯,通常是link錯誤,不能找到~Base()的引用(gcc的錯誤報告)。這是因為,解構函式、建構函式和其他內部函式不一樣,在呼叫時,編譯器需要產生一個呼叫鏈。也就是,Derived的解構函式裡面隱含呼叫了Base的解構函式。而剛才的程式碼中,缺少~Base()的函式體,當然會出現錯誤。

這裡面有一個誤區,有人認為,virtual f()=0這種純虛擬函式語法就是沒有定義體的語義。

其實,這是不對的。這種語法只是表明這個函式是一個純虛擬函式,因此這個類變成了抽象類,不能產生物件。我們完全可以為純虛擬函式指定函式體 (http://www.research.att.com/~bs/bs_faq2.html#pure-virtual)。通常的純虛擬函式不需要函式體,是因為我們一般不會呼叫抽象類的這個函式,只會呼叫派生類的對應函式。這樣,我們就有了一個純虛解構函式的函式體,上面的程式碼需要改成:

從語法角度來說,不可以將上面的解構函式直接寫入類宣告中(行內函數的寫法)。這或許是一個不正交化的地方。但是這樣做的確顯得有點累贅

這個問題看起來有些學術化,因為一般我們完全可以在Base中找到一個更加適合的函式,通過將其定義為沒有實現體的純虛擬函式,而將整個類定義為抽象類。但這種技術也有一些應用,如這個例子:

在這個例子中,我們試圖列印出類的繼承關係。在根基類中定義了虛擬函式Hiberarchy,然後在每個派生類中都過載此函式。我們再一次看到,由於想把Base做成個抽象類,而這個類中沒有其他合適的方法成員可以定義為純虛的,我們還是隻好將Hiberarchy定義為純虛的。(當然,完全可以定義~Base函式,這就和上面的討論一樣了。^_^)

另外,可以看到,在main中有兩種呼叫方法,第一種是普通的方式,進行動態連結,執行虛擬函式,得到結果”Derived::Hiberarchy”;第二種是指定類的方式,就不再執行虛擬函式的動態連結過程了,結果是”Base::Hiberarchy”。

通過上面的分析可以看出,定義純虛擬函式的真正目的是為了定義抽象類,而並不是函式本身。與之對比,在java中,定義抽象類的語法是 abstract class,也就是在類的一級作指定(當然虛擬函式還是也要加上abstract關鍵字)。是不是這種方式更好一些呢?在Stroustrup的《C++語言的設計與演化》中我找到這樣一段話:

“我選擇的是將個別的函式描述為純虛的方式,沒有采用將完整的類宣告定義為抽象的形式,這是因為純虛擬函式的概念更加靈活一些。我很看重能夠分階段定義類的能力;也就是說,我發現預先定義一些純虛擬函式,並把另外一些留給進一步的派生類去定義也是很有用的”。

我還沒有完全理解後一句話,我想從另外一個角度來闡述這個概念。那就是,在一個多層複雜類結構中,中間層次的類應該具體化一些抽象函式,但很可能並不是所有的。中間類沒必要知道是否具體化了所有的虛擬函式,以及其祖先已經具體化了哪些函式,只要關注自己的職責就可以了。也就是說,中間類沒必要知道自己是否是一個真正的抽象類,設計者也就不用考慮是否需要在這個中間類的類級別上加上類似abstract的說明了。

當然,一個語言的設計有多種因素,好壞都是各個方面的。這只是一個解釋而已。

最後,總結一下關於虛擬函式的一些常見問題:

1) 虛擬函式是動態繫結的,也就是說,使用虛擬函式的指標和引用能夠正確找到實際類的對應函式,而不是執行定義類的函式。這是虛擬函式的基本功能,就不再解釋了。

2) 建構函式不能是虛擬函式。而且,在建構函式中呼叫虛擬函式,實際執行的是父類的對應函式,因為自己還沒有構造好, 多型是被disable的。

3) 解構函式可以是虛擬函式,而且,在一個複雜類結構中,這往往是必須的。

4) 將一個函式定義為純虛擬函式,實際上是將這個類定義為抽象類,不能例項化物件。

5) 純虛擬函式通常沒有定義體,但也完全可以擁有。

6) 解構函式可以是純虛的,但純虛解構函式必須有定義體,因為解構函式的呼叫是在子類中隱含的。

7) 非純的虛擬函式必須有定義體,不然是一個錯誤。

8) 派生類的override虛擬函式定義必須和父類完全一致。除了一個特例,如果父類中返回值是一個指標或引用,子類override時可以返回這個指標(或引用)的派生。例如,在上面的例子中,在Base中定義了 virtual Base* clone(); 在Derived中可以定義為 virtual Derived* clone()。可以看到,這種放鬆對於Clone模式是非常有用的。
其他,有待補充。

相關文章