條款42 明智地使用私有繼承

2021-06-03 11:03:06 字數 4528 閱讀 6206

條款35說明,c++將公有繼承視為 "是乙個" 的關係。它是通過這個例子來證實的:假如某個類層次結構中,student類從person類公有繼承,為了使某個函式成功呼叫,編譯器可以在必要時隱式地將student轉換為person。這個例子很值得再看一遍,只是現在,公有繼承換成了私有繼承:

class person ;

class student:                      // 這一次我們

private person ;           // 使用私有繼承

void dance(const person& p);        // 每個人會跳舞

void study(const student& s);       // 只有學生才學習

person p;                           // p是乙個人

student s;                          // s是乙個學生

dance(p);                           // 正確, p是乙個人

dance(s);                           // 錯誤!乙個學生不是乙個人

很顯然,私有繼承的含義不是 "是乙個",那它的含義是什麼呢?

"別忙!" 你說。"在弄清含義之前,讓我們先看看行為。私有繼承有那些行為特徵呢?" 那好吧。關於私有繼承的第乙個規則正如你現在所看到的:和公有繼承相反,如果兩個類之間的繼承關係為私有,編譯器一般不會將派生類物件(如student)轉換成基類物件(如person)。這就是上面的**中為物件s呼叫dance會失敗的原因。第二個規則是,從私有基類繼承而來的成員都成為了派生類的私有成員,即使它們在基類中是保護或公有成員。行為特徵就這些。

這為我們引出了私有繼承的含義:私有繼承意味著 "用...來實現"。如果使類d私有繼承於類b,這樣做是因為你想利用類b中已經存在的某些**,而不是因為型別b的物件和型別d的物件之間有什麼概念上的關係。因而,私有繼承純粹是一種實現技術。用條款36引入的術語來說,私有繼承意味著只是繼承實現,介面會被忽略。如果d私有繼承於b,就是說d物件在實現中用到了b物件,僅此而已。私有繼承在軟體 "設計" 過程中毫無意義,只是在軟體 "實現" 時才有用。

私有繼承意味著 "用...來實現" 這一事實會給程式設計師帶來一點混淆,因為條款40指出,"分層" 也具有相同的含義。怎麼在二者之間進行選擇呢?答案很簡單:盡可能地使用分層,必須時才使用私有繼承。什麼時候必須呢?這往往是指有保護成員和/或虛函式介入的時候 ---- 但這個問題過一會兒再深入討論。

條款41提供了一種方法來寫乙個stack 模板,此模板生成的類儲存不同型別的物件。你應該熟悉一下那個條款。模板是c++最有用的組成部分之一,但一旦開始經常性地使用它,你會發現,如果例項化乙個模板一百次,你就可能例項化了那個模板的**一百次。例如stack模板,構成stack成員函式的**和構成stack成員函式的**是完全分開的。有時這是不可避免的,但即使模板函式實際上可以共享**,這種**重複還是可能存在。這種目標**體積的增加有乙個名字:模板導致的 "**膨脹"。這不是件好事。

對於某些類,可以採用通用指標來避免它。採用這種方法的類儲存的是指標,而不是物件,實現起來就是:

· 建立乙個類,它儲存的是物件的void*指標。

· 建立另外一組類,其唯一目的是用來保證型別安全。這些類都借助第一步中的通用類來完成實際工作。

下面的例子使用了條款41中的非模板stack類,不同的是這裡儲存的是通用指標,而不是物件:

class genericstack

};stacknode *top;                          // 棧頂

genericstack(const genericstack& rhs);   // 防止拷貝和

genericstack&                            // 賦值(參見

operator=(const genericstack& rhs);    // 條款27)

};因為這個類儲存的是指標而不是物件,就有可能出現乙個物件被多個堆疊指向的情況(即,被壓入到多個堆疊)。所以極其重要的一點是,pop和類的析構函式銷毀任何stacknode物件時,都不能刪除data指標 ---- 雖然還是得要刪除stacknode物件本身。畢竟,stacknode 物件是在genericstack類內部分配的,所以還是得在類的內部釋放。所以,條款41中stack類的實現幾乎完全滿足the genericstack的要求。僅有的改變只是用void*來替換t。

僅僅有genericstack這乙個類是沒有什麼用處的,但很多人會很容易誤用它。例如,對於乙個用來儲存int的堆疊,乙個使用者會錯誤地將乙個指向cat物件的指標壓入到這個堆疊中,但編譯卻會通過,因為對void*引數來說,指標就是指標。

為了重新獲得你所習慣的型別安全,就要為genericstack建立介面類(inte***ce class),象這樣:

class intstack

int * pop()

bool empty() const

private:

genericstack s;                 // 實現

};class catstack

cat * pop()

bool empty() const

private:

genericstack s;                 // 實現

};正如所看到的,intstack和catstack只是適用於特定型別。只有int指標可以被壓入或彈出intstack,只有cat指標可以被壓入或彈出catstack。intstack和catstack都通過genericstack類來實現,這種關係是通過分層(參見條款40)來體現的,intstack和catstack將共享genericstack中真正實現它們行為的函式**。另外,intstack和catstack所有成員函式是(隱式)內聯函式,這意味著使用這些介面類所帶來的開銷幾乎是零。

但如果有些使用者沒認識到這一點怎麼辦?如果他們錯誤地認為使用genericstack更高效,或者,如果他們魯莽而輕率地認為型別安全不重要,那該怎麼辦?怎麼才能阻止他們繞過intstack和catstack而直接使用genericstack(這會讓他們很容易地犯型別錯誤,而這正是設計c++所要特別避免的)呢?

沒辦法!沒辦法防止。但,也許應該有什麼辦法。

在本條款的開始我就提到,要表示類之間 "用...來實現" 的關係,有乙個選擇是通過私有繼承。現在這種情況下,這一技術就比分層更有優勢,因為通過它可以讓你告訴別人:genericstack使用起來不安全,它只能用來實現其它的類。具體做法是將genericstack的成員函式宣告為保護型別:

class genericstack ;

genericstack s;                   // 錯誤! 建構函式被保護

class intstack: private genericstack

int * pop()

bool empty() const

};class catstack: private genericstack

cat * pop()

bool empty() const

};intstack is;                     // 正確

catstack cs;                     // 也正確

和分層的方法一樣,基於私有繼承的實現避免了**重複,因為這個型別安全的介面類只包含有對genericstack函式的內聯呼叫。

在genericstack類之上構築型別安全的介面是個很花俏的技巧,但需要手工去寫所有那些介面類是件很煩的事。幸運的是,你不必這樣。你可以讓模板來自動生成它們。下面是乙個模板,它通過私有繼承來生成型別安全的堆疊介面:

template

class stack: private genericstack

t * pop()

bool empty() const

};這是一段令人驚嘆的**,雖然你可能一時還沒意識到。因為這是乙個模板,編譯器將根據你的需要自動生成所有的介面類。因為這些類是型別安全的,使用者型別錯誤在編譯期間就能發現。因為genericstack的成員函式是保護型別,並且介面類把genericstack作為私有基類來使用,使用者將不可能繞過介面類。因為每個介面類成員函式被(隱式)宣告為inline,使用這些型別安全的類時不會帶來執行開銷;生成的**就象使用者直接使用genericstack來編寫的一樣(假設編譯器滿足了inline請求 ---- 參見條款33)。因為genericstack使用了void*指標,操作堆疊的**就只需要乙份,而不管程式中使用了多少不同型別的堆疊。簡而言之,這個設計使**達到了最高的效率和最高的型別安全。很難做得比這更好。

本書的基本認識之一是,c++的各種特性是以非凡的方式相互作用的。這個例子,我希望你能同意,確實是非凡的。

從這個例子中可以發現,如果使用分層,就達不到這樣的效果。只有繼承才能訪問保護成員,只有繼承才使得虛函式可以重新被定義。(虛函式的存在會引發私有繼承的使用,例子參見條款43)因為存在虛函式和保護成員,有時私有繼承是表達類之間 "用...來實現" 關係的唯一有效途徑。所以,當私有繼承是你可以使用的最合適的實現方法時,就要大膽地使用它。同時,廣泛意義上來說,分層是應該優先採用的技術,所以只要有可能,就要盡量使用它。

條款42 明智地使用私有繼承

第乙個規則是,和公有繼承相反,如果兩個類之間的繼承關係為私有,編譯器一般不會將派生類物件 如student 轉換成基類物件 如person 第二個規則是,從私有基類繼承而來的成員都成為了派生類的私有成員,即使它們在基類中是保護或公有成員,即派生類物件不能訪問基類的所有成員 class person ...

明智地使用Pimpl

明智地使用pimpl 首先引用一下別人的內容 pimpl 用法背後的思想是把客戶與所有關於類的私有部分的知識隔離開。由於客戶是依賴於類的標頭檔案的,標頭檔案中的任何變化都會影響客戶,即使僅是對私有節或保護節的修改。pimpl用法隱藏了這些細節,方法是將私有資料和函式放入乙個單獨的類中,並儲存在乙個實...

Item 39 明智地使用 private 繼承

public 繼承表示 is a 的關係,這是因為編譯器會在需要的時候將子類物件隱式轉換為父類物件。然而 private 繼承則不然 class person class student private person void eat const person p person p student ...