《Effective C++》閱讀總結(二):類的構造、析構和賦值

zq發表於2022-05-28

今天是週六早上,但很不幸待會兒還是要去公司,本月kpi還剩一些工作要做,這個月計劃的Effective C++學習,也基本完成了,最後一章節模板相關那部分還看不太懂,就大概過了一遍。現在是收尾總結階段了。這本書的準則在這裡我想盡量精簡化,本篇主要是第二章節的內容:構造、析構和賦值。

5. 瞭解C++默默編寫和呼叫了哪些函式

建立class時,建構函式和解構函式是非常重要的。建構函式是在建立物件時呼叫的,涉及到相關成員變數的初始化工作。解構函式則是在銷燬物件是呼叫的,不完備的解構函式很容易造成堆記憶體洩漏。我們先看建構函式,如果不顯式定義建構函式,編譯器會預設自動建立建構函式,但一旦我們定義了某個建構函式,編譯器將不會為你生成其他建構函式(編譯器這裡認為你只需要你宣告的建構函式),若有需要,使用=default可以告訴編譯器生成對應的預設函式。
同樣,拷貝建構函式和拷貝賦值運算子如果沒有宣告,編譯器也會自動建立一個預設版本。且這些函式都是public和inline的。注意,預設生成的拷貝構造和拷貝賦值往往會執行淺拷貝,這在有些場景下是危險的。
此外,如若沒有宣告解構函式,編譯器也會生成解構函式,這個解構函式中會依次呼叫每個成員變數的解構函式。PS:一個物件析構兩次會導致行為未定義的錯誤。

6. 若不想使用編譯器生成的函式,就要明確拒絕

上一條準則說明了編譯器會自動生成一些成員函式,但當我們需要禁用一些自動生成的函式介面時,我們可以將其宣告為private的,例如私有的operator=可以禁止拷貝賦值。在C++11以上版本,我們還可以使用=delete來直接禁止對應方法的呼叫。

7. 為多型基類宣告virtual解構函式

這個準則是和多型呼叫一致的,當我們將一個子類物件的指標賦給一個父類指標物件時,我們可以通過虛擬函式表呼叫到真正的子類方法,而在析構的時候也是一樣,只有當基類的解構函式時虛擬函式的時候,才能先正確呼叫到子類的解構函式,然後子類的解構函式會自動呼叫基類的解構函式,從而完成物件資源的完全釋放,不會導致記憶體洩漏。PS:如果一個class帶有任何virtual函式,那麼就是希望該class在未來會被繼承,就有可能將一個子類物件指標賦值給父類指標,對應的virtual函式會執行多型呼叫,解構函式亦同。反之,不期望作為基類的class,不應該宣告virtual解構函式。

8. 別讓異常逃離解構函式

如果在析構過程中出現了異常,將其往上層、即呼叫方傳播是不明智的,因為只會導致析構沒有正常結束,很有可能導致記憶體洩漏。我們應當儘量將可能丟擲異常的操作放到成員函式中,並對異常進行處理

9. 絕對不在構造和析構過程中呼叫virtual函式

因為虛擬函式是為了讓子類選擇是否重寫的成員函式,同時為該函式提供一個預設實現;(純虛擬函式是強制其繼承體系中可例項化的子類必須實現)。所以,在建構函式中呼叫一個虛擬函式,這個虛擬函式只能對映到當前層級的class中,所以可能無法按照預期呼叫到子類實現的虛擬函式上去。(另一個角度,構造還未完成,所以還找不到虛擬函式表,所以無法完成多型呼叫)。
解構函式中呼叫虛擬函式也是十分危險的,假如你在子類解構函式開始呼叫,子類的成員便會變得未定義,執行完子類析構後,開始進入父類解構函式,這個解構函式中如果呼叫virtual函式,只會對映到父類的對應virtual函式上,並不會對映到你最初呼叫的那個子類的成員函式上,這種時候很容易發生純虛擬函式被呼叫的報錯,導致程式直接crash掉。

10. 令operator=返回一個reference to *this

我們寫的每行程式碼,如果有很多操作符,按照優先順序通過右結合律去匹配運算元。比如連續賦值,所以賦值操作也需要返回一個值去和次右運算元匹配。為實現連續賦值,賦值操作必須返回一個reference指向賦值操作符左側實參。所以,對於class中的操作符過載函式,也需要返回一個reference to *this。這非必須,但很必要,也符合預期。

11. 在operator=中處理“自我賦值”

這個也不是很重要的準則,只是為了確保客戶程式碼異常呼叫引發不可預料的結果。目的主要還是要保證物件拷貝時的異常安全性,所以在實現每個成員拷貝前,通常加一個“證同測試”是明智之舉。此外,利用copy-and-swap技術,可以保證異常安全性,先宣告拷貝賦值傳參方式為by value,那麼可以構造一個右運算元的副本,然後將其與左運算元交換內容,最後返回左運算元的引用,右運算元是臨時物件會自動銷燬。

12. 複製物件時勿忘其每一部分

OOP中我們封裝的類一般要實現兩個copy函式,一個是拷貝構造,一個是拷貝賦值。首先,要明確拷貝操作的深淺,尤其是指標的拷貝,還有父類的成員拷貝也需要顯式呼叫。最後,不能用拷貝賦值函式呼叫拷貝建構函式,反之也不行,行為無意義。這兩個函式執行的目的基本一致,所以可能會存在重複程式碼,所以可將重複程式碼拎出到一個單獨的成員函式來呼叫,以降低程式碼複雜度。

小結:以上。

相關文章