网站开发开发优势,做运营有前途吗,服务企业,做网站 图片侵权C 用异常使得可以将正常执行代码和出错处理区别开来。 比如一个栈#xff0c;其为空时#xff0c;调用其一个pop 函数#xff0c;接下来怎么办#xff1f; 栈本身并不知道该如何处理#xff0c;需要通知给其调用者#xff08;caller#xff09;#xff0c;因为只有调用… C 用异常使得可以将正常执行代码和出错处理区别开来。 比如一个栈其为空时调用其一个pop 函数接下来怎么办 栈本身并不知道该如何处理需要通知给其调用者caller因为只有调用者清楚接下来该怎么做。 异常就提供了一个很好机制。 但是异常需要操作系统编译器RTTI的特性支持。 下面围绕一个问题 “为什么析构函数不能抛出异常” 展开C中异常的实现。 Effective C 里面有一条”别让异常逃离析构函数“大意说是Dont 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 cbar(a, b);
15 return c;
16 }
17
18 int main()
19 {
20 foo( 1, 2);
21 } 其汇编代码 1 PUBLIC ?widgetYAHHHZ ; widget2 ; COMDAT ?widgetYAHHHZ3 _TEXT SEGMENT4 _a$ 8 ; size 45 _b$ 12 ; size 46 ?widgetYAHHHZ PROC ; widget, COMDAT7 8 ; 4 : {9 10 push ebp11 mov ebp, esp12 sub esp, 192 ; 000000c0H13 push ebx14 push esi15 push edi16 lea edi, DWORD PTR [ebp-192]17 mov ecx, 48 ; 00000030H18 mov eax, -858993460 ; ccccccccH19 rep stosd20 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 edi29 pop esi30 pop ebx31 mov esp, ebp32 pop ebp33 ret 034 ?widgetYAHHHZ ENDP ; widget35 _TEXT ENDS36 PUBLIC ?barYAHHHZ ; bar37 EXTRN __RTC_CheckEsp:PROC38 ; Function compile flags: /Odtp /RTCsu /ZI39 ; COMDAT ?barYAHHHZ40 _TEXT SEGMENT41 _c$ -8 ; size 442 _a$ 8 ; size 443 _b$ 12 ; size 444 ?barYAHHHZ PROC ; bar, COMDAT45 46 ; 9 : {47 48 push ebp49 mov ebp, esp50 sub esp, 204 ; 000000ccH51 push ebx52 push esi53 push edi54 lea edi, DWORD PTR [ebp-204]55 mov ecx, 51 ; 00000033H56 mov eax, -858993460 ; ccccccccH57 rep stosd58 59 ; 10 : int c widget(a, b);60 61 mov eax, DWORD PTR _b$[ebp]62 push eax63 mov ecx, DWORD PTR _a$[ebp]64 push ecx65 call ?widgetYAHHHZ ; widget66 add esp, 867 mov DWORD PTR _c$[ebp], eax68 69 ; 11 : return c;70 71 mov eax, DWORD PTR _c$[ebp]72 73 ; 12 : }74 75 pop edi76 pop esi77 pop ebx78 add esp, 204 ; 000000ccH79 cmp ebp, esp80 call __RTC_CheckEsp81 mov esp, ebp82 pop ebp83 ret 084 ?barYAHHHZ ENDP ; bar85 _TEXT ENDS86 PUBLIC ?fooYAHHHZ ; foo87 ; Function compile flags: /Odtp /RTCsu /ZI88 ; COMDAT ?fooYAHHHZ89 _TEXT SEGMENT90 _c$ -8 ; size 491 _a$ 8 ; size 492 _b$ 12 ; size 493 ?fooYAHHHZ PROC ; foo, COMDAT94 95 ; 15 : {96 97 push ebp98 mov ebp, esp99 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 cbar(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 ?barYAHHHZ ; 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 ?fooYAHHHZ 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 ?fooYAHHHZ ; 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 Base2 {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 catchendl;
18 }
19 } 运行没有问题。 实验2: 打开上面注释掉的第13行代码 //base.fun(); 再试运行结果呢 在debug 模式下弹出对话框 为什么呢 因为SEH 是一个链表链表头地址存在FS:[0] 的寄存器里面。 在实验2函数base.fun先抛出异常从FS:[0]开始向上遍历 SHL 节点匹配到catch 块。 找到代码里面为一个catch块再去展开栈调用base 的析构函数然而析构又抛出异常。 如果系统再去从SEL链表匹配会改变FS:[0]值这时候程序迷失了不知道下面该怎么什么 因为他已经丢掉了上一次异常链那个节点。 实验3如果析构函数的异常被处理呢 程序还会正常运行吗 1 class Base2 {3 public:4 void fun() { throw 1; }5 ~Base() 6 {7 try8 {9 throw 2;
10 }
11 catch (int e)
12 {
13 // do something
14 }
15 }
16 };
17
18 int main()
19 {
20 try
21 {
22 Base base;
23 //base.fun();
24 }
25 catch (...)
26 {
27 //cout get the catchendl;
28 }
29 } 的确可以运行。 因为析构抛出来的异常在到达上一层析构节点之前已经被别的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?id1469