Effective C++筆記⑥

繼承和麪向對象設計

面向對象編程(OOP)幾乎已經風靡了兩個年代了,所以關於繼承、派生、virtual函數等等。儘管如此,C++的OOP有可能和你原本習慣的OOP稍有不同:“繼承”可以是單一繼承或多重繼承,每一個繼承連接(link)可以是public,protected或private,也可以是virtual或non-virtual。本章節需要理解的問題:

  1. 成員函數的各個選項:virtual? non-virtual? pure virtual?
  2. 缺省參數值與virtual函數有什麼交互影響?
  3. 繼承如何影響C++的名稱查找規則?
  4. 設計選項有哪些?
  5. 如果class的行爲需要修改,virtual函數是最佳選擇嗎?

 

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

C++對於public繼承,如下:

class Person { ... };
class Student: public Person { ... };

根據生活經驗我們知道,每個學生都是人,但並非每個人都是學生。這邊是這個繼承體系的主張。我們預期,對人可以成立的每一件事,對學生來說也都成立。

於是,在C++領域中,任何函數如果期望獲得一個類型爲Person(指針或引用)的實參,都也願意接受一個Student對象(或指針或引用):

void ear(const Person& p);
void study(const Student& s);
Person p;
Student s;
eat(p);
eat(s);
study(s);
study(p);        //錯誤,並非每個人都會學習

這個論點只對public繼承才成立。只有當Student以public形式繼承Person,C++的行爲纔會如我所描述。private繼承的意義與此完全不同(見條款39),至於protected繼承......

public繼承和is-a之間的等價關係聽起來頗爲簡單,但有時候你的直覺可能會誤導你。舉個例子,企鵝是一種鳥,鳥可以飛,但如果以C++的語言去描述這層關係,結果如下:

class Bird{
public:
    virtual void fly();
    ...
};

class Penguin:public Bird{
    ...
};

這個體系告訴我們,企鵝是鳥,所以它也能夠飛,但我們知道那是絕不可能發生的。

我們可以將其改成會飛的鳥才能夠繼承飛這一特性:

class Bird { ... };

class FlyingBird: public Bird{
public:
    virtual void fly();
    ...
};

class Penguin: public Bird { ... };

又如下段代碼:

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 oldHeight = r.height();
    r.setWidth(r.width() + 10);
    assert(r.height() == oldHeight);
}

顯然,上述的assert結果永遠爲真。因爲makeBigger只改變r的寬度:r高度從未被改變。

現在考慮這段代碼,其中使用public繼承,允許正方形被視爲一種矩形:

class Square: public Rectangle { ... };
Square s;
...
assert(s.width() == s.height());
makeBigger(s);
assert(s.width() == s.height());

顯然,對於上述的判斷均爲真,畢竟正方形的邊長都是一樣的。

但現在我們遇上了一個問題。我們如何調節下面哥哥assert判斷式:

  1. 調用makeBigger之前,s的高度和寬度相同;
  2. 在makeBigger函數內,s的寬度改變,但高度不變;
  3. makeBigger返回之後,s的高度再度和其寬度相同;

我們應該確定確實瞭解這些個“classes相互關係”之間的差異,並知道如何在C++中最好地塑造他們。

 

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

諸如以下代碼:

int x;            //全局變量
void someFunc()
{
    double x;     //作用域內變量
    std::cin>>x;  //賦予x新值
}

這個讀取數據的語句指涉的是local變量x,而不是global變量x,因爲內層作用域的名稱會遮掩外圍作用域的名稱。我們可以這麼看本例作用域形勢:

現在導入繼承。我們知道,位於一個derived class成員函數內指涉base class內的某物(也許是某個成員函數、typedef、或成員變量)時,編譯器可以找出我們所指涉的東西,因爲derived classes繼承了聲明於base classes內的所有東西。實際運作方式是,derived class作用域被嵌套在base class作用域內,像這樣:

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();
    ...
};

此例內含private、public名稱,以及一組成員變量和成員函數名稱。這些成員函數包括pure virtual、impure virtual和non-virtual三種,這是爲了強調我們談的是名稱,和其他無關。本例使用單一繼承,然而一旦瞭解單一繼承下發生的事,很容易就可以推想C++在多重繼承下的行爲。

假設derived class內的mf4的實現碼像這樣:

void Derived::mf4()
{
    ...
    mf2();
    ...
}

當編譯器看到這裏使用名稱mf2,必須估算它指涉什麼東西。編譯器的做法是查找各作用域,看看有沒有某個名爲mf2的聲明式。首先查找local,而後查找外圍,也就是class Derived覆蓋的作用域。本例爲base class,在那兒編譯器找到一個名爲mf2的東西了,所以停止了查找。

再次考慮前一個例子,這次重載mf1和mf3,並且添加一個新版mf3到Derived去。如條款36所說,這裏發生的事情是:Derived重載了mf3,那是一個繼承而來的non-virtual函數。

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();
    void mf3();
    void mf4();
    ...
};

以上代碼中以作用域爲基礎的“名稱遮掩規則”並沒有改變,因此base class內素有名爲mf1和mf3的函數都被derived class內的mf1和mf3函數遮掩掉了。從名稱查找觀點來看,Base::mf1和Base::mf3不再被Derived繼承。

Derived d;
int x;
...
d.mf1();
d.mf1(x);    //錯誤,Derived::mf1遮掩了Base::mf1
d.mf2();
d.mf3();
d.mf3(x);    //錯誤,Derived::mf3遮掩了Base::mf3

要解決上述問題,我們可以使用using聲明式達成目標:

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:
    using Base::mf1;        //讓Base class內名爲mf1和mf3的所有東西
    using Base::mf3;        //在Derived作用域內都可見(並且public)
    virtual void mf1();
    void mf3();
    void mf4();
    ...
};

這意味着如果你繼承base class並加上重載函數,而你又希望重新定義或複寫其中一部分,那麼你必須爲那些原本會遮掩的每個名稱引入一個using聲明式,否則某些你希望繼承的名稱會被遮掩。

以上都是在public下進行的繼承,但是如果我們以private形式繼承,而Derived唯一想繼承的mf1是那個無參數版本。using聲明式在這裏排不上用場,因爲using聲明式會令繼承而來的某給定名稱之所有同名函數在derived class中可見。我們僅僅需要一個簡單的轉交函數:

class Base{
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    ...
};

class Derived: private Base{
public:
    virtual void mf1() {    //轉交函數
        Base::mf1();        //暗自成爲inline(見條款30)
    }
    ...
};

...
Derived d;
int x;
d.mf1();    
d.mf1(x);    //錯誤,Base::mf1()被遮掩了

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

表面上是public繼承概念,經過嚴密的檢查之後,發現它由兩部分組成:

  • 函數接口繼承
  • 函數實現繼承

爲了更好地感覺上述選擇之間的差異,我們考慮一個展現繪圖程序中各種幾何形狀的class繼承體系:

class Shape{
public:
    virtual void draw() const = 0;
    virtual void error(const std::string& msg);
    int objextID() const;
    ...
};

class Rectangle: public Shape { ... };
class Ellipse: public Shape { ... };

Shape是個抽象class,它的純虛函數draw使他成爲一個抽象class。所以客戶不能夠創建Shape class的實體,只能創建其派生類的實體。儘管如此,Shape還是強烈影響了所有以public形式繼承它的派生類,因爲:

  • 成員函數的接口總是會被繼承。一如條款32所說,public繼承意味着is-a,所以對基類爲真的任何事情一定也對其派生類爲真。因此如果某個函數可施行於某class上,一定也可施行於其派生類上。

對於上述代碼中不同的聲明,我們需要進行仔細的探討:

class Shape{
public:
    virtual void draw() const = 0;
    ...
};

純虛函數有兩個突出的特性:

  1. 必須被任何“繼承了它們”的具象class重新聲明;
  2. 在抽象class中通常沒有定義;

【注】聲明一個純虛函數的目的是爲了讓派生類只繼承函數接口。

令人意外的是,我們可以爲純虛函數提供定義。也就是說可以爲draw供應一份實現代碼,C++並不會對此發出怨言,但調用它的唯一途徑是“調用時明確指出其class名稱”:

Shape* ps = new Shape;        //錯誤,Shape是抽象的
Shape* ps1 = new Rectangle;
ps1->draw();                  //調用Rectangle::draw
Shape* ps2 = new Ellipse;
ps2->draw();                  //調用Ellipse::draw
ps1->Shape::draw();           //調用Shape::draw
ps2->Shape::draw();           //調用Shape::draw

【注】聲明簡樸的非純虛函數的目的,是讓派生類繼承該函數的接口和缺省實現。

考慮如下例子:

class Shape{
public:
    virtual void error(const std::string& msg);
    ...
};

其接口表示,每個class都必須支持一個“當遇上了錯誤可調用”的函數,但每個class可自由處理錯誤。如果某個class不想針對錯誤做出任何特殊行爲,它可以回退到Shape class提供的缺省錯誤處理行爲。

最後,讓我們看看Shape的非虛函數objectID:
 

class Shape{
public:
    int objectID() const;
    ...
};

如果成員函數是個non-virtual函數,意味是它並不打算在派生類中有不同的行爲。實際上一個非虛成員函數所表現的不變性凌駕其特異性,因爲它表示不論派生類變得多麼特異化,它的行爲都不可以改變。

【注】聲明非虛函數的目的是爲了令派生類繼承函數的接口及一份強制性實現。

 

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

假設你正在寫一個視頻遊戲軟件,你打算爲遊戲內的人物設計一個繼承體系。你的遊戲屬於暴力砍殺類型,劇中人物被傷害或因其他因素而降低健康狀態的情況並不罕見。你因此決定提供一個成員函數healthValue,它會返回一個整數,表示人物健康程度。由於不同的人物可能以不同的方式計算他們的健康指數,將healthValue申明爲virtual似乎是再明白不過的做法:

class GameCharacter{
public:
    virtual int healthValue() const;
    ...
};

但是上面的申明缺還是有明顯的缺點。

 

藉由non-virtual interface手法實現Template Method模式

這一個流派主張virtual函數應該幾乎總是private。這個流派的擁護者建議,較好的設計是保留healthValue爲public成員函數,但讓它成爲non-virtual,並調用一個private virtual函數進行實際工作:

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)手法。就是Template Method設計模式的一個獨特表現形式。這樣一個手法的優點隱身於上述代碼註釋“事前/事後工作”中。能夠告訴你的代碼保證在“virtual函數進行真正工作之前和之後”被調用。

但是這有個需要注意的點:NVI手法設計在派生類內重新定義private virtual函數。“重新定義virtual函數”表示某些事“如何被完成,”調用virtual函數”則表示它“何時”被完成。各自相互獨立。其實在NVI手法下沒必要讓virtual函數一定得是private。某些class繼承體系要求派生類在virtual函數的實現內必須調用其基類的對應兄弟,而爲了讓這樣的調用合法,virtual函數必須是protected,不能是private。

 

藉由Function Pointers實現Strategy模式

NVI手法對public virtual函數而言是一個有趣的替代方案,但從某種設計角度觀之,它只比窗飾花樣更強一些而已。畢竟我們還是使用virtual函數來計算每個人的健康指數。另一個更戲劇性的設計主張“人物健康指數的計算與人物類型無關”,這個的計算完全不需要“人物”這個成分。例如我們可能會要求每個人物的構造函數接受一個指針,指向一個健康計算函數,而我們可以調用該函數進行實際計算:

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;
};

這個做法是常見的Strategy設計模式的簡單應用。拿他和“植基於GameCharacter繼承體系內之virtual函數”的做法不比較,它提供了某些有趣彈性:

同一人物類型之不同實體可以有不同的健康計算函數。例如:

class EvilBadGuy: public GameCharacter{
public:
    explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter(hcf)
    {
        ...
    }
    ...
};

int loseHealthQuickly(const GameCharacter&);    //健康指數計算函數1
int loseHealthSlowly(const GameCharacter&);     //健康指數計算函數1

EvilBadGuy ebg1(loseHealthQuickly);             //相同類型的人物搭配
EvilBadGuy ebg2(loseHealthSlowly);              //不同的健康計算方式

某已知人物之健康指數計算函數可在運行期變更。例如GameCharacter可提供一個成員函數setHealthCalculator,用來替換當前的健康指數計算函數。

 

藉由tr1::function完成Strategy模式

一旦習慣了templates以及它們對隱式接口(見條款41)的使用,基於函數指針的做法看起來便過分苛刻和死板了。如果我們不再使用函數指針,而是改用一個類型爲tr1::function的對象,這些約束就全都不見了。就如條款54所說,這樣的對象可持有任何可調用物(也就是函數指針、函數對象、或成員函數指針),只要其簽名式兼容於需求端。以下將剛纔的設計改爲使用tr1::functiion:

class GameCharacter;        //前置聲明
//以下函數是計算健康指數的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
    //HealthCalcFunc可以是任何“可調用物”,可被調用並接受任何兼容
    //於GameCharacter之物,返回任何兼容於int的東西,詳下。

    typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
    {}
    
    int healthValue() const
    {
        return healthFunc(*this);
    }
    ...

private:
    HealthCalcFunc healthFunc;
};

上面的代碼中,typedef處代表的函數是“接受一個引用指向const GameCharacter,並返回int”。這個tr1::function類型產生的對象可以持有任何與此簽名式兼容的可調用物。所謂兼容,意思是這個可調用物的參數可被隱式轉換爲const GameCharacter&,而其返回類型可別被隱式轉換爲int。

 

古典的Strategy模式

傳統的Strategy做法會將健康計算函數做成一個分離的繼承體系中的virtual成員函數,如下:

對應的代碼如下:

class GameCharacter;
class HealthCalcFunc{
public:
    ...
    virtual int calc(const GameCharacter& gc) const
    { ... }
    ...
};

HealthCalcFunc defaultHealthCalc;
class GameCharacter{
public:
    explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc) 
            :pHealthCalc(phcf) {}
    int healthValue() const
    {
        return pHealthCalc->calc(*this);
    }
private:
    HealthCalcFunc* pHealthCalc;
};

 

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

假設Class D系由Class B以public形式派生而來,Class B定義有一個public成員函數mf。由於mf的參數和返回值都不重要,所以我假設兩者皆爲void。換句話說的意思是:

class B{
public:
    void mf();
    ...
};

class D: public B { ... };

同時我們做如下操作:

D X;

B* pB = &x;        //基類指針指向子類對象
pB->mf();

D* pD = &x;        //子類指針指向子類對象
pD->mf();

由於兩者調用的函數相同,憑藉的對象也相同,所以行爲也相同嗎?

答案是否認的。如果mf是個non-virtual函數而D定義有自己版本的mf函數,那就不是如此:

class D: public B{
public:
    void mf();
    ...
};
pB->mf();    //調用B::mf
pD->mf();    //調用D::mf

造成此一兩面行爲的原因是,non-virtual函數如B::mf和D::mf都是靜態綁定。這意思是,由於pB被聲明爲一個指向B的指針,通過B調用的non-virtual函數永遠是B定義的版本,即使pB指向一個類型爲“B派生之class”的對象。

但如果mf是個virtual函數,當mf被調用,任何一個D對象都可能表現出B或D的行爲:決定因素不在對象自身,而在於“指向該對象之指針”當初的聲明類型。

 

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

一開始你只能繼承兩種函數:

  1. virtual函數;
  2. non-virtual函數;

然而重新定義一個繼承而來的non-virtual函數永遠都是錯誤的(見條款36),所以我們可以安全地將本條款的討論侷限於“繼承一個帶有缺省參數值的virtual函數”。

所欲爲的靜態類型,就是它在程序中被聲明時所採用的類型。考慮一下的class繼承體系:

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;
    ...
};

class Circle: public Shape{
public:
    virtual void draw(ShapeColor color) cosnt;
    //注意:以上這麼寫則當客戶以對象調用此函數,一定要指定參數值
    //     因爲靜態綁定下這個函數並不從其base繼承缺省參數值
    //     但若以指針(或引用)調用此函數,可以不指定參數值
    //     因爲動態綁定下這個函數會從其base繼承缺省參數值
};

現在考慮以下指針:

Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;

【注】上述都以Shape*爲靜態類型!

對象的動態類型則是指“目前所指對象的類型”。也就是說,動態類型可以表現出一個對象將會有什麼行爲。就以上而言,pc的動態類型就是Circle*,pr的動態類型就是Rectangle*。而ps沒有動態類型,因爲其沒有指向任何對象。

動態類型如名稱所示,可以在程序執行過程中改變(通常經由賦值動作)。

virtual函數系動態綁定而來,意思是調用一個虛函數時,酒精調用哪一份函數來實現代碼,取決於發出調用的那個對象的動態類型:

pc->draw(Shape::Red);    //調用Circle::draw(Shape::Red)
pr->draw(Shape::Red);    //調用Rectangle::draw(Shape::Red)

同時,虛函數是動態綁定的,而缺省參數是靜態綁定的。你可能在“調用一個派生類內的虛函數”時,調用基類爲它指定的缺省參數值:

pr->draw();    //調用Rectangle::draw(Shape::Red)

當你試着遵守這條規則,並且同時提供缺省參數值給基類和派生類用戶,又會發生什麼事呢?

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;
    ...
};

代碼重複,且帶着相依性:如果Shape內的缺省參數值改變了,所有“重複給定缺省參數值”的那些派生類也不必須改變,苟澤它們最終會導致“重複定義一個繼承而來的缺省參數值”。怎麼辦?

當想令虛函數表現出你所想要的行爲但卻遭到麻煩,聰明的做法是考慮替代設計。條款35列了不少virtual函數的替代設計,其中之一是NVI手法,令基類內的一個公有非虛函數調用私有虛函數,後者可被派生類重新定義。在這裏,我們可讓非虛函數指定缺省參數,而私有虛函數負責真正的工作:

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

class Rectangle: public Shape{
public:
    ...
private:    
    virtual void doDraw(ShapeColor color) const;    //不能指定缺省參數值
    ...
};

由於非虛函數絕對不被派生類複寫(見條款36),這個設計很清楚地使draw函數的color缺省參數值總是爲Red。

 

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

複合是類型之間的一種關係,當某種類型的對象內含它種類型的對象,便是這種關係。例如:

class Address { ... };
class PhoneNumber { ... };

class Person{
public:
    ...
private:
    std::string name;        //合成成分物
    Address address;         //同上
    PhoneNumber voiceNumber; //同上
    PhoneNumber faxNumber;   //同上
};

本例中,Person對象由string、Address、PhoneNumber構成。在程序員之間複合這個屬於有許多同義詞:

  • 分層(layering)
  • 內含(containment)
  • 聚合(aggregation)
  • 內嵌(embeding)

區分is-a(是一種)和is-implemented-in-terms-of(根據某物實現出)這兩種對象關係是比較麻煩的。假設你需要一個template,希望製造出一組classes用來表現出不重複對象組成的sets。由於複用(reuse)是件美妙無比的事情,你的第一個直覺是採用不標準程序庫提供的set template。

不幸的是,set的實現往往招致“每個元素耗用三個指針”的額外開銷。因爲sets通常以平衡查找樹實現而成,使得在查找、安插、移除元素時保證擁有對數時間效率。

如果你是一位數據結構專家,就會知道實現set的方法太多了,其中一種便是在底層採用linked lists。而標準程序庫有一個list template,於是可以複用它。更明確的說,你決定讓你那個萌芽中的set template繼承std::list。也就是Set<T>繼承List<T>。於是,對Set template聲明如下:

template<typename T>
class Set: public std::list<T> { ... };

但正如條款32所說,如果D是一種B,對B爲真的每一件事對D也都是真的。那麼list可以存儲重複的元素,但是set可以嗎?

由於這兩個classes之間並非is-a關係,所以public繼承不適合用來塑模它們。正確的做法是,你應當瞭解,Set對象可根據一個list對象實現出來:

template<class T>
class Set{
public:
    bool member(const T& item) const;
    void insert(const T& item);
    void remove(const T& item);
    std::size_t size() const;
private:
    std::list<T> rep;            //用來表述Set的數據
};

Set成員函數可大量依賴list及標準程序庫其他半部分提供的機能來完成,所以其實很直觀也很簡單,只要你熟悉以STL編寫程序:

template<typename T>
bool Set<T>::member(const T& item) const
{
    return std::find(rep.begin(), rep.end(), item)!=rep.end();
}

template<typename T>
void Set<T>::insert(const T& item)
{
    if(!member(item)) rep.push_back(item);
}

template<typename T>
void Set<T>::remove(const T& item)
{
    typename std::list<T>::iterator it = std.find(rep.begin(), rep.end(), item);
    if(it != rep.end()) rep.erase(it);
}

template<typename T>
std::size_t Set<T>::size() const
{
    return rep.size();
}

這些函數如此的簡單,適合成爲inling的候選人,但是別忘了查看條款30!

 

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

例子如下:

class Person { ... };
class Student: private Person { ... };
void eat(const Person& p);            //任何人都會喫
void study(const Student& s);         //只有學生纔在學校學習

Person p;                             //p是人
Student s;                            //s是學生

eat(p);                               //沒問題,p是人,會喫
eat(s);                               //錯誤!

【注】

  1. 如果classes之間的繼承關係是private,編譯器不會自動將一個派生類對象(例如Student)轉換爲一個基類對象(例如Person);
  2. 而且private繼承中,所有繼承過來的成員,在派生類中均爲private屬性;

Private繼承的意義:

如果讓D類繼承B類,那麼這是爲了採用B類內已經備妥的某些特性,不是因爲B類和D類對象存在有任何觀念上的關係。private繼承純粹只是一種實現技術。借用條款34提出的術語,private繼承意味只有實現部分被繼承,接口部分應略去。

那麼何時才使用private繼承呢?

  • 當protected成員和/或virtual函數牽扯進來的時候;
  • 當一個意欲成爲派生類者訪問一個意欲成爲基類者的protected成分;
  • 爲了重新定義一或多個virtual函數;

 

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

需要認清的一件事是,當多重繼承進入設計景框,程序又可能從一個以上的基類繼承相同名稱(如函數、typedef等)。那會導致較多的歧義機會。例如:

class BorrowableItem{    //圖書館允許借東西
public:
    void checkOut();     //離開時進行檢查
    ...
};

class ElectronicGadget{
private:
    bool checkOut() const;    //執行自我檢測
    ...
};

class MP3Player: public BorrowableItem,public ElectronicGadget
{
    ...
};

MP3Player mp;
mp.checkOut();    //產生歧義,到底是調用了哪個類中的函數?

爲了解決以上帶來的歧義,我們必須明確指出調用哪一個基類內的函數:

mp.BorrowableItem::checkOut();

如果調用了另外一個基類的函數,則會獲得“嘗試調用private成員函數”的錯誤!

多重繼承的意思是繼承一個以上的基類,但這些基類並不常在繼承體系中又有更高級的基類,因爲那會導致要命的“磚石型多重繼承“,又稱“菱形繼承”,即:

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

而解決這個的辦法就是:虛繼承!

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

使用虛繼承,有以下的建議:

  1. 非必要不使用virtual bases,平常可使用non-virtual繼承;
  2. 如果必要,儘可能避免在其中放置數據;

現在來看一個例子 ------ 塑模“人”的C++接口類:

class IPerson{
public:
    virtual ~Person();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
};

IPerson的客戶必須以IPerson的指針和引用來編寫程序,因爲抽象類無法被實體化創建對象。爲了創建一些可當做IPerson來使用的對象,IPerson的客戶使用工廠函數將“派生自IPerson的具象classes”實體化:

//工廠函數,根據一個獨一無二的數據庫ID創建一個Person對象
std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);

//這個函數從使用者手上獲取一個數據庫ID
DatabaseID askUserForDatababseID();

DatabbaseID id(askUserForDatabaseID());
std::tr1::shared_ptr<IPerson> pp(makePerson(id));

但是makePerson如何創建對象並返回一個指針指向它呢?無疑地一定有某些派生自IPerson的具象class,在其中makePerson可以創建對象。

假設這個class名爲CPerson。CPerson必須提供“繼承自IPerson”的純虛函數的實現代碼。例如,假設有個既有的數據庫相關class,名爲PersonInfo,提供CPerson所需要的實質東西:

class PersonInfo{
public:
    explicit PersonInfo(DatabbaseID pid);
    virtual ~PersonInfo();
    virtual const char* theName() const;
    virtual const char* theBirthDate() const;
    ...
private:
    virtual const char* valueDelimOpen() const;
    virtual const char* valueDelimClose() const;
    ...
};

PersonInfo被設計用來協助以各種格式打印數據庫字段,每個字段值的起始點和結束點以特殊字符串爲界。缺省的頭尾界限符號是方括號(中括號),所以(例如)字段值“Ring-tailed Lemur”將被格式化爲:

[ Ring-tailed Lemur ]

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章