天天看點

《C++程式設計風格(修訂版)》——3.4 封裝

本節書摘來自異步社群出版社《c++程式設計風格(修訂版)》一書中的第3章,第3.4節,作者:【美】tom cargill,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

c++程式設計風格(修訂版)

新的堆棧抽象更簡單,程式的代碼量也更小,并且也使用了更少的記憶體。然而,在stackindex及其派生類的關系中還存在着尚未暴露出來的問題。在本章的前面部分,我們已經注意到,派生類的成員函數push()和pop()隐藏了它們從基類繼承而來的同名成員函數。不過,我們也可以在intstack和charstack對象上來調用這些基類函數,這就需要使用函數的完全解析名字:stackindex::push()和stackindex::pop()。事實上,在它們各自的派生類成員函數中,也正是通過這種方式來調用stackindex::push()和stackindex::pop()的。

不過,即使不使用作用域解析運算符,我們還是可以通路到基類的成員函數push()和pop()。例如,當通過stackindex類型的指針或者引用來使用intstack或charstack的對象時,那麼調用的将是基類的成員函數push和pop。然而,這種基類成員函數的可通路性将會嚴重地破壞堆棧的封裝性:程式中的客戶代碼可能會使得堆棧對象處于不一緻的狀态。在下面的函數中給出了破壞intstack封裝性的3種方法:

《C++程式設計風格(修訂版)》——3.4 封裝

通過調用基類的公有成員函數,violate()将堆棧的索引推進了一位,但卻沒有提供一個數值壓入到堆棧中。這樣導緻的結果就是堆棧的大小增加了1,但由于在入棧的時候并沒有提供一個值,是以在随後調用pop()時,所傳回的值将是未定義的:這個值将是進行壓入操作時,在相應數組元素記憶體位置上的任意值。

我們已經看到了由于客戶代碼可以直接操縱堆棧的實作,是以将導緻了堆棧對象處于未定義的狀态,這樣堆棧抽象的封裝性就被破壞了。在我們修改代碼來消除基類中的指針數組時,這個問題并沒有被暴露出來。其實,在最初的代碼中,這個封裝性的漏洞就已經存在了。現在我們必須堵上這個漏洞。

《C++程式設計風格(修訂版)》——3.4 封裝

與繼承相關的還有一個問題:基類的析構函數并沒有被聲明為虛函數。如果動态建立了一個intstack對象,并通過基類型的指針來删除這個對象時,那麼将隻會調用基類的析構函數。從這個意義來看,析構函數的行為與其他成員函數的行為一樣:析構函數的調用取決于指針的類型。

由于派生類的析構函數沒有被調用,是以程式在使用intstack或者charstack時就存在着潛在的記憶體洩漏。在第2章中,string類的記憶體洩漏問題是由于在成員函數中遺漏了對delete的調用。而在本程式中,即使派生類的析構函數是正确的,也還會産生記憶體洩漏。隻有當派生類的析構函數被調用時,函數中的delete才會執行,而現在當我們通過基類型的指針來删除派生類的對象時,派生類的析構函數并不會被調用。如果為堆棧資料配置設定的數組沒有被删除,那麼在程式中将會不斷積累垃圾記憶體并将最終耗盡記憶體空間。在軟體運作的早期,記憶體洩漏很難被檢測出來,而小規模的測試并不會消耗過多的記憶體,是以也無法産生明顯的錯誤現象。在第7章中将給出如何通過程式來監視記憶體的使用情況以及如何發現記憶體洩漏。

我們可以通過在基類中聲明一個虛的析構函數來改正這個潛在的記憶體洩漏,但這樣做隻是一種權宜的解決方案,并不能反映出代碼中真正的結構性問題。對于這些類,我們可以找出更好的解決方案。我們将在第4章和第9章中再次讨論基類的虛析構函數問題。

事實上,我們可以有兩種解決方案,這兩種方案都可以同時解決封裝性的漏洞問題和記憶體洩漏問題。第一種方案是将stackindex作為一個私有繼承的基類。私有繼承不但能夠防止基類的公有接口成為派生類公有接口的一部分,還能夠防止将基類型的指針或者引用指向派生類的對象。如果将stackindex作為一個私有的基類,那麼在編譯函數violate()時将産生一系列的錯誤資訊:

《C++程式設計風格(修訂版)》——3.4 封裝

任何對私有基類成員的通路都是非法的,同樣,任何将私有基類型的指針或者引用指向派生類的對象也是非法的。這樣,在析構過程中的問題也就同時得到了解決。由于私有基類型的指針不能指向派生類的對象,是以通過基類型的指針來對派生類對象進行的delete操作也就不存在。

繼續閱讀