C++ 异常 与 ”为什么析构函数不能抛出异常“ 问题
C++ 用異常使得可以將正常執行代碼和出錯處理區別開來。 比如一個棧,其為空時,調用其一個pop 函數,接下來怎么辦? 棧本身并不知道該如何處理,需要通知給其調用者(caller),因為只有調用者清楚接下來該怎么做。 異常,就提供了一個很好機制。 但是異常需要操作系統,編譯器,RTTI的特性支持。
下面圍繞一個問題?“為什么析構函數不能拋出異常?”?展開C++中異常的實現。
Effective C++ 里面有一條”別讓異常逃離析構函數“,大意說是Don't do that, otherwise the behavior is undefined. 這里討論一下從異常的實現角度,討論一下為什么不要 ?
1. 函數調用框架和SEH( Structure Error Handling)
?? 程序
1 int widget( int a, int b) 2 { 3 return a + b; 4 } 5 6 ?int bar(int a, int b) 7 { 8 int c = widget(a, b); 9 return c; 10 } 11 12 ?int foo( int a, int b) 13 { 14 int c=bar(a, b); 15 return c; 16 } 17 18 ?int main() 19 { 20 foo( 1, 2); 21 }?? 其匯編代碼
1 PUBLIC ?widget@@YAHHH@Z ; widget 2 ; COMDAT ?widget@@YAHHH@Z 3 ?_TEXT SEGMENT 4 _a$ = 8 ; size = 4 5 ?_b$ = 12 ; size = 4 6 ??widget@@YAHHH@Z PROC ; widget, COMDAT 7 ? 8 ?; 4 : { 9 ? 10 ?push ebp 11 ?mov ebp, esp 12 ?sub esp, 192 ; 000000c0H 13 ?push ebx 14 ?push esi 15 ?push edi 16 ?lea edi, DWORD PTR [ebp-192] 17 ?mov ecx, 48 ; 00000030H 18 ?mov eax, -858993460 ; ccccccccH 19 ?rep stosd 20 21 ?; 5 : return a + b; 22 ? 23 ?mov eax, DWORD PTR _a$[ebp] 24 ?add eax, DWORD PTR _b$[ebp] 25 26 ?; 6 : } 27 ? 28 ?pop edi 29 ?pop esi 30 ?pop ebx 31 ?mov esp, ebp 32 ?pop ebp 33 ?ret 0 34 ?widget@@YAHHH@Z ENDP ; widget 35 ?_TEXT ENDS 36 PUBLIC ?bar@@YAHHH@Z ; bar 37 ?EXTRN __RTC_CheckEsp:PROC 38 ?; Function compile flags: /Odtp /RTCsu /ZI 39 ; COMDAT ?bar@@YAHHH@Z 40 ?_TEXT SEGMENT 41 _c$ = -8 ; size = 4 42 ?_a$ = 8 ; size = 4 43 ?_b$ = 12 ; size = 4 44 ??bar@@YAHHH@Z PROC ; bar, COMDAT 45 ? 46 ?; 9 : { 47 ? 48 ?push ebp 49 ?mov ebp, esp 50 ?sub esp, 204 ; 000000ccH 51 ?push ebx 52 ?push esi 53 ?push edi 54 ?lea edi, DWORD PTR [ebp-204] 55 ?mov ecx, 51 ; 00000033H 56 ?mov eax, -858993460 ; ccccccccH 57 ?rep stosd 58 59 ?; 10 : int c = widget(a, b); 60 ? 61 ?mov eax, DWORD PTR _b$[ebp] 62 ?push eax 63 ?mov ecx, DWORD PTR _a$[ebp] 64 ?push ecx 65 ?call ?widget@@YAHHH@Z ; widget 66 ?add esp, 8 67 ?mov DWORD PTR _c$[ebp], eax 68 69 ; 11 : return c; 70 71 mov eax, DWORD PTR _c$[ebp] 72 73 ; 12 : } 74 75 pop edi 76 pop esi 77 pop ebx 78 add esp, 204 ; 000000ccH 79 cmp ebp, esp 80 call __RTC_CheckEsp 81 mov esp, ebp 82 pop ebp 83 ret 0 84 ?bar@@YAHHH@Z ENDP ; bar 85 _TEXT ENDS 86 PUBLIC ?foo@@YAHHH@Z ; foo 87 ; Function compile flags: /Odtp /RTCsu /ZI 88 ; COMDAT ?foo@@YAHHH@Z 89 _TEXT SEGMENT 90 _c$ = -8 ; size = 4 91 _a$ = 8 ; size = 4 92 _b$ = 12 ; size = 4 93 ?foo@@YAHHH@Z PROC ; foo, COMDAT 94 95 ; 15 : { 96 97 push ebp 98 mov ebp, esp 99 sub esp, 204 ; 000000ccH 100 push ebx 101 push esi 102 push edi 103 lea edi, DWORD PTR [ebp-204] 104 mov ecx, 51 ; 00000033H 105 mov eax, -858993460 ; ccccccccH 106 rep stosd 107 108 ; 16 : int c=bar(a, b); 109 110 mov eax, DWORD PTR _b$[ebp] 111 push eax 112 mov ecx, DWORD PTR _a$[ebp] 113 push ecx 114 call ?bar@@YAHHH@Z ; bar 115 add esp, 8 116 mov DWORD PTR _c$[ebp], eax 117 118 ; 17 : return c; 119 120 mov eax, DWORD PTR _c$[ebp] 121 122 ; 18 : } 123 124 pop edi 125 pop esi 126 pop ebx 127 add esp, 204 ; 000000ccH 128 cmp ebp, esp 129 call __RTC_CheckEsp 130 mov esp, ebp 131 pop ebp 132 ret 0 133 ?foo@@YAHHH@Z ENDP ; foo 134 _TEXT ENDS 135 PUBLIC _main 136 ; Function compile flags: /Odtp /RTCsu /ZI 137 ; COMDAT _main 138 _TEXT SEGMENT 139 _main PROC ; COMDAT 140 141 ; 21 : { 142 143 push ebp 144 mov ebp, esp 145 sub esp, 192 ; 000000c0H 146 push ebx 147 push esi 148 push edi 149 lea edi, DWORD PTR [ebp-192] 150 mov ecx, 48 ; 00000030H 151 mov eax, -858993460 ; ccccccccH 152 rep stosd 153 154 ; 22 : 155 ; 23 : foo( 1, 2); 156 157 push 2 158 push 1 159 call ?foo@@YAHHH@Z ; foo 160 add esp, 8 161 162 ; 24 : } 163 164 xor eax, eax 165 pop edi 166 pop esi 167 pop ebx 168 add esp, 192 ; 000000c0H 169 cmp ebp, esp 170 call __RTC_CheckEsp 171 mov esp, ebp 172 pop ebp 173 ret 0 174 _main ENDP 175 _TEXT ENDS 176 END?? 調用框架?
?2。? 加入SEH 之后,函數調用框架稍微修改一下: 對每一個函數加入一個Exception_Registration 的鏈表,鏈表頭存放在FS:[0] 里面。當異常拋出時,就去遍歷該鏈表找到合適的catch 塊。 對于每一個Exception_Registration 存放鏈表的上一個節點,異常處理函數( Error Handler). 用來處理異常。 這些結構都是編譯器加上的,分別在函數調用的prologue 和epilogue ,注冊和注銷 一個異常處理節點。
?NOTE: error handling
1. 當異常發生時,系統得到控制權,系統從FS:[0]寄存器取到異常處理鏈的頭,以及異常的類型, 調用異常處理函數。(異常函數是編譯器生成的)
2. 從鏈表頭去匹配 異常類型和catch 塊接收的類型。( 這里用到RTTI 信息)
3. unwind stack。這里需要析構已經創建的對象。( 這里需要判斷析構哪些對象,這一步是編譯器做的)
4. 執行catch 塊代碼。
后返回到程序的正常代碼,即catch塊下面的第一行代碼。
? 可見,在exception 找到對應的 Catche 塊后, 去棧展開(unwind stack),析構已有的對象后,進入到Catch 塊中。 問題是: 程序怎么知道程序運行到哪里? 哪些對象需要調用析構函數? 這也是編譯器做的,對于每一個Catch 塊,其記錄下如果該catch 塊若被調用,哪些對象需要被析構。 這有這么一張表。具體實現可以參見reference2.
3. 當析構拋出異常時,接下來的故事。
?? 實驗1:? Base 類的析構拋出異常;
1 class Base 2 { 3 public: 4 void fun() { throw 1; } 5 ~Base() { throw 2; } 6 }; 7 8 int main() 9 { 10 try 11 { 12 Base base; 13 //base.fun(); 14 } 15 catch (...) 16 { 17 //cout <<"get the catch"<<endl; 18 } 19 }???????? 運行沒有問題。
?? 實驗2: 打開上面注釋掉的第13行代碼(?//base.fun();?),再試運行,結果呢? 在debug 模式下彈出對話框
???
為什么呢?
因為SEH 是一個鏈表,鏈表頭地址存在FS:[0] 的寄存器里面。 在實驗2,函數base.fun先拋出異常,從FS:[0]開始向上遍歷 SHL 節點,匹配到catch 塊。 找到代碼里面為一個catch塊,再去展開棧,調用base 的析構函數,然而析構又拋出異常。 如果系統再去從SEL鏈表匹配,會改變FS:[0]值,這時候程序迷失了,不知道下面該怎么什么? 因為他已經丟掉了上一次異常鏈那個節點。
實驗3:如果析構函數的異常被處理呢, 程序還會正常運行嗎?
的確可以運行。
因為析構拋出來的異常,在到達上一層析構節點之前已經被別的catch 塊給處理掉。那么當回到上一層異常函數時, 其SEH 沒有變,程序可以繼續執行。
這也許就是為什么C++不支持異常中拋的異常。
4. 效率:
??? 當無異常拋出時,其開銷就是在函數調用的時候注冊/注銷 異常處理函數,這些開銷很小。
??? 但是當異常拋出時,其開銷就大了,編譯異常鏈,用RTTI比配類型,調用析構;但是比傳統的那種返回值,層層返回,效率也不會太差。 帶來好的好處是代碼好維護,減少出錯處理的重復代碼,并且與邏輯代碼分開。
??? 權衡一下,好處還是大大的:)
5. 總結一下流程:
? 為了安全,”析構函數盡可能的不要拋出異常“。
? 如果非拋不可,語言也提供了方法,就是自己的異常,自己給吃掉。但是這種方法不提倡,我們提倡有錯早點報出來。
Note:
1.同樣還有一個問題,”構造函數可以拋出異常么? 為什么?“
C++ 里面當構造函數拋出異常時,其會調用構造函數里面已經創建對象的析構函數,但是對以自己的析構函數沒有調用,就可能產生內存泄漏,比如自己new 出來的內存沒有釋放。
有兩個辦法。在Catch 塊里面釋放已經申請的資源 或者 用智能指針把資源當做對象處理。
Delphi 里面當構造函數拋異常時,在其執行Catch 代碼前,其先調用析構函數。
所以,構造拋出異常,是否調用析構函數,不是取決于技術,而是取決于語言的設計者。
2. 關于多線程,異常是線程安全的。 對于每一個線程都有自己的 Thread Info/Environment Block. 維護自己的SEH結構。
Reference:
1.http://www.codeproject.com/KB/cpp/exceptionhandler.aspx
2.http://baiy.cn/doc/cpp/inside_exception.htm
3.http://www.mzwu.com/article.asp?id=1469
總結
以上是生活随笔為你收集整理的C++ 异常 与 ”为什么析构函数不能抛出异常“ 问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: R-Tree空间索引算法的研究历程和最新
- 下一篇: C++ 内存对齐