來源:陳太漢
1:子類不要覆寫父類的非虛擬函式。
2:子類不要覆寫從父類繼承過來的預設引數
3:子類與父類之間的賦值問題
1:子類不要覆寫父類的非虛擬函式。
為了解釋方便,先看一個簡單的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
class A { public: A(int d):data(d){ } void print() { cout<<"A print..."<<data<<endl; } virtual void test(int i=2) { cout<<"A test..."<<i<<endl; } private: int data; }; class B:public A { public : B(int d):A(d){ } void print() { cout<<"B print..."<<endl; } virtual void test(int i=4) { cout<<"B test..."<<i<<endl; } }; //測試程式碼 int main() { { B b(5); b.print(); A *a=&b; a->print(); cout<<endl; b.test(); a->test(); cout<<endl; A a1=b; a1.test(); } getchar(); return 0; } |
執行結果截圖:
例子中指標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。
就此打住,未完待續…