程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> Effective C++筆記:繼承與面向對象設計

Effective C++筆記:繼承與面向對象設計

編輯:C++入門知識

關於OOP

博客地址:http://www.cnblogs.com/ronny 轉載請注明出處!

1,繼承可以是單一繼承或多重繼承,每一個繼承連接可以是public、protected或private,也可以是virtual或non-virtual。

2,成員函數的各個選項:virtual或non-virtual或pure-virtual。

3,成員函數和其他語言特性的交互影響:缺省參數值與virtual函數有什麼交互影響?繼承如何影響C++的名稱查找規則?設計選項有如些?如果class的行為需要修改,virtual函數是最佳選擇嗎?

4,public繼承意味著“is-a”。

5,virtual函數意味著“接口必須被繼承”,non-virtual意味著“接口和實現都必須被繼承”。

條款32:確定你的public繼承塑模出is-a關系

“public繼承”意味著is-a。適用於base classes身上的每一件事情一定也適合於derived class身上,因為每個derived classes對象也都是一個base classes對象。

上面這種關系聽起來頗為簡單,但有時候你的直覺可能會誤導你,比如,企鵝是一種鳥,這是事實。鳥可以飛,這也是事實,但是如果在用類描述時把企鵝定義為鳥的public繼承則會出問題,比如給基類定義fly函數。

條款33:避免遮掩繼承而來的名稱

derived classes內的名稱會遮掩base classes內的名稱。在public繼承下從來沒有人希望如些。

// 博客地址:http://www.cnblogs.com/ronny/ 轉載請注明出處!
class Base
{
private:
    int x;
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
    void mf3();
    void mf3(double);
};
class Derived :public Base
{
public:
    virtual void mf1();    //屏蔽了Base裡的mf1(int)
    void mf3();    // 屏蔽了Base裡的mf3
    void mf4();
};
Derived d;
int x;
d.mf1();    //ok,調用Derived::mf1
d.mf1(x);    //error,因為Derived::mf1遮掩Base=::mf1
d.mf2();    //ok,調用Base::mf2
d.mf3();    //ok,調用Derived::mf3
d.mf3(x);    // error,因為Derived::mf3遮掩了Base::mf3

Derived內的函數遮掩了Base內所有的同名函數。當然實現中如果是一種public繼承,則是一定需要同理繼承重載函數的,因為必須符合is-a關系。

我們可以用using聲明式達成目標:

class Derived :public Base
{
public:
    using Base::mf1;
    using Base::mf3;
    virtual void mf1();
    void mf3();
    void mf4();
};
Derived d;
int x;
d.mf1();    //ok,仍然調用Derived::mf1
d.mf1(x);    //現在沒有問題了
d.mf2();    //ok,調用Base::mf2
d.mf3();    //ok,仍然調用Derived::mf3
d.mf3(x);    //沒有問題了

我們還可以使用交換函數來完成這個功能:有時候我們並不想繼承base classes的所有函數。

class Derived :private Base
{
public:
    virtual void mf1()    // 轉交函數
    {
        Base::mf1();
    }
};

條款34:區分接口繼承和實現繼承

接口繼承和實現繼承不同。在public繼承之下,derived classes總是繼承base class的接口。

pure virtual函數只具體指定接口繼承

簡樸的(非純)impure virtual函數具體指定接口繼承及缺省實現繼承。

non-virtual函數具體指定接口繼承以及強制性實現繼承

條款35:考慮virtual函數以外的其他選擇

這條款談了兩種設計模式,鼓勵我們多思考的。

以游戲中的人物設計繼承體系為例子,不同的人物有不同的計算健康指數的方法,就叫healthValue函數吧,很自然的就會想到設計一個基類,把healthValue函數設計為virtual的用於繼承。

此條款提供了兩種不同的思路,用於virtual替代方案:

1. Template Method模式,由Non-Virtual Interface手法實現。

具體到以上的例子就是大概如下:

class GameCharacter {
public:
    int healthValue() const {
        // ...
        int retVal = doHealthValue();
        // ...
        return retVal;
    }
private:
    virtual int doHealthValue() const {
    }
};

 

其實最先提的把healthValue設計為virtual的方法也是Template Method模式,而這裡就是保留healthValue為public,而實現為private virtual(當然這裡是可以protected的),這樣的好處在於其中的前後“...”(省略號),這部分可以進行一些類似檢查、調整的操作,保證doHealthValue()在一個適當的場景下調用。而且子類也可以繼承實現private virtual成員函數。

2. Strategy模式,這一模式令實現方法是個變量,就算是同一個對象在不同的時段也可以有不同的實現方法。但這裡都有個約束,就是對私有成員變量的訪問限制。

a) Function Pointers實現,此種實現手法的約束是只能是函數,而且形式受函數的簽名(參數數量,參數類型,返回類型)的約束。

b) tr1::function實現,擺脫了a)的約束,支持隱式類型轉換,還支持函數對象或者是成員函數(通過std::tr1::bind實現)

c) 古典實現,其實就是對象實體是一類,而實現方法是另一類。

條款36:絕對不重新定義繼承而來的non-virtual函數

當派生類中重新定義了基類中的non-virtual函數時,如果用指向基類類型的指針(實際指向派生類對象)來訪問該non-virtual函數時,訪問的是基類對象的non-virtual函數。

造成上面行為的原因是,non-virtual是靜態綁定的,區別於virtual函數的動態綁定。

條款37:絕不重新定義繼承而來的缺省參數值

virtual函數系動態綁定的,而缺省參數值是靜態綁定的。

所謂對象的靜態類型:在程序中被聲明時被采用的類型。而對象的動態類型是指“目前所指對象的類型”,也就是說,動態類型可以表現出一個對象將會有什麼行為。

class Shape
{
public:
    enum ShapeColor{ Red, Green, Blue };
    virtual void draw(ShapeColor color = Red)const = 0;
};
class Rectangle :public Shape
{
public:
    virtual void draw(ShapeColor color = Green)const = 0;
};

上面的代碼中,如果我們定義了一個類型為Shape*的指針指向Rectangle。

Shape* ps=new Rectangle;

ps->draw();

我們的實際願望可能是希望讓draw的默認參數為Green,但是實際上確是Red,因為默認參數是靜態綁定的。

也許你可能會想讓派生類的中virtual函數和base類中保持一致。但是下面的代碼顯然也是不可行的。

class Shape
{
public:
    enum ShapeColor{ Red, Green, Blue };
    virtual void draw(ShapeColor color = Red)const = 0;
};
class Rectangle :public Shape
{
public:
    virtual void draw(ShapeColor color = Red)const = 0;
};

上面不僅會產生代碼重復,而且帶來了相依性(如果Shape內的缺省參數變了,所有派生類的virtual函數缺省值都要修改)。

聰明的做法是考慮替代設計,如條款35中的一些virutal函數的替代設計,其中之一是NVI手法,令base class內的一個public non-virtual函數調用private virtual函數。

class Shape
{
public:
    enum ShapeColor{ Red, Green, Blue };
    virtual void draw(ShapeColor color = Red)const
    {
        doDraw(color);
    }
private:
    virtual void doDraw(ShapeColor color)const = 0;
};
class Rectangle :public Shape
{
private:
    virtual void doDraw(ShapeColor color)const;
};

條款38:通過復合塑模出has-a或“根據某物實現出”

復合或包含意味著has-a。如果我們想設計一個自己的set,我們思考後覺得可以用list來實現它,但是如果我把它設計出list的一個派生類,就會有問題,因為父類的所有行為在派生類都是被允許的,而list允許元素重復,而set則顯然不行,所以set與list之間不符合is-a關系,我們可以把list設計為set的一個成員,即包含關系(has-a)。

條款39:明知而審慎地使用private繼承

private繼承意味著is-implemented-in-terms of(根據某物實現)。它通常比復合的級別低。但是當derived class需要訪問protected base class的成員,或需要重新定義繼承而來的virtual函數時,這麼設計是合理的。

和復合不同,private繼承可以造成empty base最優化。這對致力於“對象尺寸最小化”的程序庫開發者而言,可能很重要。

class Empty{};
class HoldsAnInt
{
private:
    int x;
    Empty e;
};

這時候你會發現sizeof(HoldsAnInt)>sizeof(int),因為一些內存對齊的要求。

而如果是:

class HoldsAnInt :private Empty{
private:
    int x;
};

這時候幾乎可以肯定:sizeof(HoldsAnInt)==sizeof(int)。

條款40:明智而審慎地使用多重繼承

使用多重繼承就要考慮歧義的問題(成員變量或者成員函數的重名)。

最簡單的情況的解決方案是顯式的調用(諸如item.Base::f()的形式)。

復雜一點的,就可能會出現“鑽石型多重繼承”,以File為例:

class File { ... }
class InputFile : public File { ... }
class OutputFile : public File { ... }
class IOFile : public InputFile, public OutputFile { ... }

這裡的問題是,當File有個filename時,InputFile與OutputFile都是有的,那麼IOFile繼承後就會復制兩次,就有兩個filename,這在邏輯上是不合適的。解決方案就是用virtual繼承

class File { ... }
class InputFile : virtual public File { ... }
class OutputFile : virtual public File { ... }
class IOFile : public InputFile, public OutputFile { ... }

這樣InputFile與OutputFile共享的數據就會在IOFile中只保留一份了。

但是virtual繼承並不常用,因為:

1. virtual繼承會增加空間與時間的成本。

2. virtual繼承會非常復雜(編寫成本),因為無論是間接還是直接地繼承到的virtual base class都必須承擔這些bases的初始化工作,無論是多少層的繼承都是。

請記住

多重繼承比單一繼承復雜。它可能導致新的歧義性,以及對virtual繼承的需要。

virtual繼承會增加大小、速度、初始化(賦值)復雜度等等成本。如果virtual base classes不帶任何數據,將是最具實用價值的情況。

多重繼承的確有正當用途。其中一個情節涉及“public繼承某個Interface class”和“private繼承某個協助實現的class”的兩相組合。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved