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::binf(&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;
};

. 這樣做之後,還有什麼不同的計算方式,只要再爲這個集成體系聲明一個派生類即可。

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