條款42: 明智地使用私有繼承
條款35說明,C++將公有繼承視為 "是一個(gè)" 的關(guān)系。它是通過這個(gè)例子來證實(shí)的:假如某個(gè)類層次結(jié)構(gòu)中,Student類從Person類公有繼承,為了使某個(gè)函數(shù)成功調(diào)用,編譯器可以在必要時(shí)隱式地將Student轉(zhuǎn)換為Person。這個(gè)例子很值得再看一遍,只是現(xiàn)在,公有繼承換成了私有繼承:
class Person { ... };
class Student:/t/t // 這一次我們
private Person { ... };/t // 使用私有繼承
void dance(const Person& p); // 每個(gè)人會(huì)跳舞
void study(const Student& s); // 只有學(xué)生才學(xué)習(xí)
Person p;/t/t/t // p是一個(gè)人
Student s;/t/t/t // s是一個(gè)學(xué)生
dance(p);/t/t/t // 正確, p是一個(gè)人
dance(s);/t/t/t // 錯(cuò)誤!一個(gè)學(xué)生不是一個(gè)人
很顯然,私有繼承的含義不是 "是一個(gè)",那它的含義是什么呢?
"別忙!" 你說。"在弄清含義之前,讓我們先看看行為。私有繼承有那些行為特征呢?" 那好吧。關(guān)于私有繼承的第一個(gè)規(guī)則正如你現(xiàn)在所看到的:和公有繼承相反,如果兩個(gè)類之間的繼承關(guān)系為私有,編譯器一般不會(huì)將派生類對(duì)象(如Student)轉(zhuǎn)換成基類對(duì)象(如Person)。這就是上面的代碼中為對(duì)象s調(diào)用dance會(huì)失敗的原因。第二個(gè)規(guī)則是,從私有基類繼承而來的成員都成為了派生類的私有成員,即使它們?cè)诨愔惺潜Wo(hù)或公有成員。行為特征就這些。
這為我們引出了私有繼承的含義:私有繼承意味著 "用...來實(shí)現(xiàn)"。如果使類D私有繼承于類B,這樣做是因?yàn)槟阆肜妙怋中已經(jīng)存在的某些代碼,而不是因?yàn)轭愋虰的對(duì)象和類型D的對(duì)象之間有什么概念上的關(guān)系。因而,私有繼承純粹是一種實(shí)現(xiàn)技術(shù)。用條款36引入的術(shù)語(yǔ)來說,私有繼承意味著只是繼承實(shí)現(xiàn),接口會(huì)被忽略。如果D私有繼承于B,就是說D對(duì)象在實(shí)現(xiàn)中用到了B對(duì)象,僅此而已。私有繼承在軟件 "設(shè)計(jì)" 過程中毫無意義,只是在軟件 "實(shí)現(xiàn)" 時(shí)才有用。
私有繼承意味著 "用...來實(shí)現(xiàn)" 這一事實(shí)會(huì)給程序員帶來一點(diǎn)混淆,因?yàn)闂l款40指出,"分層" 也具有相同的含義。怎么在二者之間進(jìn)行選擇呢?答案很簡(jiǎn)單:盡可能地使用分層,必須時(shí)才使用私有繼承。什么時(shí)候必須呢?這往往是指有保護(hù)成員和/或虛函數(shù)介入的時(shí)候 ---- 但這個(gè)問題過一會(huì)兒再深入討論。
條款41提供了一種方法來寫一個(gè)Stack 模板,此模板生成的類保存不同類型的對(duì)象。你應(yīng)該熟悉一下那個(gè)條款。模板是C++最有用的組成部分之一,但一旦開始經(jīng)常性地使用它,你會(huì)發(fā)現(xiàn),如果實(shí)例化一個(gè)模板一百次,你就可能實(shí)例化了那個(gè)模板的代碼一百次。例如Stack模板,構(gòu)成Stack<int>成員函數(shù)的代碼和構(gòu)成Stack<double>成員函數(shù)的代碼是完全分開的。有時(shí)這是不可避免的,但即使模板函數(shù)實(shí)際上可以共享代碼,這種代碼重復(fù)還是可能存在。這種目標(biāo)代碼體積的增加有一個(gè)名字:模板導(dǎo)致的 "代碼膨脹"。這不是件好事。
對(duì)于某些類,可以采用通用指針來避免它。采用這種方法的類存儲(chǔ)的是指針,而不是對(duì)象,實(shí)現(xiàn)起來就是:
? 創(chuàng)建一個(gè)類,它存儲(chǔ)的是對(duì)象的void*指針。
? 創(chuàng)建另外一組類,其唯一目的是用來保證類型安全。這些類都借助第一步中的通用類來完成實(shí)際工作。
下面的例子使用了條款41中的非模板Stack類,不同的是這里存儲(chǔ)的是通用指針,而不是對(duì)象:
class GenericStack {
public:
GenericStack();
~GenericStack();
void push(void *object);
void * pop();
bool empty() const;
private:
struct StackNode {
void *data;/t/t // 節(jié)點(diǎn)數(shù)據(jù)
StackNode *next;/t // 下一節(jié)點(diǎn)
StackNode(void *newData, StackNode *nextNode)
: data(newData), next(nextNode) {}
};
StackNode *top;/t/t/t // 棧頂
GenericStack(const GenericStack& rhs); // 防止拷貝和
GenericStack&/t/t/t // 賦值(參見
operator=(const GenericStack& rhs); // 條款27)
};
因?yàn)檫@個(gè)類存儲(chǔ)的是指針而不是對(duì)象,就有可能出現(xiàn)一個(gè)對(duì)象被多個(gè)堆棧指向的情況(即,被壓入到多個(gè)堆棧)。所以極其重要的一點(diǎn)是,pop和類的析構(gòu)函數(shù)銷毀任何StackNode對(duì)象時(shí),都不能刪除data指針 ---- 雖然還是得要?jiǎng)h除StackNode對(duì)象本身。畢竟,StackNode 對(duì)象是在GenericStack類內(nèi)部分配的,所以還是得在類的內(nèi)部釋放。所以,條款41中Stack類的實(shí)現(xiàn)幾乎完全滿足the GenericStack的要求。僅有的改變只是用void*來替換T。
僅僅有GenericStack這一個(gè)類是沒有什么用處的,但很多人會(huì)很容易誤用它。例如,對(duì)于一個(gè)用來保存int的堆棧,一個(gè)用戶會(huì)錯(cuò)誤地將一個(gè)指向Cat對(duì)象的指針壓入到這個(gè)堆棧中,但編譯卻會(huì)通過,因?yàn)閷?duì)void*參數(shù)來說,指針就是指針。
為了重新獲得你所習(xí)慣的類型安全,就要為GenericStack創(chuàng)建接口類(interface class),象這樣:
class IntStack {/t/t // int接口類
public:
void push(int *intPtr) { s.push(intPtr); }
int * pop() { return static_cast<int*>(s.pop()); }
bool empty() const { return s.empty(); }
private:
GenericStack s;/t/t // 實(shí)現(xiàn)
};
class CatStack {/t/t // cat接口類
public:
void push(Cat *catPtr) { s.push(catPtr); }
Cat * pop() { return static_cast<Cat*>(s.pop()); }
bool empty() const { return s.empty(); }
private:
GenericStack s;/t/t // 實(shí)現(xiàn)
};
正如所看到的,IntStack和CatStack只是適用于特定類型。只有int指針可以被壓入或彈出IntStack,只有Cat指針可以被壓入或彈出CatStack。IntStack和CatStack都通過GenericStack類來實(shí)現(xiàn),這種關(guān)系是通過分層(參見條款40)來體現(xiàn)的,IntStack和CatStack將共享GenericStack中真正實(shí)現(xiàn)它們行為的函數(shù)代碼。另外,IntStack和CatStack所有成員函數(shù)是(隱式)內(nèi)聯(lián)函數(shù),這意味著使用這些接口類所帶來的開銷幾乎是零。
但如果有些用戶沒認(rèn)識(shí)到這一點(diǎn)怎么辦?如果他們錯(cuò)誤地認(rèn)為使用GenericStack更高效,或者,如果他們魯莽而輕率地認(rèn)為類型安全不重要,那該怎么辦?怎么才能阻止他們繞過IntStack和CatStack而直接使用GenericStack(這會(huì)讓他們很容易地犯類型錯(cuò)誤,而這正是設(shè)計(jì)C++所要特別避免的)呢?
沒辦法!沒辦法防止。但,也許應(yīng)該有什么辦法。
在本條款的開始我就提到,要表示類之間 "用...來實(shí)現(xiàn)" 的關(guān)系,有一個(gè)選擇是通過私有繼承。現(xiàn)在這種情況下,這一技術(shù)就比分層更有優(yōu)勢(shì),因?yàn)橥ㄟ^它可以讓你告訴別人:GenericStack使用起來不安全,它只能用來實(shí)現(xiàn)其它的類。具體做法是將GenericStack的成員函數(shù)聲明為保護(hù)類型:
class GenericStack {
protected:
GenericStack();
~GenericStack();
void push(void *object);
void * pop();
bool empty() const;
private:
.../t/t/t // 同上
};
GenericStack s;/t/t // 錯(cuò)誤! 構(gòu)造函數(shù)被保護(hù)
class IntStack: private GenericStack {
public:
void push(int *intPtr) { GenericStack::push(intPtr); }
int * pop() { return static_cast<int*>(GenericStack::pop()); }
bool empty() const { return GenericStack::empty(); }
};
class CatStack: private GenericStack {
public:
void push(Cat *catPtr) { GenericStack::push(catPtr); }
Cat * pop() { return static_cast<Cat*>(GenericStack::pop()); }
bool empty() const { return GenericStack::empty(); }
};
IntStack is;/t/t // 正確
CatStack cs;/t/t // 也正確
和分層的方法一樣,基于私有繼承的實(shí)現(xiàn)避免了代碼重復(fù),因?yàn)檫@個(gè)類型安全的接口類只包含有對(duì)GenericStack函數(shù)的內(nèi)聯(lián)調(diào)用。
在GenericStack類之上構(gòu)筑類型安全的接口是個(gè)很花俏的技巧,但需要手工去寫所有那些接口類是件很煩的事。幸運(yùn)的是,你不必這樣。你可以讓模板來自動(dòng)生成它們。下面是一個(gè)模板,它通過私有繼承來生成類型安全的堆棧接口:
template<class T>
class Stack: private GenericStack {
public:
void push(T *objectPtr) { GenericStack::push(objectPtr); }
T * pop() { return static_cast<T*>(GenericStack::pop()); }
bool empty() const { return GenericStack::empty(); }
};
這是一段令人驚嘆的代碼,雖然你可能一時(shí)還沒意識(shí)到。因?yàn)檫@是一個(gè)模板,編譯器將根據(jù)你的需要自動(dòng)生成所有的接口類。因?yàn)檫@些類是類型安全的,用戶類型錯(cuò)誤在編譯期間就能發(fā)現(xiàn)。因?yàn)镚enericStack的成員函數(shù)是保護(hù)類型,并且接口類把GenericStack作為私有基類來使用,用戶將不可能繞過接口類。因?yàn)槊總€(gè)接口類成員函數(shù)被(隱式)聲明為inline,使用這些類型安全的類時(shí)不會(huì)帶來運(yùn)行開銷;生成的代碼就象用戶直接使用GenericStack來編寫的一樣(假設(shè)編譯器滿足了inline請(qǐng)求 ---- 參見條款33)。因?yàn)镚enericStack使用了void*指針,操作堆棧的代碼就只需要一份,而不管程序中使用了多少不同類型的堆棧。簡(jiǎn)而言之,這個(gè)設(shè)計(jì)使代碼達(dá)到了最高的效率和最高的類型安全。很難做得比這更好。
本書的基本認(rèn)識(shí)之一是,C++的各種特性是以非凡的方式相互作用的。這個(gè)例子,我希望你能同意,確實(shí)是非凡的。
從這個(gè)例子中可以發(fā)現(xiàn),如果使用分層,就達(dá)不到這樣的效果。只有繼承才能訪問保護(hù)成員,只有繼承才使得虛函數(shù)可以重新被定義。(虛函數(shù)的存在會(huì)引發(fā)私有繼承的使用,例子參見條款43)因?yàn)榇嬖谔摵瘮?shù)和保護(hù)成員,有時(shí)私有繼承是表達(dá)類之間 "用...來實(shí)現(xiàn)" 關(guān)系的唯一有效途徑。所以,當(dāng)私有繼承是你可以使用的最合適的實(shí)現(xiàn)方法時(shí),就要大膽地使用它。同時(shí),廣泛意義上來說,分層是應(yīng)該優(yōu)先采用的技術(shù),所以只要有可能,就要盡量使用它。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注