探索繼承技術
本文是基於大家已經知道繼承技術的基礎上強化一些知識
繼承的客戶視圖:
Super
↑ Sub類型的對象也是Super類型的對象,因為Sub是從Super繼承而來的。
Sub
指向對象的指針或者引用可以引用所聲明類的對象或其任何子類對象。比如,指向Super的指針可以實際上指向Sub對象,對於引用也是這樣。客戶代碼仍然只訪問Super中的方法和數據成員,但是通過這種機制,對Super進行操作的代碼也可以對Sub進行操作。
Super *SuperPoint = new Sub();
繼承的子類視圖:
子類(public繼承)可以訪問超類的public和protected方法和數據成員。
覆蓋方法:
超類中只有聲明為virtual的方法才能被子類正確覆蓋。
一個好的經驗就是所有方法都聲明為virtual(構造函數除外),這樣就不需要擔心覆蓋方法是否有效了。唯一的不足就是犧牲了性能。
語法:在子類的定義中重新聲明。在子類的實現文件中,重新定義。超類和子類中,此函數的定義之前都不需要加上virtual關鍵字,在類定義的聲明處加上就可以了。
如果子類中不想繼續被子類的子類中的方法覆蓋,在子類中方法的聲明處就不需要加上virtual關鍵字了,但經驗就是,最好加上,以備子類繼續被擴展。
如:
class Super
{
public:
...code...
virtual void someMethod();
...code...
};
void Super:: someMethod()
{
cout<<”Super’s method.”<<endl;
}
class Sub:public Super
{
public:
...code...
virtual void someMethod();
virtual void otherSomeMethod();
...code...
};
void Sub:: someMethod();
{
cout<<”Sub’s method.”<<endl;
}
覆蓋方法的客戶視圖:
經過前面的覆蓋之後,對於someMethod()方法,Super與Sub對象都可以調用,只是行為有所不同。對於指針或引用可以引用類的對象或任意子類的對象。對象自身知道自己實際是哪個類的對象,所以只要方法用virtual聲明,就會調用正確的方法。
考慮下面:
Super mySuper;
Sub mySub;
Super *superPoint = &mySub;
Super &ref = mySub;
Super obj = mySub;
mySuper.someMethod();
mySub.someMethod();
superPoint->someMethod();
ref.someMethod();
obj.someMethod();
依次輸出為:
Super’s method.”
Sub’s method.
Sub’s method.
Sub’s method.
Super’s method.”
注意:即使超類的指針和引用知道實際上它是一個子類對象,也不能調用在超類中未定義的子類方法或者成員。
對於:
ref.otherSomeMethod(); //bug
對於非指針、非引用的對象,它不知道自己到底是哪個類的對象,這樣,下面的aObj對象就會失去子類中的一些知識。
Sub mySub;
Super aObj = mySub;
aObj.someMethod();
輸出:
Super’s method.”
綜上:就相當於把Super對象看做一個盒子,Sub看做另一個更大的盒子(因為子類添加了一些自己的內容)。在使用Sub的引用或指針時,盒子不會改變,只是采用一種新的辦法來訪問。然而,在把Sub強制轉換為Super時,就會扔掉一些Sub自己獨有的內容,才能把它放進一個較小的盒子中。
考慮父類:
1. 父構造函數:
C++定義的對象的創造順序為:
a. 如果有的話,首先構造基類。
b. 非static數據成員按照聲明順序構造。
c. 執行構造函數。
注意其父類構造函數是系統自動調用的。如果其父類存在默認構造函數,C++會自動調用。如果父類沒有默認構造函數,或者盡管有,但是希望使用另外一個構造函數,則可以把構造函數用鏈串起來,就像初始化列表中初始化數據成員一樣。
如:
class Super
{
public:
Super(int i);
};
class Sub:public Super
{
public:
Sub();
};
Sub::Sub():Super(7)
{
...code...
}
如果向父類構造函數傳遞自己的數據成員作為參數則是不行的,因為先調用父類構造函數,後再初始化自己的數據成員,如果這樣,則傳遞的數據成員是未初始化的。
2. 父析構函數
因為析構函數不能包含參數,所以C++會給父類自動調用其析構函數。撤銷的順序與構造順序恰好相反。
a. 調用析構函數體
b. 按照構造的逆序刪除數據成員
c. 如果有父類,析構父類
注意,作為經驗,所有析構函數都應該使用virtual關鍵字聲明。如果不這樣,就可能會發生錯誤。考慮:如果代碼可能對一個超類指針調用刪除操作,但這個超類指針實際指向一個子類對象,那麼析構鏈的開始位置就不對了。
3. 引用父類的數據
在子類中函數和數據成員的名稱可能會產生二義性,進行多重繼承尤其如此。C++提供了一種機制來消除二個類之間的名字二義性:作用域解析操作符。
在子類中覆蓋方法時,實際上是替換其他代碼感興趣的原始代碼?不過父類中的方法依然存在,可能還會用到。在子類中的方法要調用父類中的方法(該方法子類覆蓋了),則需加上父類名加上作用域解析操作符。
如:
class Super
{
public:
...code...
virtual string doSomething(){ return “Super”;}
...code...
};
class Sub:public Super
{
public:
...code...
virtual string doSomething(){ return “Sub”+ Super:: doSomething() ;}
virtual void otherSomeMethod();
...code...
};
4. 向上類型強制轉換和向下類型強制轉換
向上類型強制轉換:
下面會造成切割
Super mySuper = mySub;
下面就不會發生切割了
Super *superPoint = &mySub;
Super &ref = mySub;
總結:進行向上類型強制轉換時,要使用指向超類的指針或引用來避免切割問題。
向下類型強制轉換:
考慮下面:
void presumptuous(Super* inSuper)
{
Sub * mySub = static_cast<Sub*>(inSuper);
...other code...
}
如果編寫此方法的人來調用這個函數,可能沒什麼問題,因為他知道此函數希望參數類型為Sub*。單數如果別人來調用,就會給它傳遞一個Super*,並不會完成編譯時檢查來對參數進行類型轉換,函數會盲目地假定inSuper實際上是指向Sub對象的指針。
向下類型轉換有時是必須的,在可控的環境中可以有效使用向下類型強制轉換。此時應該利用dynamic_cast,它使用該類型對象的內置知識來防止無意義的類型轉換。如果對指針不能進行動態類型轉換,指針值為NULL而不是指向無意義的數據。如果未能對對象的引用使用dynamic_cast,則會拋出std::bad_cast異常。
總結:只有在必要並且保證使用動態類型轉換的時候使用向下類型強制轉換。
繼承以實現多態
純虛方法與抽象基類:
純虛方法是指在類定義中顯式未定義的方法。包含純虛方法的類稱為抽象類,抽象類不能實例化,但是仍然可以使用抽象類類型的指針和引用。
語法:virtual string getString() const = 0;
如果二個要實現二個兄弟類的互相轉換,可以在兄弟類中增加一個類型構造函數,它看起來和復制構造函數很像,但是復制構造函數引用同一類型的對象,而類型構造函數引用的是兄弟類的對象。
如:
class SpreadsheetCell
{code};
class StringSpreadsheetCell:public SpreadsheetCell
{
public:
StringSpreadsheetCell();
StringSpreadsheetCell(const DoubleSpreadsheetCell& inDoubelCell);
};
class DoubleSpreadsheetCell:public SpreadsheetCell
{code};
使用類型構造函數,給定DoubleSpreadsheetCell很容易構造StringSpreadsheetCell。但是不要被類型轉換所迷惑。從一個兄弟類轉換到另一個兄弟類是不行的。除非重載類型轉換操作符。
這樣實現二個StringSpreadsheetCel、一個StringSpreadsheetCell
一個DoubleSpreadsheetCell、二個DoubelSpreadsheetCel相加就可以寫一個公共的operator+重載了。
const StringSpreadsheetCell operator+( const StringSpreadsheetCell& lhs, const StringSpreadsheetCell& rhs)
{code}
多重繼承:
從多個類中繼承:
class A{code};
class B{code};
class C:public A,public B{};
C對象支持A和B中所有public方法和數據成員。
類C的方法可以訪問A和B中的protected數據和方法。
C對象可以轉換為A和B對象。
創建C對象時,會自動調用A和B的默認構造函數,調用順序在類定義中這二個類的列出順序。
撤銷C對象時,會自動調用A和B的析構函數,調用順序與類定義中這二個類的列出順序相反。
命名沖突與二義基類:
名字二義性:
如果A和B都有一個public eat()方法,一旦C對象調用eat()方法,就會產生二義性(也可是是同名的數據成員)。
解決此二義性:
1.static_cast<A>(myC.eat()); //向上轉換產生切割調用A的eat()方法
2.myC.A::eat(); //使用作用域解析操作符調用A的eat()方法
引起二義性問題的另外一種原因就是:從同一個類繼承二次。
class A{code};
class B:public A{code};
class C:public A,public B{};
二義基類:
多個父類自身有共同的父類。
使用這種繼承體系,最好是使上層的共同父類成為抽象基類,所有方法都聲明為純虛方法。