Effective C++:物件導向與繼承

發表於2012-08-01

來源:陳太漢

1:子類不要覆寫父類的非虛擬函式。

2:子類不要覆寫從父類繼承過來的預設引數

3:子類與父類之間的賦值問題

《Effective C++》建構函式解構函式Assignment運算子

1:子類不要覆寫父類的非虛擬函式。

為了解釋方便,先看一個簡單的例子。

執行結果截圖:

Effective C++:物件導向與繼承

例子中指標a是指向物件b的,但是他們呼叫的print方法卻不是同一個。這裡涉及到靜態繫結和動態繫結的問題。a的靜態型別是A,a的動態的型別卻是B,b的靜態型別和動態型別都是B,因為靜態型別就是申明時的型別,動態型別是其真正指向的型別。還有一點就是非虛方法是靜態繫結,虛擬方法是動態繫結。Print是非虛方法,它是靜態繫結,呼叫的是自己的物件申明型別的方法,所以a呼叫的是A的print,b呼叫的是B的print方法。我想我們更想知道C++是怎麼實現動態繫結。我們都知道含有虛方法的類都有一個虛擬方法表,每個物件的例項都有一個指標指向這個虛擬方法表,子類會繼承父類的virtual方法,也可以覆寫父類的虛擬方法,如果子類覆寫父類的虛擬方法,那麼在虛擬表中對應的指標就指向子類覆寫父類的方法,如果子類不覆寫父類的虛擬方法,則還是指向父類的方法,這樣就形成了動態繫結。不同的子類按照自己的方式覆寫父類的虛擬方法,表現出不同的行為這就是多型。在多重繼承中,每個物件可能有多個虛擬表,那麼它的例項就會有多個指向虛擬表的指標,如果多個父類有一個相同的方法,那麼你就不能直接用這個例項呼叫這個方法,因為編譯器根本不知道它該呼叫哪個方法,你要指定是那個父類的方法,當你指明瞭哪個父類,編譯就可以通過對應的指標呼叫對應的虛擬表中對應的方法。那麼例項呼叫虛擬方法的過程是怎麼樣的呢,你有沒有想過?其實上面也提到一點,大致三步:

1:根據物件的vptr指標找到其虛擬方法表vtbl;

2:找到被呼叫方法在vtbl中對應的指標;

3:呼叫2中指標指向的方法。

 

2:子類不要覆寫從父類繼承過來的預設引數

這一條其實還是涉及到靜態繫結和動態繫結的問題,關於這個問題我想上面已經說得比較清楚了,預設值也是靜態繫結,這是毫無疑問的,因為它在編譯期就已經確定了,而虛擬方法確實動態繫結,你把靜態繫結的東西和動態繫結的東西攪在一起沒有問題,但是你還有得寸進尺的在子類中覆寫靜態的東西就會出問題,對不起,父類不管子類中靜態的東西,它只管自己靜態的東西,所以當子類不要覆寫從父類繼承過來的預設引數時,子類就可能出現精神分裂的行為,上面那個列子就是證明。

上面更多提到的都是關於虛擬方法的,那麼非虛擬方法呢,物件例項時怎麼呼叫非虛擬方法的呢?非虛擬方法是怎麼實現的呢?非虛擬方法就像一般的C函式那樣被實現的,所以他們的呼叫不需要像虛擬方法一樣先要找到一個指標,然後在通過這個指標呼叫對應的方法。

3:子類與父類之間的賦值問題

首先將父類轉換成子類的事最好不要做,因為子類的很多特性父類根本沒有,當你把一個從父類轉換過來的子類,當做子類來用的話,很可能出問題。接下來我們重點討論將子類轉換成父類。還是通過上面例子來說明問題。

B b(2);

A a=b;//呼叫copy constructor

a=b;//呼叫 operator=

上面兩行程式碼,第一行先例項化了一個物件b,第二行將b賦給a,那麼是怎麼將b賦給a的呢,這裡其實呼叫的不是operator=,而是copy constructor,因為構造一個物件必須呼叫constructor,或是copy constructor,那麼這裡肯定是呼叫copy constructor,operator=只是一個賦值動作,一個物件還沒有構造出來怎麼給他賦值呢,在operator=可不是用來幫你構造物件的哦,在第三行的時候a已經被構造出來了,那麼這裡真的就是賦值了呼叫的就是operator=。總之一句話,一個物件作為左值時,第一次肯定呼叫的是copy constructor,被初始化後(分配了記憶體),之後的操作才是賦值。一個物件作為by value形式的引數,那麼每次呼叫的都是copy constructor,而不是operator=,我們一般都會說將實參賦給形參,其實是用實參構造一個形參。

將b賦給a,就是將b的A部分賦給a,a就是一個完全的A了,它對B一無所知,更不會表現出B的任何行為,所以by value是很暴力並且很耗效能的,也不會出現多型的行為。所以要避免使用by value,儘量用by reference。

就此打住,未完待續…

 

相關文章