effective C++筆記--繼承與面向物件設計(一)
文章目錄
確定你的public繼承塑模is-a關係
. public繼承在父類和子類之間的關係應該是:子類的物件也是一個父類的物件,但是父類的物件不是子類的物件,通俗點講就是父類能派上用處的地方,子類也能派上用處,但是反過來就不是了。
假設有一個類表示鳥,它能派生出表示企鵝的類,鳥可以飛,所以在鳥這個類中有一個方法是fly,但是企鵝應該是不會飛的,這就對上述敘述帶來疑惑,所以在設計繼承體系的時候應該有更多的考慮,比如將鳥分為會飛的和不會飛的兩個類來表示。
is-a並非唯一存在與class之間的關係,另外的常見的關係有has-a和is-implemented-in-terms-of(根據某物實現出)。
避免遮掩繼承而來的名字
. 如同區域性變數在區域性作用域中使用的時候會覆蓋掉外部變數,這點在繼承體系中也很相似的:子類的同名變數將會覆蓋父類中所有同名變數。因為子類的作用域就像是嵌在父類的作用域中的。
class Base{ private: int x; public: virtual void f1() = 0; virtual void f1(int); void f2(); void f2(int); }; class Derived:public Base{ public: virtual void f1(); void f2(); void f3(); }; Derived d; int x; ... d.f1(); //正確,呼叫Derived::f1() d.f1(x); //錯誤,因為Derived::f1遮掩了Base::f1 d.f2(); //正確,呼叫Derived::f2() d.f2(x); //錯誤,因為Derived::f2遮掩了Base::f2
. 以作用域為基礎的“名稱掩蓋規則”並沒有被改變,因此Base類中名為f1和f2的函式都被Derived類中的f1和f2遮掩掉了,就像沒有繼承這兩個函式一樣。
如果正在使用public繼承但是又不想繼承那些過載的函式,可以使用using宣告式達到目的:
class Base{ private: int x; public: virtual void f1() = 0; virtual void f1(int); void f2(); void f2(int); }; class Derived:public Base{ public: using Base::f1; using Base::f2; virtual void f1(); void f2(); void f3(); }; Derived d; int x; ... d.f1(); //正確,呼叫Derived::f1() d.f1(x); //正確,呼叫Base.f1(int) d.f2(); //正確,呼叫Derived::f2() d.f2(x); //正確,呼叫Base.f2(int)
. 有時候可能並不像繼承所有的函式,當然這在public繼承中不可能發生,因為public繼承是一種is-a的關係。然而在private繼承下,假設Derived唯一想繼承的是一個無引數的版本,using宣告在這裡將用不上,因為using宣告會將所有同名函式在Derived class中都可見。可以通過一個簡單的轉交函式來實現:
class Base{
private:
int x;
public:
virtual void f1() = 0;
virtual void f1(int);
...
};
class Derived:private Base{
public:
virtual void f1(){
Base::f1();
}
...
};
Derived d;
int x;
...
d.f1(); //正確,呼叫Derived::f1()
d.f1(x); //錯誤,Base::f1(int)被遮蓋
區分介面繼承和實現繼承
. 在基類中宣告函式的方式有三種,可以是純虛擬函式,可以是虛擬函式或者是一個非虛的函式,比如:
class Shape{
public:
virtual void draw() const = 0; //隱喻畫出物件
virtaul void error(const std::string& msg); //報告錯誤
int objectID() const; //返回當前物件的識別碼
...
};
class Rectangle : public Shape{...};
class Ellipse : public Shape{...};
. Shape是一個抽象類,所以不能為它建立實體,只能建立它的派生類的實體,但是它還是強烈的影響了所有以public形式繼承了它的派生類,因為:
成員函式的介面總是被繼承。 public繼承意味著is-a關係,所以發生對基類為真的事情一定也對派生類為真。
宣告一個純虛擬函式的目的是為了讓派生類只繼承函式介面。 純虛擬函式有兩個最突出的特性:它們必須被任何繼承了它們的具象類重新宣告;它們在抽象class中通常沒有定義。如上面的程式碼,畫出抽象的形狀是不合理的,但是可以畫出具體的如矩形或是圓形,因此Shape::draw的宣告式就像在對派生類的設計者說:你必須提供一個draw函式,但我不干涉你怎麼實現它。
宣告一個非純的虛擬函式的目的是讓派生類繼承該函式介面和預設的實現。 派生類會繼承基類的非純的虛擬函式的函式介面,但是通常它會提供一份實現程式碼,派生類可能會覆寫它。比如上面的程式碼,error函式介面表示每個class都應該支援遇上錯誤可呼叫的函式,但每個class可自由處理錯誤,如果某個class不需要對錯誤做特殊的處理,它可以退回到Shape class提供的預設的錯誤行為,因此Shape::error的宣告式就像在對派生類的設計者說:你必須提供一個error函式,但是如果你不想自己寫一個,可以使用Shape class提供的版本。
宣告一個非虛擬函式意味著它不打算在派生類中有不同的行為。 實際上一個非虛成員函式所表現的不變形凌駕與特異性,因為他表示不論派生類變得多麼特異化,它的行為都不能改變。如以上的程式碼,Shape::objectID表示:每個Shape派生類物件都有一個用來產生物件識別碼的函式,此方法應該保持一致,任何派生類都不應該嘗試改變他的行為。
考慮virtual函式以外的其他選擇
. 假設設計的一個遊戲中,編寫了一個遊戲人物類,其中有名為healthValue的成員函式來表示人物的健康值,不同的人物應該有不同的方式愛計算健康值,因此將這個成員函式宣告為virtual的似乎是再明白不過的方法了:
class GameCharacter{
public
virtual int healthValue() const; //不是純虛擬函式表示有預設的計算方法
...
};
為了跳出面向物件設計時的常規思路,可以考慮一些其他的方法:
由Non-Virtual Interface(NVI)手法實現Template Method模式(模板方法模式):
. 有一個有趣的思想流派主張:virtual函式應該總是private的。這個流派的擁護者建議,較好的設計是保留healthValuee為public成員函式,並且是非虛擬函式,通過呼叫一個private的虛擬函式來完成實際工作,比如:
class GameCharacter{
public
int healthValue() const{
...
int ret = doHealthValue();
...
return ret;
}
...
private:
virtual int doHealthValue() const;
...
};
. 這一基本設計就是通過共有的非虛成員函式間接呼叫private 虛擬函式。這麼做有一個優點,即可以在共有函式中,呼叫虛擬函式之前和之後都做一些相關工作,這意味著這個非虛擬函式可以確保一個virtual函式在呼叫前設定好適當的場景,並在呼叫後清理場景。事前工作包括鎖定互斥器、驗證函式先決條件等,事後工作包括解鎖互斥器、驗證函式事後條件等。
但是有時候要求virtual函式必須是public的,這樣就沒辦法使用NVI手法了。
由Function Pointer實現Strategy模式(策略模式):
. 有一種思路可以讓讓每個人物的建構函式中接受一個指標,指向一個計算健康值的函式,可以通過呼叫這個函式完成實際的計算:
class GameCharacter; //前置宣告
//計算健康值的預設函式
int defaultHealthCalc(const GameCharacter& );
class GameCharacter{
public:
typedef int (HealthCalcFunc)(const GameCharacter&); //定義函式指標
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
:healthFunc(hcf){}
int healthValue() const{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
};
. 通過在構造的時候傳入不同的計算健康值的函式也可以給不同的人物設定不同的計算健康值的方法,並且這個函式在繼承體系之外,不會訪問到類的非公有部分,如果人物的健康值能只通過公有資訊就能計算得到,那就沒什麼問題,但是如果需要非公有資訊進行精確計算的時候,就會有問題了,解決辦法是弱化類的封裝性,這代價是否值得需要好好考慮。
由tr1::function完成strategy模式:
. tr1::function是一個類模板,它與函式指標很像,但是因為過載了(),所以看上去比函式指標更易使用,而且函式指標智慧繫結外部的函式,而tr1::function可以繫結任何型別的函式,其形式如:function<int (const GameCharacter&)>,尖括號的前一個引數表示返回值,後一個用括號括起來的表示引數型別,且這兩個型別都具有相容性,即可呼叫物的引數可以被隱式轉換成const GameCharacter& ,返回值可被隱式轉換為int。之前的程式碼可以改為:
class GameCharacter; //前置宣告
//計算健康值的預設函式
int defaultHealthCalc(const GameCharacter& );
class GameCharacter{
public:
typedef std::tr1::function<int (const GameCharacter&) > HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
:healthFunc(hcf){}
int healthValue() const{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
};
. 程式碼的改變很小,但是為程式帶來了巨大的彈性,tr1::function物件可以指向一個函式、一個函式物件或是一個成員函式:
short calcHealth(const GameCharacter&); //健康計算函式
struct HealthCalculator{ //函式物件
int operator()(const GameCharacter&) const{
...
}
};
class GameLevel{
public:
float health(const GameCharacter&) const; //成員函式
...
};
class GoodGuy : public GameCharacter{
...
};
class BadGuy : public GameCharacter{
...
};
GoodGuy gg1(calcHealth); //人物1,使用函式計算健康值
BadGuy bg1(HealthCalculator()); //人物2,使用函式物件計算健康值
GameLevel gl;
GoodGuy gg2(
std::tr1::bind(&GameLevel::health,
gl,
_1)); //人物3,使用成員函式計算健康值
. 古典strategy模式: 還可以將計算健康值的這一做法抽象為一個類,將不同的計算方法都作為這個做法的派生類,然後在人物類中包含一個指向這個計算健康值的基類的指標作為成員屬性:
class GameCharacter; //前置宣告
class HealthCalcFunc{
public:
...
virtual int calc(const GameCharacter& gc) const{
...
}
...
};
HealthCalcFunc defaultCalc;
class GameCharacter{
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
:pHealthClac(phcf){}
int healthValue() const{
return pHealthClac->clac(*this);
}
...
private:
HealthCalcFunc* pHealthCalc;
};
. 這樣做之後,還有什麼不同的計算方式,只要再為這個整合體系宣告一個派生類即可。