《Effective C++》:條款34:區分介面繼承和實現繼承,《effective34
public繼承的概念,由2部分構成:函數介面(function Interface)繼承和函數實現(function implementation)繼承。這兩種繼承的差異有點像函數的聲明和函數的定義之間的差異。
我們在設計class時,有時希望derived class只繼承函數的介面(即函式宣告);有時候希望derived class繼承函數介面和實現,但又覆寫它們所繼承的實現;又有時候希望derived class同時繼承函數的介面和實現,但不覆寫任何東西。
為了更好理解上述差異,用一個繪圖程式來說明:
class Shape{ public: virtual void draw() const=0; virtual void error(const std::string& msg); int objectID() const; …… }; class Rectangle: public Shape{……}; class Ellipse:public Shape{……};
Shape中有pure virtual函數,所以它是個抽象類別,不能建立Shape對象,但Shape強烈影響了所有以public繼承它的derivedclass,因為
- 成員函數的介面總會被繼承。條款32所說,public繼承意味著is-a。
Shape class有三個函數。draw是pure virtual函數;error是impure pure函數;objectID是non-virtual函數。
pure virtual函數有兩個特點:它們必須被繼承了它們的具體class重新聲明,而且在抽象class中通常沒有定義。這也就是說明:
- 聲明一個pure virtual函數的目的是為了讓derived class只繼承函數介面。
這也是合情合理的,因為Shape::draw並不知道我們要畫什麼映像,當然無法給出實現了。但是我們可以為pure virtual函數提供定義,即為Share::draw提供一份實現,C++不會發出怨言,但是調用這個函數的唯一途徑是調用時指明其class名稱:
Shape* ps=new Shape; ps->draw(); ps->Share::draw();
impure virtual函數和pure virtual函數有所不同,derived classes繼承其函數介面,但impure virtual函數會提供一份實現代碼,derived class可能覆寫(override)它。
- 聲明簡樸的(非純)impure virtual函數的目的,是讓derived classes繼承該函數的介面和預設實現。
考慮Shape::error這個例子,error介面表示,每個class都必須支援一個遇上錯誤時可調用的函數,但每個class可以自由處理錯誤。如果某個class不想針對錯誤做出特殊行為,可以退回到Shape class提供的預設錯誤處理行為。也就是說Shape::error的聲明式告訴derived class設計者:你必須支援一個error函數,但如果你不想自己寫,可以使用Shape class提供的預設版本。
如果允許impure virtual函數同時指定函式宣告和函數預設行為,有可能造成危險。考慮一個具體例子,一個XYZ航空公司設計飛機繼承體系,該公司有A型和B型兩種飛機,都以相同方式飛行,可以考慮這樣設計繼承體系:
class Airport{ ……}; class Airplane{ public: virtual void fly(const Airport& destation); …… }; void Airplane::fly(const Airport& destation) { //將飛機飛到指定的destination } class ModelA: public Airplane{……}; class ModelB: public Airplane{……};
因為不同型飛機不需要不同的fly實現,Airplane::fly被聲明為virtual;為了避免在ModelA和ModelB重新撰寫相同代碼,預設的飛行行為有Airplane::fly提供。
上面這種設計方式是典型的物件導向設計。兩個classes共用的性質放到base class中,然後被這兩個class繼承。這樣可以突出共同性質,避免代碼重複。
但是如果XYZ要購買一種新型飛機C,C和A、B飛行方式不同。XYZ公司程式員給C型飛機添加了一個class,但是沒有重新定義fly函數
class ModelC: public Airplane{……};
然後又寫了如下代碼
Airport PDX();//某個機場 Airplane* pa=new ModelC; …… pa->fly(PDX);//調用了Airplane::fly
這會造成大災難,因為程式員試圖以ModelA或ModelB的方式來飛ModelC。問題不在於Airplane::fly有預設行為,在於ModelC在未搞清楚的情況下就使用了這個預設行為。幸運的是可以做到:提供預設實現給derived classes,但是除非derived classes真的要用。這個做法是切斷virtual函數介面和其預設實現之間的串連。
class Airplane{ public: virtual void fly(const Airport& destation)=0; protected: void defaultFly(const Airport& destation); }; void Airplane::deFaultFly(const Airport& destation) { //將飛機飛到指定目的地 }
這裡將Airplane::fly改為pure virtual函數,只提供介面。但是預設的行為在Airplane::defaultFly函數中出現。如果要使用其預設行為,可以在fly函數調用defaultFly函數。
class ModelA: public Airplane{ public: virtual void fly(const Airport& destation) { defaultFly(destation)} }; class ModelB: public Airplane …… class ModelC: public Airplane{ public: virtual void fly(const Airport& destination); }; void ModelC:fly(const Airport& destination) { //將C型飛機飛到指定目的地 }
上面設計中,Airplane::defaultFly是個non-virtual,derived classes不用重新定義(**條款**36)。如果Airplane::defaultFly是virtual函數,就會出現迴圈問題:萬一derived classes忘記重新定義defaultFly函數會怎樣?
有的人返回以不同的函數分別將提供介面和預設實現,這樣會因為過度雷同的函數名稱引起class命名空間汙染問題;但是他們同意介面和預設實現應該分開。我們可以利用“pure virtual函數必須在derived classes中重新聲明,但它們可以擁有自己的實現”這個特點
class Airplane{ public: virtual void fly(const Airport& destination)=0; …… }; void Airplane::fly(const Airport& destination)//pure virtual函數實現 { //預設實現 } class ModelA: public Airplane{ public: virtual void fly(const Airport& destination) {Airplane::fly(destination);} …… }; class ModelB:public Airplane …… class ModelC: public Airplane { public: virtual void fly(const Airport& destination); …… }; void ModelC::fly(const Airport& destination) { //ModelC的實現 }
這個實現和上一個不同之處在於,用pure virtual函數Airplane::fly替換了獨立函數Airplane::defaultFly。現在的fly被分割為兩個基本要素:其聲明部分表現為介面(derived classes必須使用),定義部分表現為預設行為(derived classes明確提出申請才可以用)。
最後來看看Shape的non-virtual函數objectID;Shape::objectID是個non-virtual函數,這意味著它不打算在derived class中有不同行為。
- 聲明non-virtual函數的目的是為了令derived classes繼承函數 的介面及一份強制性實現。
可以把Shape::objectID看做“每個Shape對象都有一個用來產生識別碼的函數,這個識別碼採用相同計算方法。non-virtual函數代表的意義是不變性(invariant)淩駕特異性(specialization),所以不應該在derived classes中被重新定義,這個**條款**36討論的重點。
pure virtual函數對應只繼承介面;simple(impure) virtual函數對應繼承介面和一份預設實現;non-virtual函數對應繼承介面和一份強制實現。在設計classes時,要分清這些區別和聯絡,否則容易犯兩個錯誤:
- 第一個錯誤是將所有函式宣告為non-virtual。這會使derived classes沒有空間進行特化工作;non-virtual函數會給解構函式帶了問題(條款**7)。如果關心virtual函數的成本問題,可以參考條款**30的80-20法則。典型的程式有80%時間在執行20%代碼,函數中有80%的virtual函數不一定會給程式帶了多大效率損失,將心力花在20%代碼上才是關鍵。
- 第二個錯誤是將所有成員函式宣告為virtual。有時候這樣是正確的,例如**條款**31的Interface classes。如果有些函數在derived classes中不應該被重新定義,那麼就應該將這些函式宣告為non-virtual。
總結
- 介面繼承和實現繼承不同。在public繼承下,derived classes總是繼承base classes的介面。
- pure virtual函數只具體指定介面繼承。
- impure virtual函數具體指定介面繼承和預設實現繼承。
- non-virtual函數具體指定介面繼承和強制性實現繼承。