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:如果析构函数的异常被处理呢, 程序还会正常运行吗?

1 class Base 2 { 3 public: 4 void fun() { throw 1; } 5 ~Base() 6 { 7 try 8 { 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 catch"<<endl; 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?id=1469

C++ 异常 与 ”为什么析构函数不能抛出异常“ 问题相关推荐

  1. 构造函数和析构函数中抛出异常

    文章目录 1 构造函数中抛出异常 2 析构函数中的异常 1 构造函数中抛出异常 如果构造函数中抛出异常会发生什么情况? 构造函数中抛出异常: 构造过程立即停止. 当前对象无法生成. 析构函数不会被调用 ...

  2. C++中构造函数和析构函数可以抛出异常吗?

    C++中构造函数和析构函数可以抛出异常吗? 一.  析构函数 参照<Effective C++>中条款08:别让异常逃离析构函数.  总结如下: 1. 不要在析构函数中抛出异常!虽然C++ ...

  3. C++ 析构函数不要抛出异常

    从语法上来说,析构函数可以抛出异常,但从逻辑上和风险控制上,析构函数中不要抛出异常,因为栈展开容易导致资源泄露和程序崩溃,所以别让异常逃离析构函数. 1.析构函数抛出异常的问题 析构函数从语法上是可以 ...

  4. 构造函数 和 析构函数 能否抛出异常

    构造函数和析构函数分别管理对象的建立和释放,负责对象的诞生和死亡的过程.当一个对象诞生时,构造函数负责创建并初始化对象的内部环境,包括分配内存.创建内部对象和打开相关的外部资源,等等.而当对象死亡时, ...

  5. Item 8:析构函数不要抛出异常 Effective C++笔记

    Item 8: Prevent exceptions from leaving destructors. 析构函数不要抛出异常 因为析构函数经常被自己主动调用,在析构函数中抛出的异常往往会难以捕获,引 ...

  6. python抛出异常 后如何接住,Python 异常的捕获、异常的传递与主动抛出异常操作示例...

    本文实例讲述了Python 异常的捕获.异常的传递与主动抛出异常操作.分享给大家供大家参考,具体如下: 异常的捕获 demo.py(异常的捕获): try: # 提示用户输入一个整数 num = in ...

  7. python异常捕获_Python 异常的捕获、异常的传递与主动抛出异常操作示例

    本文实例讲述了Python 异常的捕获.异常的传递与主动抛出异常操作.分享给大家供大家参考,具体如下: 异常的捕获 demo.py(异常的捕获): try: # 提示用户输入一个整数 num = in ...

  8. java 回滚异常_回滚事务并关闭抛出异常的连接

    目前在我的JavaEE应用程序服务器中使用本地和远程EJB,MDB(Singleton和Stateless),我正在使用JDBC-Transactions for Hibernate Core . 管 ...

  9. 异常(如何捕获和抛出异常,自定义异常)

    异常 实际工作中,遇到的情况不可能是非常完美的.比如:你写的某个模块,用户输入不一定符合你的要求,你的程序要大考某个文件,这个文件可能不存在或者文件格式不对,你要读取数据库的数据,数据可能是空的等,我 ...

最新文章

  1. vim 删除操作命令
  2. [Java基础]让Map value自增
  3. QT计算器功能的实现
  4. qhfl-6 购物车
  5. JAVA应用开发MQ实战最佳实践——Series2:消息队列RocketMQ性能测试案例
  6. Web 四种常见的POST提交数据方式
  7. Hql中使用in参数
  8. 一步一步学Silverlight 2系列(20):如何在Silverlight中与HTML DOM交互(下)
  9. vue mock模拟后台接口数据
  10. cad2010多个文件并排显示_并排查看Excel工作表只需1个小动作,工作效率大涨百倍!...
  11. mybatis 自定义插件的使用
  12. python根据excel数据生成柱状图并导出成图片格式
  13. CSS做3D旋转魔方(立方体)
  14. 【CF335 E】Counting Skyscrapers
  15. 7-3 将整数转换为汉字大写数字
  16. 开源:MIS金质打印通原理及实现 Step by step (1)
  17. javaFx屏幕截图工具
  18. 关于iOS中图片处理的小结
  19. 如何用java语言编写小游戏?(贪吃蛇)
  20. svn和git不过是工具而已

热门文章

  1. Java的文件流定义,java文件流的问题!急
  2. apache geode项目结构_Apache Flink-基于Java项目模板创建Flink应用(流计算和批计算)...
  3. 鲸鱼优化算法_盘点 35 个 Java 代码优化细节
  4. listview控件在php的使用方法,Android_Android编程之控件ListView使用方法,本文实例讲述了Android编程之控 - phpStudy...
  5. 光纤收发器不同品牌之间的兼容性互通
  6. 【渝粤教育】国家开放大学2018年秋季 2302T供应链管理 参考试题
  7. [渝粤教育] 中国地质大学 电力系统保护原理 复习题 (2)
  8. 奥鹏计算机基础18秋在线作业答案,18秋华师《计算机基础》在线作业1(标准答案).doc...
  9. asp 取数据 计算_ASP.NET Core 简介
  10. wifi定位算法android,WIFI定位算法