C++進階_Effective_C++第三版(六) 繼承與面向對象設計 Inheritance and Object-Oriented Design

繼承與面向對象設計 Inheritance and Object-Oriented Design

面向對象編程已經風靡編程界,關於繼承、派生、virtual函數等等需要深入瞭解。

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

Make sure public inheritance models “is-a”.
令派生類class D以public形式繼承基類class B,這便是告訴C++編譯器每一個類型爲D的對象同時也是一個類型爲B的對象,反之不成立。B比D表現出更一般化的概念,而D比B表現出更特殊化的概念。如下例子:

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

根據生活經驗,每個學生都是人,但並非每個人都是學生。這便恰好對應public繼承體系。於是任何函數如果期望獲得一個類型爲Person的實參,都也可以接受一個Student對象:

void eat(const Person& p);	//任何人都會喫
void study(const Student& p);	//只有學生纔到學校學習
Person p;		//p是人
Student s;	//s是學生
eat(p);		//沒問題,p是人
eat(s);		//沒問題,s是學生,而學生也是is-a人
study(s);		//沒問題,s是學生
study(p);		//錯誤!p不是學生

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

class 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)	//用以增加r的面積
{
	int oldHeight = r.height();
	r.setWidth(r.width() + 10);		//爲r的寬度加10
	assert(r.height() == oldHeight);  //判斷r的高度是否未曾改變
}
class Square: public Rectangle{};
Square s;
assert(s.width() == s.height());	//一定爲真
makeBigger(s);				//由於繼承s是一個矩形,對其增加面積
assert(s.width() == s.height());	//對所有正方形應該仍然爲真,但是這裏卻不爲真了。
  • “public繼承”意味着is-a。適用於基類身上的每一件事情一定也適用於派生類身上,因爲每一個派生類對象也都是一個基類對象。

33、避免遮掩繼承而來的名稱

Avoid hiding inherited names.
當位於一個派生類成員函數內指涉基類內的某物(也許是成員函數、tyepdef、成員變量)時,編譯器可以找出我們所指涉的東西,因爲派生類繼承了聲明於基類內的所有東西。實際運作方式是,派生類作用域被嵌套在基類作用域內:

class Base{
private:
	int x;
public:
	virtural void mf1() = 0;
	virtural void mf2();
	void mf3();
};
class Derived:public Base{
public:
	virtual void mf1();
	void mf4();};
//mf4如下實現:
void Derived::mf4()
{mf2();
}

當編譯器看到mf4調用mf2.首先查找local作用域(即mf4覆蓋的作用域),沒找到任何東西名爲mf2,再查找外圍作用域(即Derived類覆蓋的作用域)還是沒找到,於是再往外圍移動(繼承基類Base類),找到了一個名爲mf2的東西,停止查找。如果Base內還沒有的話,查找動作繼續下去,首先找內涵Base的那個namespace(s)的作用域,最後往global作用域去找。
如下例子,基類重載了mf1和mf3,派生類添加一個新的mf3.

class Base{
private:
	int x;
public:
	virtural void mf1() = 0;
virtural void mf1(int);
	virtural void mf2();
	void mf3();
void mf3(double);
};
class Derived:public Base{
public:
	virtual void mf1();
	void mf3();
	void mf4();};

以作用域爲基礎的“名稱遮掩規則”,基類內所有名爲mf1和mf3的函數都被派生類Derived內的mf1和mf3函數遮掩掉了。從名稱查找觀點來看,Base::mf1和Base::mf3不再被Derived繼承。

Derived d;
int x;
d.mf1();	//沒問題,調用Derived::mf1
d.mf1(x);	//錯誤,因爲Derived::mf1遮掩了Base::mf1
d.mf2();	//沒問題,調用Base::mf2
d.mf3();	//沒問題,調用Derived::mf3
d.mf3(x);	//錯誤,因爲Derived::mf3遮掩了Base::mf3

這些機制爲了防止在程序庫或框架內建立新的派生類時附帶地從疏遠的基類繼承重載函數。但是通常會想繼承重載函數。實際如果正在使用public繼承而又不繼承那些重載函數,就違反基類和派生類之間的is-a關係。爲了解決此問題可以使用using聲明式:

class Base{
private:
	int x;
public:
	virtural void mf1() = 0;
virtural void mf1(int);
	virtural void mf2();
	void mf3();
void mf3(double);
};
class Derived:public Base{
public:
	using Base::mf1;	//基類內名稱爲mf1的所有東西在派生類作用域內都可見。
using Base::mf3;	//基類內名稱爲mf3的所有東西在派生類作用域內都可見。
	virtual void mf1();
	void mf3();
	void mf4();};
//使用
Derived d;
int x;
d.mf1();	//沒問題,調用Derived::mf1
d.mf1(x);	//沒問題,調用Base::mf1
d.mf2();	//沒問題,調用Base::mf2
d.mf3();	//沒問題,調用Derived::mf3
d.mf3(x);	//沒問題,調用Base::mf3

有時候不想繼承基類的所有函數,但又想調用某些基類函數,可以使用一個簡單的轉交函數實現:

class Base{
private:
	int x;
public:
	virtural void mf1() = 0;
	virtural void mf1(int);
};
class Derived:private Base{
public:
	virtural void mf1()		//轉交函數
	{	Base::mf1();	 }	//暗自成爲inline
};

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

  • 派生類內的名稱會遮掩基類內的名稱。在public繼承下從來沒有人希望如此。
  • 爲了讓被遮掩的名稱再見天日,可使用using聲明式或轉交函數。

34、區分接口繼承和實現繼承

Differentiate between inheritance of interface and inheritance of implementation.
表面上簡單的public繼承,經過更嚴密的檢查,其實由兩部分組成,函數接口繼承和函數實現繼承,這兩種繼承的差別很像函數聲明與函數定義之間的差異。
做爲class的設計者,有時候希望派生類只繼承成員函數的接口,有時候希望派生類同時繼承函數的接口和實現,但又希望能夠覆寫它們所繼承的實現,有時候希望派生類同時繼承函數的接口和實現,並且不允許覆寫任何東西。
如下例子:

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

Shape是個抽象class:它的pure virtual函數draw使它成爲一個抽象class。所以客戶不能夠創建Shape class的實體,只能創建其派生類的實體。儘管如此,Shape哈斯強烈影響了所有以public形式繼承它的派生類。 成員函數的接口總會被繼承。public繼承意味着is-a,所以對基類爲真的任何事情一定對其派生類爲真。因此如果某個函數可施行於某class身上,一定也可施行於其派生類身上。pure virtual函數有兩個突出的特性:必須被任何“繼承了它們”的具體class重新聲明,而且它們在抽象class中通常沒有定義。
聲明一個pure virtual函數的目的是爲了讓派生類只繼承函數接口。對於Shape::draw函數很合理,因爲所有Shape對象都應該是可繪出的,Shape::draw得聲明式是對具體派生類設計的一種約束,必須提供一個draw函數,但不干涉怎麼實現它。比較意外的是,可以爲pure virtual函數提供定義,但調用它的唯一途徑是調用時明確指出其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

簡樸的impure virtual函數和pure virtual函數有點不同。派生類繼承其函數接口,但impure virtual函數會提供一份實現代碼,派生類可能覆寫它。聲明簡樸的(非純)impure virtual函數的目的,是讓派生類繼承該函數的接口和缺省實現:

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

其接口表示,每個class都必須支持一個“當遇上錯誤時可調用”的函數,但每個class可自由處理錯誤。如果某個class不想針對錯誤做出任何特殊行爲,它可以退回到Shape class提供的缺省錯誤處理行爲。也就是說Shape::error的聲明式告訴派生類的設計者,必須支持一個error函數,如果不想自己寫一個,可以使用Shape類提供的缺省版本。
如果成員函數是個non-virtual函數,意味是它不打算在派生類中有不同的行爲。實際上一個non-virtual成員函數所表現的不變性凌駕其特異性,因爲它表示不論派生類多麼特異性,它的行爲都不可能改變。聲明non-virtual函數的目的是爲了令派生類繼承函數的接口及一份強制性實現。可以把Shape::objectID的聲明看作是:每個Shape對象都有一個用來產生對象識別碼的函數:此識別碼總是採用相同計算方法,該方法由Shape::objectID的定義式決定,任何派生類都不應該嘗試改變其行爲。由於non-virtual函數代表的意義是不變性凌駕特異性,所以絕不應該在派生類中被重新定義。

  • 接口繼承和實現繼承不同。在public繼承下,派生類總是繼承基類的接口。
  • pure virtual函數只具體指點接口繼承。
  • 簡樸的(非純)impure virtual函數具體指定接口繼承及缺省實現繼承。
  • non-virtual函數具體指定接口繼承以及強制性實現繼承。

35、考慮virtual函數以外的其他選擇

Consider alternatives to virtual functions.
假設寫一個視頻遊戲軟件,打算爲遊戲內的人物設計一個繼承體系。遊戲屬於暴力砍殺類型,劇中人物被傷害或因其他因素會降低健康狀態,提供一個成員函數healthValue,返回一個整數,表示人物的健康程度。由於不同的人物可能以不同的方式計算他們的健康指數,所以此函數聲明爲virtual,未將其聲明爲pure virtual,表示會有缺省算法:

class GameCharacter{
public:
	virtual int healthValue() const;	//返回人物的健康指數,派生類可重新定義};

但是有一個流派,主張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設計模式的一個獨特表現形式。把這個non-virtual函數稱爲virtual函數的外覆器(wrapper)。
NVI手法的一個優點就是保證在virtual函數進行真正工作之前和之後被調用,意味外覆器確保得以在一個virtual函數被調用之前設定好適當場景,並在調用結束之後清理場景。“事前工作”可以包括鎖定互斥鎖、製造運轉日誌記錄項、驗證class約束條件、驗證函數先決條件等等。“事後工作”可以包括互斥器解除鎖定、驗證函數的事後條件、再次驗證class約束條件等等。如果讓使用者直接調用virtual函數,就沒有任何好辦法可以做這些事。
在NVI手法下其實沒必要讓virtual函數一定得是private。某些class繼承體系要求派生類在virtual函數實現內必須調用其基類的對應兄弟,virtual函數必須是protected,不能是private。有時候virtual甚至一定得是public(例如具備多態性質的基類的析構函數)。
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設計模式的簡單應用。提供和一些彈性:
同一個人物類型的不同實體可以有不同的健康計算函數:

Class EvilBadGuy:public GameCharacter{
public:
	explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc):GameCharacter(hcf)
	{}
};
int loseHealthQuickly(const GameCharacter& );	//計算函數1
int loseHealthSlowly(const GameCharacter& );	//計算函數2
EvilBadGuy ebg1(loseHealthQuickly);	//相同類型的人物搭配不同的健康計算方式
EvilBadGuy ebg2(loseHealthSlowly);

某已知人物的健康指數計算函數可在運行期變更。例如GameCharacter可提供一個成員函數setHealthCalculator,用來替換當前的健康指數計算函數。也就是說健康指數計算函數不再是GameCharacter繼承體系內的成員函數,這些計算函數並未特別訪問即將被計算健康指數的那個對象的內部成員。例如defaultHealthCalc並未訪問EvilBadGuy的non-public成分。
但是如果需要non-public信息進行精確計算,此時就需要弱化class的封裝,實現以non-mmber函數訪問class的non-public成分。例如可聲明那個non-mmber函數爲friends,huo爲其實現某一部分提供public訪問函數。
對於如果對於Strategy設計模式得實現不再使用函數指針,而是改用一個類型爲tr1::function的對象,這些約束就都不見了。這樣的對象可持有任何可調用物(函數指針、函數對象、成員函數指針),只要其簽名式兼容於需求端,改用tr1::function實現:

class GameCharacter; 			//前置聲明
int defaultHealthCalc(const GameCharacter& gc); 	//計算健康指數的缺省算法
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具現體的目標籤名式是“接受一個reference指向const GameCharacter並返回int”。所謂兼容意思這個可調用物的參數可被隱式轉換爲const GameCharacter&,而其返回類型可被隱式轉換爲int。
和前一個設計比較,這個設計幾乎相同。唯一不同是如今GameCharacter持有一個tr1::function對象,相當於一個指向函數的泛化指針:

short calcHealth(const GameCharacter&);	//健康計算函數 返回non-int
struct HealthCalculator{		//爲計算健康而設計的函數對象
	int operator() (const GameCharacter&) const{}
};
class GameLevel{
public:
	float health(const GameCharacter&) const;	//成員函數,返回non-int};
Class EvilBadGuy:public GameCharacter{		//一種人物類型
public:
	explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc):GameCharacter(hcf)
	{}
};
Class EyeCandyCharacter:public GameCharacter{		//另一種人物類型
public:
	explicit EyeCandyCharacter (HealthCalcFunc hcf = defaultHealthCalc):GameCharacter(hcf)
	{}
};
EvilBadGuy ebg1(calcHealth);			//使用函數
EyeCandyCharacter ecc1(HealthCalculator())		//使用函數對象
GameLevel currentLevel;
…
EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health, currentLevel,_1));	//使用成員函數

因爲GameLevel類ude成員函數health宣稱自己接受一個參數(GameCharacter的引用),但實際接受兩個參數,因爲它也獲得一個隱式參數GameLevel,也就是this所指的那個,但是GameCharacter的健康計算函數只接受一個參數,所以使用tr1::bind將其轉化使得GameLevel::health可被使用。

  • virtual函數的替代方案包括NVI手法及Strategy設計模式的多種形式。NVI手法自身是一個特殊形式的Template Method設計模式。
  • 將機能從成員函數移到class外部函數,帶來的一個缺點是,非成員函數無法訪問class的non-public成員。
  • tr1::function對象的行爲就像一般函數指針。這樣的對象可接納“與給定的目標籤名式兼容”的所有可調用物。

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

Never redefine an inherited non-virtual function.
如有繼承體系class D由class B以public形式派生而來,class B定義一個public成員函數mf:

class B {
public:
	void mf();};
class D: public B {};
D x;
//調用1
B* pB = &x;
pB->mf();
//調用2
D* pD = &x;
pD->mf();

上述兩個調用都是調用B:mf(),雖然指針不同,但是指向的是同一個對象。但是如果D定義了自己的mf版本:

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

導致這個結果是因爲non-virtual函數如B::mf和D:mf都是靜態綁定。意思是由於pB被聲明爲一個pointer-to-B,通過pB調用的non-virtual函數永遠是B所定義的版本,即使pB指向一個類型爲“B派生的類”的對象。virtual函數是動態綁定的,所以不受這個問題影響,如果mf是個virtual函數,不論是通過pB或pD調用mf,都會導致調用D::mf,因爲pB和pD真正指向的都是一個類型爲D的對象。

  • 絕對不要重新定義繼承而來的non-virtual函數。

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

Never redefine a function’s inherited default parameter value.
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;};
class Circle: public Shape{
public:
virtual void draw(ShapeColor color) const;	
//	客戶以對象調用此函數,一定要指定參數值。
//	因爲靜態綁定下這個函數不從其base繼承缺省參數值。
//	但若以指針調用此函數,可以不指定參數值,
//	因爲動態綁定下這個函數會從其base繼承缺省參數值。};
// 以下調用
Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;

ps、pc和pr都被聲明爲pointer-to-Shape類型,所以它們都以它爲靜態類型。不論它們真正指向什麼,它們的靜態類型都是Shape*。對象的所謂動態類型則是指“目前所指對象的類型”。也就是說上代碼中pc的動態類型是Circle*,pr的動態類型是Rectangle*。ps沒有動態類型,因爲它尚未指向任何對象。動態類型可以在程序執行過程中改變:

ps = pc;	//ps的動態類型是Circle*
ps = pr;	//ps的動態類型是Rectangle*
pc->drwa(Shape::Red);	//調用Circle::draw(Shape::Red)
pc->draw(Shape::Red);	//調用Rectangle::draw(Shape::Red)

因爲virtual函數是動態綁定,而缺省參數值卻是靜態綁定,所以調用一個定義於派生類內的virtual函數的同時,卻使用基類爲它所指定的缺省參數值:
pr->draw(); //調用Rectangle::draw(Shape::Red)

  • 絕對不要重新定義一個繼承而來的缺省參數值,因爲缺省參數值都是靜態綁定,而virtual函數卻是動態綁定的。

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

Model “has-a” or “is-implemented-in-terms-of” through composition.
複合(composition)是類型之間的一種關係,當某種類型的對象內含其他類型的對象便是這種關係。例如:

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

例子中Person對象由string、address、PhoneNumber構成。複合(composition)有很多同義詞:layering(分層),containment(內含),aggregation(聚合)和embedding(內嵌)。
複合實際意味着“has-a”(有一個)或is-implemented-in-terms-of(根據某物實現出)。程序中的對象其實相當於所塑造的世界中的某些事物,例如人,汽車,一張張視頻畫面等。這樣的對象屬於應用域部分。其他對象則純粹是實現細節上的人工製品,像緩衝區、互斥鎖、查找樹等等。這些對象相當於軟件的實現域。當複合發生於應用域內的對象之間,表現出has-a的關係,當它發生於實現域則是表現is-implemented-in-terms-of的關係。
比較麻煩的是區分is-a和is-implemented-in-terms-of這兩種關係,假設需要一個template,希望製造出一組classes用來表現由不重複對象組成的sets。此時可以使用標準庫的list template,讓Set template繼承於std::list。也就是Set繼承list。聲明如下:
template //錯誤的用法,將list應用於Set
class Set:public std::list { … };
因爲如果按此實現,Set爲派生類,std::list爲基類,根據public繼承體系定義,對基類爲真的事情對派生類也爲真。list可以內含重複元素,但是Set不滿足,此設計是錯誤的。由於這兩個類不是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的成員函數可以大量依賴list及標準程序庫其他部分提供的機能來完成,如下:

template<class T>
bool Set<T>::member(const T& item)const
{
	return std::find(rep.begin(), rep.end(), item) != rep.end();
}
template<class T>
void Set<T>::insert(const T& item)
{
	if(!member(item))
		rep.push_bak(item);
}
template<class 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<class T>
std::size_t Set<T>::size() const
{
	return rep.size();
}
  • 複合(composition)的意義和public繼承完全不同。
  • 在應用域,複合意味has-a(有一個)。在實現域,複合意味is-implemented-in-terms-of(根據某物實現出)。

39、明智而審慎地使用private繼承

Use private inheritance judiciously.
如果classes之間的繼承關係是private,編譯器不會自動將一個派生類對象轉換爲一個基類對象。由private繼承基類而來的所有成員,在派生類中都會變成private屬性,不管在基類中是protected或public屬性。Private繼承意味着implemented-in-terms-of(根據某物實現出)。如果你讓class D以private形式繼承class B,用意就是爲了採用class B內已經備妥的某些特性,不是因爲B對象和D對象存在有任何觀念上的關係。private繼承純粹只是一種實現技術。private繼承意味只有實現部分被繼承,接口部分應略去。如果D以private形式繼承B,意思是D對象根據B對象實現而得,再沒有其他含義了。private繼承再軟件設計層面上沒有意義,其意義只及於軟件實現層面。Private繼承和複合功能很相似,所以儘可能的使用複合,必要時才使用private繼承。必要時主要指當protected成員和或virtual函數牽扯進來的時候,或者當空間方面的利害關係足以需要private繼承時。
例如有類Widgets,需要記錄每個成員函數的被調用次數。運行期間週期性的查看統計信息,此時爲了複用代碼,我們發現類Timer:

class Timer{
public:
	explicit Timer(int tickFrequency);
	virtual void onTick() const;	//定時器每超時一次,自動調用一次此函數。
};

我們可以重新定義那個virtual函數,讓其取出Widget的當時狀態。爲了讓Widget重新定義Timer內的virtual函數,Widget必須繼承自Timer。但是public繼承在此不適當,因爲Widget並不是一個Timer。此時必須以private形式繼承Timer:

class Widget: private Timer {
private:
	virtual void onTick() const;	//查看Widget數據};

但是這個設計看起來很好,實際可以不使用private繼承,可以用複合取而代之,只要在Widget內聲明一個嵌套式private class,後者以public形式繼承Timer並重新定義onTick,然後放一個這種類型的對象在Widget內:

class Widget{
private:
	class WidgetTimer: public Timer {
public:
	virtual void onTick() const;};
WidgetTimer timer;};

上述兩種實現看起來都可以實現需求,但是如果Widget擁有派生類,同時想阻止派生類重新定義onTick。這樣只能通過複合這種方式實現,因爲派生類取不到WidgetTimer對象。

  • Private繼承意味implemented-in-terms-of(根據某物實現出)。它通常比複合的級別低。但是當派生類需要訪問protected
    基類的成員,或需要重新定義繼承而來的virtual函數,使用其實很合理的。
  • 和複合不同,private繼承可以造成empty base最優化。這對致力於對象尺寸最小化的程序庫開發者而言,可能很重要。

40、明智而審慎地使用多重繼承

Use multiple inheritance judiciously.
C++社羣對於多重繼承的認識分兩大基本陣營,一派認爲如果單一繼承實好的,多重繼承一定更好。另一派認爲,單一繼承是好的,但多重繼承不值得使用。
當多重繼承進入設計框架,程序有可能從一個以上的基類繼承相同名稱。那會導致較多的歧義。例如:

class BorrowableItem{
public:
	void checkOut();};
class ElectronicGadget(){
private:
	bool checkOut()const;};
class MP3Player: public BorrowableItem, public ElectronicGadget{};
MP3Player mp;
mp.checkOut();	//這裏有歧義,無法知道調用哪個checkOut。

爲了解決這個歧義,就需要明確指出要調用哪個基類內的函數:、

mp.BorrowableItem::checkOut();

多重繼承中,如果這些基類並不在繼承體系中,但又有更高級的基類,這會導致致命的鑽石型多重繼承:

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

任何時候如果一個繼承體系中其中某個基類和某個派生類之間有一條以上的相通路線,這樣會導致一個問題,是否打算讓基類內的成員變量經由每一條路徑被複制,假設File類有個成員變量fileName,那麼IOFile從其每一個基類繼承一份,所以其對象內應該有兩份fileName成員變量。但從另一角度說,簡單的邏輯告訴我們,IOFile對象只該有一個文件名稱,所以其繼承自兩個基類而來的fileName不該重複。C++中其缺省做法爲執行復制,如果那個不是需要的,必須令那個帶有此數據的類成爲一個virtual基類。爲了實現此必須令所有直接繼承它的類採用virtual繼承:

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

C++標準程序庫內含一個多重繼承體系,其結構爲鑽石型繼承體系,不過其類爲類模板,名稱爲basic_ios,basic_istream,basic_ostream和basic_iostream。
從正確行爲的觀點看,public繼承應該總是virtual。但是使用virtual繼承的那些類所產生的對象往往比使用non-virtual繼承的對象體積大,訪問virtual基類成員變量時,也比訪問non-virtual基類成員變量速度慢。而且支配“virtual基類初始化”的規則比non-virtual基類的更復雜且不直觀。因爲virtual基類的初始化責任是由繼承體系的最底層類負責,這樣類如果派生自virtual基類而需要初始化,其必須認知其virtual基類,當一個新的派生類加入繼承體系中,必須承擔其virtual基類的初始化責任。所以非必要不是使用virtual基類,必須使用virtual基類,儘可能避免在其中放置數據。

  • 多重繼承比單一繼承複雜,它可能導致新的歧義性,以及對virtual繼承的需要。
  • virtual繼承會增加大小、速度、初始化(及賦值)複雜度等等成本。如果virtual基類不帶任何數據,將是最具有實用價值的情況。
  • 多重繼承的確有正當用途。其中一個情節涉及“public繼承某個Interface•class”和“private繼承某個協助實現的class”的兩相組合。

上一篇: C++進階_Effective_C++第三版(五) 實現 Implementations
下一篇: C++進階_Effective_C++第三版(七) 模板與泛型編程 Templates and Generic Programming

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