本文主要參考《Effective C++ 3rd》中的第六章部分章節的內容。
關注的問題集中在繼承、派生、virtual函數等。如:
這些都是我們將要從這一章裡學到的內容。
謹記public繼承的含義:
如果class D以public形式繼承class B,則每一個類型D的對象同時也是一個類型B的對象,反之不成立。
即,B比D表現出更一般化的概念,而D比B表現出更特殊的概念。
如:
class Person { ... }; class Student : public Person { ... };
這個體系告訴我們:每個學生都是人,但並非每個人都是學生。
從C++的角度來看,任何函數,如果期望獲得一個類型為Person(或指向Person對象的指針或引用),也都願意接受一個Student對象(或指針或引用)。
需要留意的一點是:
以我們在生活中的直覺為基礎來塑模is-a關系有時是錯誤的,可以說犯了“經驗主義錯誤”。
如:
class Square應該以public形式繼承class Rectangle嗎?
即正方形是一個(is-a)矩形嗎?
至少我們在學校裡是這麼學到的:正方形是一個矩形,但是矩形不一定是正方形。
那麼我們來寫一些這個繼承
class Rectangle { public: virtual void setHeight(int newHeight); virtual void setWidth(int newWidth); virtual int height() const; virtual int width() const; ...... }; void makeBigger (Rectangle& r) { int oldHeght = r.height(); r.setWidth(r.width() + 10); assert( r.heght() == oldHeght ); // 判斷r的高度是否改變,永為真。 }
在這個矩形的基礎上派生出一個正方形
class Square : public Rectangle { ... }; Square s; ... assert( s.width() == s.height() ); makeBigger(s); assert( s.widht() == s.height() );
顯然makeBigger只改變矩形的寬度,而不改變矩形的長度。這和s是個正方形矛盾。
public所包含的含義為:能夠使用在base class對象身上的每件事,應該同樣可以使用在derived class對象身上。
由此可見,其他領域或者生活中,我們習得的直覺,在軟件領域並不總是正確的。
因此,除了is-a關系,我們還要更多地思考和在適當的場合使用has-a和is-implemented-in-terms-of(根據某物實現出)
小結:
“public繼承”意味著is-a。適用於base classes身上的每一件事情一定也適用於derived classes身上,每一個derived class對象也都是一個base class對象。
關鍵字:作用域。
先看一個簡單的例子:
int x; void someFunc() { double x; std::cin >> x; }
這個讀取數據的語句使用的是局部變量x,而不是全局變量x。因為內層作用域的名稱會遮掩外圍作用域的名稱。
加入繼承機制,有如下的代碼:
class Base { private: int x; public: virtual void mf1() = 0; virtual void mf2() ; void mf3(); .... }; class Derived : public Base { public: virtual void mf1(); void mf4(); .... };
mf4函數中有如下實現:
void Derived::mf4() { ... mf2(); ... }
編譯器的查找作用域順序:
local作用域--->class Derived覆蓋的作用域
---> class覆蓋的作用域(本例到這停止)
---> Base的那個namespace作用域
---> global作用域。
現在來為上面的兩個類加幾個成員函數:
class Base { private: int x; pubic: virtual void mf1() = 0; virtual void mf1( int ); virtual void mf2() ; void mf3(); viod mf3( double ); .... }; class Derived : public Base { public: virtual void mf1(); void mf3(); void mf4(); .... };
這樣做會有什麼效果呢?
Derived d; int x; ...... d.mf1(); d.mf1(x); //error d.mf2(); d.mf3(); d.mf3(x); //error
由此可見,基於作用的名稱遮掩規則,並沒有因為重載函數而特殊處理,那些名字相同的重載函數同樣被遮掩掉了。
如果我們想在子類中繼承那些重載函數,並重寫其中的一部分(像本例中的mf1和mf3),那麼可以使用using語句
讓Base class內名為mf1和mf3的所有東西(所有重載函數)在Derived作用域內都是可見的。
class Base { private: int x; public: Base() {}; virtual void mf1() = 0; virtual void mf1( int m ) { std::cout << "Base mf1 int: "<< m << std::endl; } ; virtual void mf2() { std::cout << "Base mf2 " << std::endl; }; void mf3() { std::cout << "Base mf3" << std::endl;}; void mf3( double m ) { std::cout << "Base mf3 double:" << m << std::endl; }; }; class Derived : public Base { public: using Base::mf1; // 讓Base class內名為mf1和mf3的所有東西(所有重載函數) using Base::mf3; // 在Derived作用域內都是可見的。 virtual void mf1() { std::cout << "Derived mf1" << std::endl; }; void mf3() { std::cout << "Derived mf3" << std::endl; }; void mf4() { std::cout << "Derived mf4" << std::endl; }; };
調用:
Derived* d = new Derived(); d->mf1(); d->mf1(1); d->mf2(); d->mf3(); d->mf3(1); d->mf4();
運行截圖:
class Base { public: virtual void mf1() = 0; virtual void mf1(int ); .... }; class Derived : private Base { public: virtual void mf1() { Base::mf1(); } // 轉交函數 ...... };
小結:
derived classes內的名稱會遮掩base classes內的所有相同名稱的重載函數,在public繼承下這個機制並不希望發揮作用。
可使用using聲明式或轉交函數來調用被遮掩的重載函數。
選擇繼承的集中情況:
看一個幾何圖形例子:
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 { ...... };
首先考慮純虛函數draw
pure virtual函數有兩個最突出的特征:
綜合上面兩個特征:聲明一個純虛函數的目的是為了讓derived class只繼承函數接口
滿足了本節開頭的情景a。
考慮虛函數error。
虛函數的目的是讓derived classes繼承該函數的接口和缺省實現。滿足了情景b。
最後,考慮non-virtual函數objectID。
聲明non-virtual函數的目的是為了令derived classes繼承函數的接口及一份強制性實現。
對應了情景c。
純虛函數、虛函數和非虛函數使得你可以精確地指定你想要derived classes繼承的東西。
小結:
接口繼承和實現繼承不同。在public繼承之下,derived classes總是繼承base class的接口
pure virtual函數只具體指定接口繼承。
virtual 函數具體指定接口繼承及缺省實現繼承。
non-virtual函數具體指定接口繼承以及強制性實現繼承。
考慮為游戲內的人物設計一個繼承體系。
class GameCharacter { public: virtual int healthValue() const; // 返回人物的健康指數。 ...... };
有時候,常規的面向對象設計方法往往看起來是那麼的自然,以至於我們從未考慮其他的一些解法。
這一節就讓我們跳出常規設計的思維,考慮一些不那麼常規的設計方法。
方法1:借由non-virtual interface手法實現Template Method模式
class GameCharacter { public: int healthValue() const { ... int retVal = doHealthValue(); ... return retVal; } .... private: virtual int doHealthValue() const { ... } };
讓客戶通過public non-virtual成員函數間接調用private virtual函數,稱為non-virtual interface(NVI)手法。
這個non-virtual函數(healthValue)稱為virtual函數的包裝器(wrapper)。
從程序執行的角度來看,derived classes重新定義了virtual函數,從而賦予它們“如何實現功能”的控制能力,base classes保留控制“函數何時被調用”的權利。
方法2:借由Function Pointer實現Strategy模式
代碼如下:
class GameCharacter:; int defaultHealthCalc(const GameCharacter& gc); class GameCharacter { public: typedef int ( *HealthCalcFunc ) ( const GameCharacter& ); explicit GameCharacter( HealthCalcFunc hcf = defaultHealthCalc ) : healthFunc(hcf) { } int healthValue() const { return healthFunc(*this); } ... private: HealthCalcFunc healthFunc; };
還有其他的一些方法,在此並不一一討論,詳見《Effective C++》
在子類中重定義繼承而來的non-virtual函數,會導致子類你的設計出現矛盾。
比如在class Base有一個non-virtual函數setBigger,而所有繼承Base的子類都可以執行變大的動作,那麼這個動作就是一個不變性(共性)。
而在class Derived : public Base子類中,重寫了setBigger函數,那麼class Derived便無法反映出“不變性凌駕於特性”的性質。
從另一方面說,如果setBigger操作真的需要在子類中重定義,那麼就不應該把它設定為一個共性(non-virtual)。
因此,重新定義繼承來的non-virtual函數可能並不會對你的程序的運行造成太大的困擾,但是正如上面提到的,這是設計上的矛盾,或者說缺陷。
本小節的討論局限於“繼承一個帶有缺省參數值的virtual函數”。
理由:virtual函數動態綁定,缺省參數值靜態綁定。
class Shape { public: Shape() {}; enum ShapeColor { Red = "red", Green = "green" , Blue = "blue"}; virtual void draw(ShapeColor color = Red) const = 0 { std::cout << "This shape is " << color << std::endl; } }; class Rectangle : public Shape { public: Rectangle() {}; virtual void draw ( ShapeColor color = Green ) const; }; class Circle : public Shape { public: virtual void draw(ShapeColor color) const; };
先考慮如下指針
Shape* ps; Shape* pc = new Circle; Shape* pr = new Rectangle;
ps、pc、pr的靜態類型都是Shape*
所謂動態類型就是“目前所指對象的類型”。也就是說動態類型可以表現出一個對象將會有什麼行為。
在本例中,ps沒有動態類型,pc的動態類型為Circle*,pr的動態類型為Rectangle*。
動態類型可以在程序執行過程中改變(通常是經由賦值動作)。如
ps = pc; // ps的動態類型現在是Circle ps = pr; // ps的動態類型現在是Rectangle
上面是對動態綁定和靜態綁定的簡單復習。
現在,考慮帶有缺省參數值的virtual函數。
在上面的例子中,Shape中的draw函數的color默認參數是Red,而子類中的draw函數的color默認參數是Green。
Shape* shape = new Rectangle(); shape->draw();
根據動態綁定規則,上述代碼的輸出應該為:This shape is 1
但是運行代碼之後會發現,結果並不是我們想的那樣。
參考資料:
《Effective C++ 3rd》