搜尋

首頁  >  問答  >  主體

为什么c++抛出异常后还能对函数内的局部对象进行析构?

C++是如何确保出了异常还能调用析构函数的

天蓬老师天蓬老师2806 天前650

全部回覆(3)我來回復

  • 迷茫

    迷茫2017-04-17 11:45:14

    如何確保? 標準確保。因為這是標準規定的

    以下摘自 C++ 11 Standard (draft N3690)

    15.2 Constructors and destructors [except.ctor]

    不光保證了被析構,也規定了析構的順序。

    1) As control passes from the point where an exception is thrown to a handler, destructors are invoked for all automatic objects constructed since the try block was entered. The automatic objects are destroyed in the reverse order of the completion of their construction.

    比較特殊一點的,如果異常發生在構造或析構的時候,其子對象也能確保被正確的析構。而該對象本身呢?構造的時候它還不存在呢,所以無須擔心。析構的情況會在後面說明。

    2) An object of any storage duration whose initialization or destruction is terminated by an exception will have destructors executed for all of its fully constructed subobjects< have destructors executed for all of its fully constructed subobjects<木> (exclus), excloo that is, for subobjects for which the principal constructor (12.6.2) has completed execution and the destructor has not yet begun execution. Similarly, if the non-delegating constructor for an object has leted conob​​話an exception, the object's destructor will be invoked. If the object was allocated in a new-expression, the matching deallocation function (3.7.4.2, 5.3.4, 12.55), to the storage occupied by the object.

    這整個過程稱為 "stack unwinding",翻譯過來叫:

    堆疊輾轉開解

    3) The process of calling destructors for automatic objects constructed on the path from a try block to the point where an exception is thrown is called

    “stack unwinding.”<🎜 thrown is called “stack unwinding.”<🎜 thrown is called “stack unwinding.”<🎜 thr. If destructor cem. with an exception, std::terminate is called (15.5.1). [Note: So destructors should generally catch exceptions and not let them propagate out of the destructor. —end note]

    注意看那個 “Note”,標準對編譯器做了要求,對於析構函數來說,是要對自身負責的。

    正是因為 stack unwinding 的保證作為基礎,才有了我們所熟知的 RAII 技術。

    另外可以注意到,如果在 stack unwinding 期間拋出異常呢?就只能呼叫 std::terminate 了:

    15.5.1 The std::terminate() function [except.terminate]

    2) 在這種情況下,std::terminate() 被稱為 (18.8.3)。 在沒有找到符合處理程序的情況下,在呼叫 std::terminate() 之前是否展開堆疊是實作定義的。 在搜尋處理程序的情況下(15.3)遇到具有不允許異常(15.4) 的noexcept 規範的函數的最外層塊,在調用std::terminate() 之前堆疊是展開、部分展開還是根本不展開是由實作定義的。在所有其他情況下,在呼叫 std::terminate() 之前不得展開堆疊。不允許實作根據展開過程最終將導致呼叫 std::terminate()

    來提前完成堆疊展開

    堆疊展開的過程並不能保證完成,但最終肯定是要呼叫std::terminate來終止。

    回覆
    0
  • ringa_lee

    ringa_lee2017-04-17 11:45:14

    在拋出異常後呼叫棧記憶體物件的析構函數,在C++標準裡有規定。析構函數本來就不是明確呼叫的,編譯器和運行環境自然知道什麼時候該呼叫析構函數,只要它們是依照C++標準實現的。至於如何實現,我想也不難吧,只需要給初始化過的物件做個標記,處理異常的時候逐一呼叫它們的析構函數不就好了。

    回覆
    0
  • 天蓬老师

    天蓬老师2017-04-17 11:45:14

    引用維基百科的描述,講的比我們解釋的清楚,黑體是我加的:

    throw

    throw是一個C++關鍵字,與其後面的運算元構成了throw語句,在語法上類似return語句。 throw語句必須被包含在try區塊之中;可以是被包含在呼叫棧的外層函數的try中。

    執行throw語句時,其操作數的結果作為對像被複製構造為一個新的對象,放在內存的特殊位置(既不是堆也不是棧,Windows上是放在“線程信息塊TIB”中)。這個新的物件由本級的try所對應的catch語句逐一做類型匹配;如果匹配不成功,則與本函數的外層catch語句依次做類型匹配;如果在本函數內不能與catch語句匹配成功,則遞歸回退到呼叫棧的上一層函數內從函數呼叫點開始繼續與catch語句相符。重複這個過程直到與某個catch語句匹配成功或直到主函數main()都不能處理該異常。

    因此,throw語句拋出的異常物件不同於一般的局部物件。一般的局部物件會在其作用域結束時被析構。而throw語句拋出的異常物件駐留在所有可能被啟動的catch語句都能存取到的記憶體空間中。

    throw語句拋出的異常物件在符合成功的catch語句的結束處被析構(即使該catch語句使用的是非「引用」的傳值參數類型)。

    由於throw語句都進行了一次副本拷貝,因此異常物件應該是可以copy建構的。 但對於Microsoft Visual C++編譯器,異常物件的複製建構函式即使私有的情形,異常物件仍然可以被throw語句正常拋出;但在catch語句的參數是傳值時,在catch語句處編譯報錯:

    cannot be caught as the destructor and/or copy constructor are inaccessible”。
    

    拋出一個表達式時,被拋出物件的靜態編譯時類型將決定異常物件的類型。

    棧展開

    堆疊展開(unwinding)是指目前的try...catch...區塊匹配成功或符合不成功異常物件後,從try區塊內異常物件的拋出位置,到try區塊的開始處的所有已經執行了各自構造函數的局部變量,並按照構造生成順序的逆序,依序被析構。 如果當前函數內對拋出的異常物件匹配不成功,則從最外層的try語句到當前函數體的起始位置處的局部變數也依次被逆序析構,實現棧展開,然後再回退到呼叫堆疊的上一層函數內從函數呼叫點開始繼續處理該異常。

    catch語句如果匹配異常物件成功,在完成了對catch語句的參數的初始化(對傳值參數完成了參數物件的copy構造)之後,對同層級的try區塊執行棧展開。

    由於執行緒執行時,被呼叫的函數的參數、返回位址、局部變數等都是依函數呼叫次序保存在函數呼叫堆疊(即執行緒運行時堆疊)上。目前被調用函數的參數、局部變量名字可以覆蓋掉早前調用函數的同名變量,看起來就是只有當前函數內的名字可以訪問,早前調用的函數內部的名字都不可訪問,就像磁帶被“捲起」。 異常處理時依照函數呼叫順序的逆序析構,依序析構各個被調函數的局部變量,就類似把已經捲起的「磁帶」再展開,抹去上面記錄的數據,故此「棧展開」得名。

    回覆
    0
  • 取消回覆