C 異常丟擲機制

2021-06-22 23:13:17 字數 4900 閱讀 6228

乙個程序中可以同時包含多個執行緒。

我們通常認為執行緒是作業系統

可識別的最小併發執行和排程單位(不要跟俺說還有 green thread 或者 fiber,os kernel 不認識也不參與這些物件的排程)。

同一程序中的多個執行緒共享**段(**和常量)、資料

段(靜態和全域性變數)和擴充套件段(堆儲存),但是每個執行緒有自己的棧段。 棧段又叫執行時棧,用來存放所有區域性變數和臨時變數(引數、返回值、臨時構造的變數等)。這一條對下文中的某些概念來說是非常重要的,但是請注意,這裡提 到的各個「段」都是邏輯上的說法,在物理上某些硬體架構或者作業系統可能不使用段式儲存。不過沒關係,編譯器會保證這些邏輯概念和假設的前提條件對每個 c/c++ 程式設計師來說始終是成立的。

由於共享了除棧以外的所有記憶體位址

段,執行緒不可以有自己的「靜態」或「全域性」變數,為了彌補這一缺憾,作業系統通常會提供一種稱為tls(thread local storage,即:「執行緒本地儲存」)的機制。通過該機制可以實現類似的功能

。tls 通常是執行緒控制塊(tcb)中的某個指標所指向的乙個指標陣列,陣列中的每個元素稱為乙個槽(slot),每個槽中的指標由使用者定義,可以指向任意位置(但通常是指向堆儲存中的某個偏移)。

:編譯器如何實現函式的呼叫和返回。一般來說,編譯器會為當前呼叫棧裡的每個函式建立乙個棧框架(stack frame)。「棧框架」擔負著以下重要任務

傳遞引數:通常,函式的呼叫引數總是在這個函式棧框架的最頂端。

呼叫者的當前棧指標:便於清理被呼叫者的所有區域性變數、並恢復呼叫者的現場。

當前函式內的所有區域性變數:記得嗎?剛才說過所有區域性和臨時變數都是儲存在棧上的。

圖1 函式呼叫棧框架示例

正如上圖所示的那樣,隨著函式被逐級呼叫,編譯器會為每乙個函式建立自己的棧框架,棧空間

逐漸消耗。隨著函式的逐級返回,該函式的棧框架也將被逐級銷毀,棧空間得以逐步釋放。順便說一句, 遞迴函式的巢狀呼叫深度通常也是取決於執行時棧空間的剩餘尺寸。

這裡順便解釋另乙個術語

:呼叫約定(calling convention)。呼叫約定通常指:呼叫者將引數壓入棧中(或放入暫存器中)的順序,以及返回時由誰(呼叫者還是被呼叫者)來清理這些引數等細節規程方面的約定。

最後再說一句,這裡所展示的函式呼叫乃是最「經典

」的方式。實際情況是:在開啟了優化選項後,編譯器可能不會為乙個內聯甚至非內聯的函式生成棧框架,編譯器可能使用很多優化技術

該函式可能會直接或間接地丟擲乙個異常:即該函式的定義存放在乙個 c++ 編譯(而不是傳統 c)單元內,並且該函式沒有使用「throw()」異常過濾器。

或者該函式的定義內使用 try 塊。

圖2 c++函式呼叫棧框架示例

由圖2可見,在每個 c++ 函式的棧框架中都多了一些東西。仔細觀察的話,你會發現,多出來的東西正好是乙個 exp 型別的結構體。進一步分析就會發現,這是乙個典型的單向鍊錶式結構:

圖5 c++ 異常丟擲

在編譯一段 c++ **時,編譯器會將所有 throw 語句替換為其 c++ 執行時庫中的某一指定函式,這裡我們叫它__cxxrtthrowexp(與本文提到的所有其它資料結構和屬性名一樣,在實際應用中它可以是任意名稱)。該函式接收乙個編譯器認可的內部結構(我們叫它exception結 構)。這個結構中包含了待丟擲異常物件的起始位址、用於銷毀它的析構函式,以及它的 type_info 資訊。對於沒有啟用 rtti 機制(編譯器禁用了 rtti 機制或沒有在類層次結構中使用虛表)的異常類層次結構,可能還要包含其所有基類的 type_info 資訊,以便與相應的 catch 塊進行匹配。

在圖5中的深灰色框圖內,我們使用 c++ 偽**展示了函式 funca 中的 「throw myexp(1);」 語句將被編譯器最終翻譯成的樣子。實際上在多數情況下,__cxxrtthrowexp函式即我們前面曾多次提到的「異常處理器」,異常捕獲和棧回退等各項重要工作都由它來完成。

__cxxrtthrowexp首先接收(並儲存)exception物件;然後從tls:current exphdl處找到與當前函式對應的 pihandler、nstep 等異常處理相關資料;並按照前文所述的機制完成異常捕獲和棧回退。由此完成了包括「丟擲」->「捕獲」->「回退」等步驟的整套異常處理機制。

windows

由於可直接借助作業系統提供的機制,所以簡化了 c++ 異常處理器的實現。

使「catch (...)」 塊得以捕獲作業系統產生的異常(如:「記憶體訪問違例」等等)。

使作業系統的異常處理機制能夠捕獲所有 c++ 異常。實際上,大多數 windows 下的 c++ 編譯器的異常機制均使用這種方式實現。

。我在本文的開頭曾提到,作為一名 c++ 程式設計師,了解其某一特性的實現原理主要是為了避免錯誤地使用該特性。要達到這個目的,還要在了解實現原理的基礎

上進行一些額外的開銷分析工作:特性

時間開銷

空間開銷ehdl無執行時開銷每「c++函式」乙個 ehdl 物件,其中的 tbltryblocks 成員僅在函式中包含至少乙個 try 塊時使用。典型情況下小於 64 位元組。  

c++棧框架極高的 o(1) 效率,每次呼叫時進行3次額外的整形賦值和一次 tls 訪問。每 呼叫兩個指標和乙個整形開銷。典型情況下小於 16 位元組。  

step 跟蹤極高的 o(1) 效率每次進出 try 塊或物件構造/析構一次整形立即數賦值。無(已記入 c++ 棧框架中的相應專案)。  

異常的丟擲、捕獲和棧回退異常的丟擲是一次 o(1) 級操作。在單個函式中進行捕獲和棧回退也均為 o(1) 操作。 但異常捕獲的總體成本為 o(m),其中 m 等於當前函式呼叫棧中,從丟擲異常的位置到達匹配 catch 塊之間所經過的函式呼叫中,包含 try 塊(即:定義了有效 tbltryblocks)的函式個數。

棧回退的總成本為 o(n),其中 n 等於當前函式呼叫棧中,從丟擲異常的位置到達匹配 catch 塊之間所經過的函式呼叫數。

在異常處理結束前,需儲存異常物件及其析構函式指標和相應的 type_info 訊息。 具體根據物件尺寸、編譯器選項(是否開啟 rtti)及異常捕獲器的引數傳遞方式(傳值或傳址)等因素有較大變化。典型情況下小於 256 位元組。

可 以看出,在沒有丟擲異常時,c++ 的異常處理機制是十分有效的。在有異常被丟擲後,可能會依當前函式呼叫棧的情形進行若干次整形比較(try塊表匹配)操作,但這通常不會超過幾十次。對於 大多數 10 年前的 cpu 來說,整形比較也只需 1 時鐘週期,所以異常捕獲的效率還是很高的。棧回退的效率則與 return 語句基本相當。

考慮到即使是傳統的函式呼叫、錯誤處理和逐級返回機制也不是沒有代價的。這些開銷在絕大多數情形下仍可以接受。空間開銷方面,每「c++ 函式」乙個 ehdl 結構體的引入在某些極端情形下會明顯增加目標檔案

尺寸和記憶體開銷。但是典型情況下,它們的影響並不大,但也沒有小到可以完全忽略的程度。如果 正在為乙個資源嚴格受限的環境開發應用程式,你可能需要考慮關閉異常處理和 rtti 機制以節約儲存空間。

以上討論的是一種典型的異常機制的實現方式,各具體編譯器廠商可能有自己的優化和改進方案

,但總體的出入不會很大。

piprev 成員指向鍊錶的上乙個節點,它主要用於在函式呼叫棧中逐級向上尋找匹配的 catch 塊,並完成棧回退工作。

pihandler 成員指向完成異常捕獲和棧回退所必須的資料結構(主要是兩張記載著關鍵

資料的表:「try」塊表:tbltryblocks及「棧回退表」:tblunwind)。

圖4 c++ 異常捕獲機制

在上一小節中,我們已經看到了nstep變 量在跟蹤物件構造、析構方面的作用。實際上 nstep 除了能夠跟蹤物件建立、銷毀階段以外,還能夠標識當前執行點是否在 try 塊中,以及(如果當前函式有多個 try 塊的話)究竟在哪個 try 塊中。這是通過在每乙個 try 塊的入口和出口各為 nstep 賦予乙個唯一 id 值,並確保 nstep 在對應 try 塊內的變化恰在此範圍之內來實現的。

在具體實現異常捕獲時,首先,c++ 異常處理器檢查發生異常的位置是否在當前函式的某個 try 塊之內。這項工作可以通過將當前函式的 nstep 值依次在pihandler指向tbltryblocks表的條目中進行範圍為 [nbeginstep, nendstep) 的比對來完成。

例如:若圖4 中的 funcb 在 nstep == 2 時發生了異常,則通過比對 funcb 的 tbltryblocks 表發現 2∈[1, 3),故該異常發生在 funcb 內的第乙個 try 塊中。

其次,如果異常發生的位置在當前函式中的某個 try 塊內,則嘗試匹配該tbltryblocks相應條目中的tblcatchblocks表。tblcatchblocks表中記錄了與指定 try 塊配套出現的所有 catch 塊相關資訊,包括這個 catch 塊所能捕獲的異常型別及其起始位址等資訊。

若找到了乙個匹配的 catch 塊,則複製當前異常物件到此 catch 塊,然後跳轉到其入口位址執行塊內**。

否則,則說明異常發生位置不在當前函式的 try 塊內,或者這個 try 塊中沒有與當前異常相匹配的 catch 塊,此時則沿著函式棧框架中piprev所指位址(即:異常處理鏈中的上乙個節點)逐級重複以上過程,直至找到乙個匹配的 catch 塊或到達異常處理鏈的首節點。對於後者,我們稱為發生了未捕獲的異常,對於 c++ 異常處理器而言,未捕獲的異常是乙個嚴重錯誤,將導致束當前程序被強制結束。

注意: 雖然在圖4示例中的 tbltryblocks 只有乙個條目,這個條目中的 tblcatchblocks 也只有一行。但是在實際情況中,這兩個表中都允許用多條記錄。意即:乙個函式中可以有多個 try 塊,每個 try 塊後均可跟隨多個與之配套的 catch 塊。

注意:按照標準意義上的理解,異常時的棧回退是伴隨著異常捕獲過程沿著異常處理 鏈逐層向上進行的。但是有些編譯器是在先完成異常捕獲後再一次性進行棧回退的。無論具體實現使用了哪種方式,除非正在開發乙個記憶體嚴格受限的嵌入式應用, 通常我們按照標準語意來理解都不會產生什麼問題。

備註:實際上 tblcatchblocks 中還有一些較為關鍵但被故意省略的字段。比如指明該 catch 塊異常物件複製方式(傳值(拷貝構造)或傳址(引用或指標))的字段,以及在何處存放被複製的異常物件(相對於入口位址的偏移位置)等資訊。

c 異常丟擲和接收機制

c 語言中定義了一套關於異常的丟擲throw 和接收catch機制 1 其中丟擲的異常型別可以是乙個類物件,某一型別的資料等,如,丟擲某一型別的資料 int x throw x 等 丟擲某一類的物件 throw exception 2 丟擲異常的位置,可以在try中寫,如 trycatch exce...

c 丟擲標準異常

可以在自己的程式中丟擲某些標準異常。丟擲標準異常時,只需生成乙個描述該異常的字串,交給異常物件,它將成為what 返回的描述字串。std strings throw std out of range s throw std out of range out of range somewhere,so...

異常的處理機制 捕獲和丟擲

jvm 預設是如何處理異常的呢?main函式收到乙個問題,有兩種處理方式 1.自己解決 2.自己解決不了,交給jvm解決 jvm有乙個預設的異常處理機制,就是將該異常顯示出來 包括 異常名稱 資訊 出現位置 異常的兩種處理方式 1.try catch finally 捕獲並處理 try catch ...