三. 異常
條款9: 利用destructors避免泄露資源
???? 在編程中,"資源"可以指任何系統級的有限資源,如內存、文件句柄、網絡套接字等。"泄露"則是指在應用程序中分配了資源,但在不再需要這些資源時沒有正確地釋放它們。這種狀況常常會導致資源過度使用,如內存溢出,這個問題在長時間運行的程序中尤其嚴重。
???? 而"利用destructors避免泄露資源"則是一種建議。在C++中,析構函數(Destructor) 是一種特殊的成員函數,它會在每次刪除所創建的對象時執行。
???? 匆忙地,可以在析構函數中清理和釋放資源,這樣當對象的生命周期結束時,析構函數會自動調用,阻止資源的泄漏。這是一種被稱作RAII(Resource Acquisition is Initialization)的編程技術。
???? 舉例來說,如果你的類負責管理一個動態分配的內存塊,那你可以在析構函數中使用?delete
?來釋放這個內存塊。這樣只要對象存在,你就可以確保它持有的資源會在合適的時機被釋放。
???? 所以,"利用destructors避免泄露資源"的意思是:在設計類時,應該充分利用構造函數和析構函數來管理資源,以防止任何可能的資源泄漏。
請記住:
???? 只要堅持這個規則,把資源封裝在對象內,通常便可以在exceptions出現時避免內存泄漏。
條款10: 在constructors內阻止資源泄露(resource leak)
???? "在constructors內阻止資源泄露"這句話的意思是,在設計類的構造函數時需要以一種防止資源泄露(如內存泄露)的方式管理資源。
???? 我們通常在構造函數中獲取需要的資源,并在析構函數中釋放這些資源,這是一種常用的RAII(資源獲取即初始化) 技術。但是,如果在獲取資源后和資源釋放之間發生異常,可能會導致資源泄漏。
???? 以內存申請為例,如果在構造函數中申請了內存,但后續程序出現異常,且這個異常沒有被捕獲,那么程序可能在沒有執行析構函數(也就是沒有釋放內存)的情況下終止,從而導致內存泄露。
???? 為了避免這種情況,你可以在構造函數中使用try-catch塊來捕獲異常,一旦拋出異常,就清理申請的資源,再重新拋出異常。或者利用一些智能指針(如unique_ptr、shared_ptr等),它們在析構時可以自動釋放資源,可用來避免資源泄漏。這樣做,就可以確保在構造函數執行過程中,無論是否發生異常,申請的資源都能被妥善處理,從而避免資源泄露。
條款11: 禁止異常(exceptions)流出destructors之外
???? "禁止異常(exceptions)流出destructors之外"這句話的含義是:在編寫析構函數(destructors)時,應確保任何可能拋出的異常都被妥善處理,不允許它們傳播(或“流出”)到析構函數的外部。
???? 這是一個重要的編程原則,因為在析構函數中允許異常傳播可能會引發各種復雜的問題。例如,如果析構函數在清理資源時拋出了一個異常,但是這個異常在外部沒有被捕獲,那么這個異常會導致程序猝死(即異常終止)。更糟糕的是,如果析構函數是在處理另一個異常時被調用的(比如在堆棧展開過程中),而它又拋出了第二個異常,那么程序會立即被終止。
???? 因此,良好的做法是使析構函數只做它該做的事情,即清理資源,而且要確保它能正確執行,不會拋出異常。在多數情況下,析構函數不應該進行可能會拋出異常的操作。如果無法避免,那么就應該在析構函數內部處理可能拋出的異常,確保它們不會流到析構函數的外部。這通常可以通過添加一個try-catch
塊來實現。
條款12: 了解“拋出一個exception”與“傳遞一個參數”或“調用一個虛函數”之間的差異
主要存在三個差異:
- exception objects總是會被復制,如果以
by value
方式捕捉,他們甚至被復制兩次。至于傳遞給函數參數的對象則不一定得復制。 - “被拋出成為exception”的對象,其被允許的類型轉換動作,比“被傳遞到函數去”的對象少。
catch
子句以其“出現于源代碼順序”被編譯器檢驗對比,其中第一個匹配成功者便被執行;而當我們以某對象調用一個虛函數,被選中執行的是哪那個“與對象類型最佳吻合”的函數,不論它是不是源代碼所列的第一個。
解釋:
???? “拋出一個exception”,"傳遞一個參數"以及"調用一個虛函數"都是編程中常見的操作,不過它們在功能和目標上有著顯著的差異。
???? 首先,"拋出一個exception"是一種特殊的程序流控制機制,用于處理程序運行時的異常情況。當程序運行到一個錯誤狀態,無法正常執行時,就可以拋出(throw)一個異常。這將立即中斷當前函數的執行,將控制權轉移到最近的可以處理該異常的異常處理程序(catch語句)。異常是一種用于處理程序錯誤的強大工具,但是如果過度使用,可能導致代碼邏輯難以理解和維護。
???? 其次,"傳遞一個參數"是調用函數時的一種操作。通過參數,我們可以將數據從函數調用者傳遞到被調用的函數中。參數可以是任何類型的數據,如整數、浮點數、字符串、對象等。
???? 最后,"調用一個虛函數"是面向對象編程中的一個概念,出現在如C++這樣支持多態性(Polymorphism)的語言中。通過在基類中聲明虛函數,派生類可以根據需要覆寫(Override)這個函數。在運行時,通過基類指針或引用調用虛函數,會根據實際的對象類型來決定調用哪個版本的虛函數。這是實現運行時多態性的關鍵。
???? 總的來說,“拋出一個exception”通常用于處理錯誤,“傳遞一個參數”則是在函數之間傳遞數據,“調用一個虛函數”是實現多態性的手段。這三者都是編程中重要的概念,且在使用時目標和上下文有非常大的差異。
針對上面的三點主要差異,這里做一下解釋說明:
- 這里的表述涉及到了在C++中對異常(exception)的處理方式以及函數參數的傳遞方式。
???? 首先,在C++中當你拋出一個異常對象時,這個對象會被復制。系統從throw語句開始,搜索這個異常匹配的catch塊,這個過程被稱為異常傳播。在這個過程中,原始異常對象會被拷貝一次。
???? 然后,當異常被catch塊捕獲時,如果以by value方式捕捉,即’catch (Exception e)',參數e是原始異常的副本,這樣就會發生第二次復制。因此,通過by value的方式捕獲異常將導致異常對象被復制兩次。
???? 另一方面,函數參數的傳遞方式可以有兩種,一種是by value,一種是by reference。當我們通過by value方式傳遞參數時,和catch塊捕捉異常一樣,參數也會被復制。但是,如果我們選擇通過by reference形式傳遞,例如’void function(Exception& e)',則不會發生復制,參數e是原始異常對象的引用。
???? 所以這句話的意思是:異常對象會在傳播過程中被復制,如果通過by value方式被捕獲,還會發生一次復制。而函數參數是否復制則取決于參數的傳遞方式。如果參數是通過by value方式傳遞,就會復制,如果是通過by reference方式傳遞,就不會復制。
- 這段話的含義是,在C++編程中,當一個對象被拋出為一個異常(exception)時,它可以進行的類型轉換比作為函數參數傳遞的對象要少。
???? 當一個對象被拋出為異常時,僅允許以下類型轉換:
- 到基類的轉換:如果拋出的是一個派生類對象,catch塊可以捕獲基類的異常。
- 通過構造函數定義的轉換:如果存在一個構造函數,其唯一參數是異常的源類型或者源類型可以轉換為這個參數類型。
???? 然而,當一個對象作為函數參數被傳遞時,除了上述的轉換之外,還允許以下幾種轉換:
- 提升(Promotion):比如將整數字面量提升為整數、將字符提升為整數等。
- 標準類型轉換:如將整數轉為浮點數或者相反,將指針轉為布爾值等。
- 用戶定義的轉換,例如通過轉換函數進行類型轉換。
???? 因此,“被拋出成為exception”的對象,其被允許的類型轉換動作,比“被傳遞到函數去”的對象要少。
- 這段話對
catch
子句的處理方式和虛函數的調用進行了比較,并強調了兩者在選擇執行方式上的不同。
???? catch
子句以其"出現于源代碼順序"被編譯器檢驗對比,其中第一個匹配成功者便被執行:
???? 這部分是對C++異常處理機制的描述。當一個異常被拋出時,編譯器會按照catch
子句在源代碼中出現的順序逐一檢驗這個異常是否匹配。一旦找到第一個匹配的catch
子句,就會執行這個子句中的代碼。這就解釋了為什么我們需要從最詳盡的異常類型開始寫catch
子句,再逐漸寫出越來越一般的類型,因為一旦找到匹配的子句,就不會再繼續查找。
???? 當我們以某對象調用一個虛函數,被選中執行的是哪那個“與對象類型最佳吻合”的函數,不論它是不是源代碼所列的第一個: 這部分是在描繪多態性在面向對象編程中的應用。在C++里,當我們通過一個基類指針或引用來調用派生類的虛函數時,實際上執行的是派生類版本的函數,即使在源代碼中,這個虛函數的聲明可能并不是第一個出現的。這是因為在編譯和運行時,編譯器和運行環境會選擇那個與對象實際類型最符的函數來執行,這種功能稱為多態。
???? 通過對比,這段話強調了catch
子句和虛函數在執行選擇上的區別:catch
子句按照源代碼順序選擇,而虛函數則是根據對象的實際類型來決定。
條款13: 以by reference方式捕捉exception
請記住:
???? catch by reference,就可以避開對象刪除問題;可以避開exception objects的切割問題;約束了exception objects需要被復制的次數
解釋:
???? 在C++中,我們可以用by reference方式來捕捉異常。這就意味著我們會直接使用到原本拋出的異常對象,而不是它的副本。
???? C++里的異常處理機制中,當我們使用“catch(異常類型 & 引用)” 的方式來捕獲異常,這就是以by reference方式捕捉異常。按照這種方式,系統只需要生成一次異常對象副本,即在拋出異常時,不需要在 catch 子句中再復制一次。這對于大對象或者復制對象資源消耗大的情況下,是很有用的策略。
try {// 代碼塊,可能拋出異常throw MyException();
}
catch (MyException& e) {// 這里捕獲的 e 是一個引用, 會直接影響到原本的異常對象// 對 e 的改變也會影響到原先拋出的 MyException 對象
}
???? 此外,采用這種方式捕捉異常還可以解決一些多態問題。如果有一些異常類通過繼承關系鏈接,捕捉父類異常引用可以接到子類的異常。這是由于引用可以保持多態性,允許我們通過基類引用來訪問派生類中的重寫虛函數。而拷貝的方式會切除對象的派生部分,帶來切割問題(slicing problem)。
???? “切割問題”(slicing problem),源于C++對于對象的處理方式,特別是在處理對象副本和繼承關系時常常遇到。
???? 切割問題通常發生在當你有一個派生類(derived class)對象,并且你試圖將其拷貝到一個基類(base class)對象時。在這個過程中,派生類中的所有附加信息都會被“切割”掉,只剩下基類部分。這是因為基類只能容納基類的成員,對應的派生類的成員對其來說是未知的,復制過程中會丟失這部分信息,這就是所謂的切割問題。
???? 當我們處理異常時,如果用基類類型去捕獲派生類的異常(這在面向對象設計中是很常見的情況,我們希望用同一段處理代碼來處理一類相關的異常),如果我們是按值捕獲的,就會發生切割問題,所有的異常都會被處理成基類類型的異常。
???? 但如果我們使用引用方式捕獲異常,就可以避免切割問題。因為引用方式不涉及對象的復制,它直接引用到了原始的異常對象,保持了對象的完整性,包括其真正的類型。這樣基于類型的動態綁定仍然能夠正確工作,我們能夠捕獲并處理正確的異常類型。
???? 所以,“catch by reference可以避開exception objects的切割問題”這句話的意思就是,通過引用的方式捕獲異常可以避免丟失原始異常類型的信息,從而避免了異常切割的問題。
條款14: 明智運用exception sepcifications
???? “明智運用exception specifications” 這句話關注的是如何適當地使用C++中的異常規范(exception specifications)。
????
???? 在C++中,異常規范是一種語法,允許函數說明它可能拋出的異常類型。這是在函數聲明的尾部,通過"throw(異常類型列表)"來進行說明的。比如,一個函數可能這樣聲明:“void foo() throw(A, B);”,表示foo()函數可能會拋出類型A和B的異常。如果函數聲明時未指定throw列表,或者這個列表為空,那么函數就可以拋出任何類型的異常,或不拋出異常。
???? 然而,在C++11以后,異常規范這一設定已經被廢棄,不再建議使用。取而代之的是noexcept關鍵字,用來指示函數是否可能拋出異常。
????
???? 在"明智運用exception specifications"這個表述中,"明智"兩字暗示了要根據具體情況和需求謹慎使用這類規范,避免濫用。過度使用異常規范可能引起不必要的復雜性,并可能對性能產生影響。另外,也需要注意隨著C++標準的更新,異常規范的使用情況也在發生變化。所以,在設計和編寫代碼時,需要密切關注最新的實踐和建議,明智地運用這些規范。
條款15: 了解異常處理(exception handing)的成本
解釋:
???? "了解異常處理(exception handing)的成本"這句話是在說我們應該了解在我們的代碼中使用異常處理機制帶來的開銷。
???? 在大多數編程語言中,異常處理都有自己的開銷。這里的"開銷"主要包括兩部分:時間開銷和空間開銷。
-
時間開銷:當一個異常被拋出時,需要在調用棧上回溯查找匹配的catch塊,這個過程需要時間。另外,創建異常對象、復制異常對象到catch塊(如果不是以引用方式捕獲)以及析構對象都是需要時間的。
-
空間開銷:異常處理機制需要在內存中保存足夠的信息,以便在異常發生時可以正確地解 unwinding 調用棧。這包括異常對象本身的空間,以及可能需要通過棧回溯的信息。
???? 通常來說,如果異常不頻繁發生,這些開銷可能不大。但是在一些性能臨界的應用中,這些成本可能就相當重要了。
???? 了解這些異常處理的成本,可以幫助我們更好地在性能和可讀性、易用性之間做出平衡。例如,一般來說,我們推薦在異常是真正"異常"的情況下使用異常機制,比如函數預計不會失敗,但卻由于某些無法預計的原因比如內存耗盡而失敗。對于預期可能會失敗的函數,比如文件打開操作,使用返回錯誤碼可能是更好的方式。這樣可以保證性能,但又不犧牲代碼的結構。