effective c++之繼承與面向對象設計

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

請記住:

1.   public繼承”意味着is-a。適用於baseclasses身上的每一件事情一定也適用於derived classes身上,因爲每一個derived class對象也都是一個base class對象。


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

在C++的繼承體系中,不管是變量名還是函數名稱的查找按照一定的規則進行:首先在局部作用域中進行查找,如果找到則停止;如果沒找到則到外層進行查找,依次類推。特別需要注意的是,C++在查找的時候,只會對名字進行比較查找,與類型無關。例如:

class  Base
{
private:
    int x;
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
    void mf3();
    void mf3(double);
};
 
class Derive:public  Base
{
public:
    virtual void mf1();
    void mf3();
    void mf4();
};
Derive d;
int x;
...
d.mf1();//callmf1() in Derive
d.mf1(x);//errorin Derive,the function mf1(int) in the base class if hidden
d.mf2();//ok callBase::mf2()
d.mf3();//ok call Derive::mf3()
d.mf3(x)//error in Derive,no function mf3(int),mf3(int) in Base is hidden

這些行爲背後的基本理由是爲了防止你在程序庫或應用框架內建立新的derived class時附帶地從疏遠的baseclass繼承重載函數。 但是你通常會想繼承重載函數。實際上如果你正在使用public繼承而又不繼承那些重載函數,就違反了base和derived之間的is-a關係,不過幸運的是,你可以使用using聲明式達成目標。

#include"stdafx.h"
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();
 
       using Base::mf1;
       using Base::mf3;
};

有時候不想使用所有基類的重載函數,比如上例中只想使用無參的mf1(),可以使用轉交函數(forwarding function)。

class Derived :public Base
{
public:
       virtual void mf1(){Base::mf1();}
};

Inline轉交函數的另一個用途是爲那些不支持using聲明的老舊編譯器另闢新路。

請記住:

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

2.    爲了讓被遮掩的名稱再見天日,可使用using聲明式或轉交函數(forwarding functions)。


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

表面上直截了當的公有繼承的概念,實際上包含兩個相互獨立的部分:函數接口的繼承和函數實現的繼承。

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

二、pure virtual純虛函數只具體指定接口繼承

聲明一個pure virtual函數的目的是爲了讓derived class只繼承函數接口。令人意外的是,竟然可以爲純虛函數提供定義,也就是爲它提供一份實現代碼,C++對它並沒有任何怨言,但調用它的唯一途徑是“調用時明確指出期class的名稱”。

ps1->draw();
Sharp* ps2 =new Rectangle;
ps2->draw();
ps1->Shape::draw();     //調用Shape的draw
ps2->Shape::draw();

這樣對純虛函數的實現用途有限;但是一如稍後你將看到,它可以實現一種機制:爲普通虛函數提供更平常更安全的缺省實現。

三、Impure virtual普通虛函數具體指定接口繼承及缺省實現繼承

聲明impure virtual函數的目的,是讓derived classes繼承該函數的接口和缺省實現(普通虛函數是告訴派生類的設計者:你必須支持一個error函數,但如果你不想自己寫一個,可以使用基類Shape提供的缺省版本)。

但是,允許impure virtual函數同時指定函數聲明和函數缺省行爲,卻有可能造成危險。例如:

class Airport{...};              //用以表示機場
class Airplane{
public:
    virtual void fly(const Airport&destination);
};
voidAirplane::fly(const Airport& destination) {
    //缺省代碼,將飛機飛至指定目的地
}
class ModelA :public Airplane {...};
class ModelB :public Airplane {...};
class ModelC :public Airplane {
...//未聲明fly函數,但它並不希望缺省飛行
};
AirportPDX(...);        //PDX是我家附近的機場
Airplane* pa =new ModelC;
...
pa->fly(PDX)//災難

A型和B型兩種飛機,它們以相同方式飛行,因此有了上述的繼承方式。基類Airplane::fly被聲明爲虛函數,爲了避免A和B撰寫相同的飛行代碼,爲Airplane::fly提供了缺省的飛行方式,這被A和B繼承。但是新的C型飛機的飛行方式和A、B的飛行方式不一樣,但是如上代碼:忘記爲C定義fly函數,這將是災難。解決方法就是將fly寫成純虛函數,這樣C不得不實現fly函數。可以輕易通過“只有派生類明確說明要用基類缺省行爲才爲派生類提供缺省實現,否則免談”。此間伎倆在於切斷“虛函數接口”和“缺省實現”之間的連接:

class Airplane{
public:
    virtual void fly(const Airport&destination) = 0;   //注意,純虛函數,僅提供飛行接口
    ...
protected:
    void defaultFly(const Airport&destination);    //fly的缺省行爲
};
voidAirplane::defaultFly(const Airport& destination) {
    //缺省行爲,將飛機飛至目的地
}
想要缺省實現的A和B可以這樣做:
class ModelA:public Airplane {
public:
    virtual void fly(const Airport&destination) {  //實現缺省飛行行爲
        defaultFly(destination);
    }
    ...
};
class ModelB:public Airplane {
public:
    virtual void fly(const Airport&destination) {
        defaultFly(destination);
    }
    ...
};

這樣C就不可能繼承不正確的fly實現代碼了,必須自己實現fly:

class ModelC:public Airplane {
public:
    virtual void fly(const Airport&destination);
    ...
};
voidModelC::fly(const Airport& destination) {
    //將C型飛機飛至指定的目的地
}

此外,有些人反對以不同的函數分別提供接口和缺省實現,向上述的fly和defaultFly那樣。他們擔心過度雷同的函數名稱會引起類命名空間污染問題。但是他們也同意,接口和缺省實現應該分開。給純虛函數定義:

class Airplane{
public:
    virtual void fly(const Airport& destination) = 0;
    ...
};
voidAirplane::fly(const Airport& destination) {   //純虛函數實現
    //缺省行爲,將飛機飛至指定目的地
}
class ModelA:public Airplane {
public:
    virtual void fly(const Airport&destination) {
        Airplane::fly(destination);
    }
};
class ModelB:public Airplane {
public:
    virtual void fly(const Airport&destination) {
        Airplane::fly(destination);
    }
};
class ModelC:public Airplane {
public:
    virtual void fly(const Airport&destination)
    ...
};
voidModelC::fly(const Airport& destination) {
    // 將C型飛機飛至指定目的地
}

這個實現和前幾乎一模一樣,只是用純虛函數Airplane::fly替換了獨立函數Airplane::defaultFly。

請記住:

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

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

3. impure virtual函數具體指定接口繼承和缺省實現繼承;

4.non-virutal函數具體指定接口繼承以及強制性實現繼承。

 

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

假設你正在寫一個視頻遊戲軟件,遊戲裏有各種各樣的人物,每個人物都有健康狀態,而且不同的人物可能以不同的方式計算他們的健康指數.該如何設計遊戲裏的人物,主要如何提供一個返回人物健康指數的接口。基於虛函數的方法是比較常用的:

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

替代方案一:藉由Non-Virtual Interface手法實現Template Method模式(NVI)

class GameCharacter{
       public:
              int healthValue() const {         //派生類不能重新定義它
                     ...                  //做一些事前工作
                     int retVal =doHealthValue();
                     ...                  //做一些事後工作
                     return retVal;
              }
       ...
       private:
              virtual int doHealthValue() const{       //派生類可以重新定義
                     ...                  //提供缺省算法
              }
};

NVI手法的一個優點可以確保在一個virtual函數被調用之前設定好適當的場景,並在調用結束之後清理場景。“事前工作”可以包括鎖定互斥器,製造運轉日誌記錄項,驗證class約束條件,驗證函數先決條件等等。“事後工作”可以包括互斥器解除鎖定,驗證函數的事後條件,再次驗證class約束條件等等。

替代方案二:藉由Function Pointers實現Strategy模式

classGameCharacter;       //前置聲明
//以下函數是計算體力值的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
       public:
              typedef int(*HealthCalcFunc)(const GameCharacter&);
              explicitGameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
              { }
              int healthValue() const{
                     return healthFunc(*this);
              }
              ...
       private:
              HealthCalcFunc healthFunc;
};

該方法優點:1、同一人物類型之不同實體可以有不同的健康計算函數,只需要在構造實例時,傳入不同的計算函數的指針;2、某已知人物之健康計算函數可在運動期變更,可以在GameCharacter裏提供一個成員函數setHealthCalc,用來替換當前的健康指數計算函數。缺點:該non-membernon-friend函數無法訪問non-public成員。

替代方案三:藉由tr1::function實現Strategy模式

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
       public:
              //HealthCalcFunc可以是任何“可調用物”,可被調用並接受任何兼容於GameCharacter之物,返回任何兼容於int的東西,詳下:
              typedef std::tr1::function<int(const GameCharacter&)> HealthCalcFunc;
              //這種定義表示HealthCalcFunc作爲一種類型,接受GameCharacter類型的引用,並返回整數值,其中支持隱式類型轉換
              explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
              {}
              int healthValue() const{
                     return healthFunc(*this);
              }
              ...
       private:
              HealthCalcFunc healthFunc;
};

不再使用函數指針,而是改用一個類型爲tr1::function的對象。可以是函數指針,函數對象,或成員函數指針,只要其簽名式兼容於需求端。

替代方案四:古典的Strategy模式

classGameCharacter{
       public:
              explicitGameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc) :pHealthCalc(phcf)
              {}
              int healthValue() const{
                     returnpHealthCalc->calc(*this);
              }
              ...
       private:
              HealthCalcFunc* pHealthCalc;
};

請記住:

1. virtual函數的替代方案NVI手法及Strategy設計模式的多種形式。NVI手法自身是一個特殊形式的Template Method設計模式;

2. 將機能從成員函數轉移到class外部函數,帶來的一個缺點是,非成員函數無法訪問classnon-public成員。

3.tr1::function對象的行爲就像一般函數指針。這樣的對象可接納“與目標籤名兼容”的所有可調用物。

 

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

假設class B爲基類,class D爲public繼承自B的子類:

 class B{
   public:
       void func(){...}       
   };
class D:publicB{...}
D x;
B* pB = &x;
pB->func();          //調用B::func
D* pD = &x;
pD->func();          //調用D::func

非虛函數都是靜態綁定的,通過pB調用的non-virtual函數永遠是B定義的版本。回顧前一個款,我們可以知道,如果是public繼承的話:

1) 適用於BaseClass的行爲一定適用於DerivedClass,因爲每一個DerivedClass對象都是一BaseClass對象;

2) 如果BaseClass裏面有非虛函數,那麼DerivedClass一定是既繼承了接口,也繼承了實現;

3) 子類裏面的同名函數會掩蓋父類的同名函數,這是由於搜索法則導致的。

如果繼承類重新定義非虛函數,就會違反以上法則。

請記住:

任何情況下都不該重新定義一個繼承而來的non-virtual函數。

 

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

爲了簡化討論,我們知道繼承的只能是兩種函數:virtual和non-virtual。前條款明確說明重新定義繼承而來的non-virtual函數永遠是錯誤的,所以我們可以將討論安全的侷限於“繼承一個帶缺省參數值的virtual函數”。在這種情況下,本條款成立的理由就非常直接而明確了:virtual函數是動態綁定的,而缺省參數值卻是靜態綁定的。

class Shape{
    public:
             enum Color{RED,GREEN,BLUE};
             virtual void draw(Color color =RED)const = 0;
             ...
    };
    class Circle:public Shape{
    public:
             //改變缺省參數值
             virtual void draw(Color color =GREEN)const{ ... }
    };
    class Rectangle:public Shape{
    public:
            //當客戶以對象調用此函數時,需要明確的指定參數值
           //靜態綁定下不繼承基類的缺省值,若以指針或引用調用則不需要指定缺省值,因爲動態綁定
           //繼承基類的參數缺省值
           virtual void draw(Color color)const{... }
};
   看一下下面幾個指針:
   Shape* ps;
   Shape* pc = new Circle;
   Shape* pr = new Rectangle;
pc->draw(); //注意調用的是Circle::draw(RED)

首先根據其調用語句用指針這一事實,我們就知道了其調用的版本應該是該指針的動態類型的函數版本,即Circle::draw,這個問題不大。下面我們來看它的傳值參數,前面我們提到缺省參數值是靜態綁定的,而pc的靜態類型是Shape,所以該參數的傳入值是Shape的該函數版本的缺省值。爲什麼C++堅持以這種乖張的方式來運作呢?答案在於運行期效率,如果缺省值也是動態綁定的,那麼編譯期就必須要有辦法在運行期爲virtual函數決定適當的參數缺省值。如果這樣做的話,就要比目前實現的"在編譯期決定"的機制更慢而且更復雜,考慮到執行速度和實現上的簡易性,C++放棄了這樣的做法。爲了遵循本款約定卻同時提供缺省參數值給你的基類和父類,可以這樣:

class Shape{
     public:
              enum Color{RED,GREEN,BLUE};
              virtual void draw(Color color =RED)const = 0;
              ...
     };
     class Circle:public Shape{
     public:
              virtual void draw(Color color =RED)const {...}
};

明顯的是代碼重複!何況你要是想改變缺省值的話,必須要同時改變基類和子類函數的缺省值。採用NVI(Non-VirtualInterface),可以這樣:

class Shape{
     public:
              enum Color{RED,GREEN,BLUE};
              void draw(Color color = RED)const{
                     ...
                     doDraw(color);
                     ...
              }
              ...
     private:
             virtual void doDraw(Color color)const = 0; 
     };
     class Circle:public Shape{
                   ...
     private:
              virtual void doDraw(Color color){... }
     };

由於draw是non-virtual而non-virtual絕對不會被重新改寫(條款36),所以color的缺省值總是爲RED。

請記住:

1. 絕對不要重新定義一個繼承而來的缺省參數值,因爲缺省參數值都是靜態綁定,而virtual函數-你唯一應該覆寫的東西-卻是動態綁定。

 

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

看似很深奧的一句話,其實在說一件很簡單的事情。public繼承是“is-a“的關係,而複合有”has-a“或”根據某物實現出(is-implemented-in-terms-of)“的意思——當複合發生在應用域內的對象之間,表現出has-a關係;當它發生於實現域內則是表示“根據某物實現出”的關係。

class Address{...};
class PhoneNumber{...};
class Person{
      ...
private:
      std::string name_;
      Address address_;
      PhoneNumber voiceNumber_;
      PhoneNumber faxNumber_;
};

上述中Person class示範的就是has-a關係。很少有人對has-a和is-a感到困惑,但是比較麻煩的是is-a(是一種)和is-implemented-in-terms-of(根據某物實現出)。例如set的實現。可以採用list實現,繼承是不可取的,這兩個類之間並不是is-a的關係:

template<typename T>
class Set
{
public:
       bool member(const T& item)const
       {
              return find(rep.begin(),rep.end(),item) != rep.end();
       }
       void insert(const T& item)
       {
              if(!member(item))
                     rep.push_back(item);
       }
       void remove(const T& item)
       {
              typename list<T>::iteratorit = find(rep.begin(),rep.end(),item);
              if(it != rep.end())
                     rep.erase(it);
       }
       size_t size()const
       {
              return rep.size();
       }
private:
       list<T> rep;
};

總之,複合與public繼承完全不同,它意味着has-a或者通過某物實現。

請記住:

1.   複合的意義和public繼承完全不同。

2.   在應用域,複合意味着has-a(有一個)。在實現域,複合意味着is-implemented-in-terms-of(根據某物實現出)

 

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

先看看下面的代碼:

class Person{...};
class Student:privatePerson{...};
void eat(const Person&p);
Person p;
Student s;
eat(p);//正確
eat(s);//錯誤

如果classes之間的繼承關係是private,編譯器不會自動將一個deirvedclass對象轉換爲一個base class對象。private繼承意味着implemented-in-terms-of(根據某物實現出)。如果你讓classD以private繼承classB,你的用意是爲了採用classB內已經備妥的某些特性,不是因爲B對象和D對象存在有任何觀念上的關係。private繼承純粹是一種實現技術,意味着只有實現部分被繼承,接口部分應該略去。如果D以private形式繼承B,意思是D對象根據B對象實現而得,private繼承在軟件設計層面上沒有意義,其意義只及於軟件實現層面

Private和has-a有點類似,它的意義與複合(組合)的意義相同。那麼問題來了,在二者之間我們如何做選擇呢?答案就是儘可能使用複合,必要時才使用private繼承,除了以下兩種情況:

1、private繼承通常比複合的級別低。當基類和派生類之間沒有任何關係,但derived class需要訪問protectedbase class的成員,或需要重新定義繼承而來的virtual函數時,就要用private繼承。例如書中例子提到的widget需要使用Timer中的虛函數onTick()。

2、private繼承可以造成empty class最優化。(其實public繼承也可以優化空基類,不知道爲何非得闡述成private繼承,另外繼承空類有何意義呢?

優化空基類(EBO: empty base optimization):

//定義一個空基類
class Empty{};
class HoldsAnInt
{
private:
       int x;
       Empty e;//複合
};
//sizeof(HoldsAnInt)= 8;
classHoldsAnInt:private Empty
{
private:
       int x;
};
//sizeof(HoldsAnInt)= 4;

請記住:

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

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

 

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

慎用多重繼承,因爲容易造成歧義。例如:

class BorrowableItem {
public:
    void checkOut();
};
 
class ElectronicGadet {
private:
    bool checkOut() const;
};
 
class MP3Player
: public BorrowableItem  public ElectronicGadet{...}
MP3Player mp;
mp.checkOut();//調用哪個checkOut()

這與C++用來解析重載函數調用的規則相符:在看到是否有個函數可取用之前,C++首先確認這個函數對此調用之言是最佳匹配。找到最佳匹配函數後才檢驗其可取用性。本例中連最佳匹配都出現歧義,爲了解決這個歧義,可以:

mp.BorrowableItem::checkOut();

你當然也可以明確調用mp.ElectronicGadget::checkOut(),但然後你會獲得一個“嘗試調用private成員函數”的錯誤。

“鑽石型多重繼承”,如果base classes在繼承體系中又有更高級的base classes,非virtual繼承會造成多份baseclass成員變量。這時要用virtual繼承,你必須令所有直接繼承自它的classes採用“virtual繼承”。以IOFile爲例:

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

這樣會導致會導致類IOFile的對象中有兩份File的副本。解決方法就是採用虛繼承:

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

在effective c++中提到當涉及“public繼承某個interfaceclass”(接口)和“private繼承某個協助實現的class”(實現)的兩相結合時候使用多重繼承也是有必要的。

請記住:

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

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

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

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