增强错误恢复能力是提高代码健壮性的最有力途径之一

之所以平时编写代码的时候不愿意去写错误处理,主要是由于这项工作及其无聊并可能导致代码膨胀,导致的结果就是本来就比较复杂的程序变得更加复杂。当然了,前面的缘由主要是针对C语言的,原因就在于C语言的‘紧耦合’性,必须在接近函数调用的地方使用错误处理,当然会增加复杂性了。

1.传统的错误处理(主要是针对C语言的方法)

1)函数中返回错误信息,或者设置一个全局的错误状态。导致的问题就和前面说到的一样,代码数量的爆炸,而且,从一个错误的函数中返回的东西本身也没什么意义。

2)使用鲜为人知的信号处理。由函数signal()和函数raise()。当然了,这样的话耦合度还是相当的高。

3)使用标准库中非局部跳转函数:setjump()和longjump(), 使用setjump()可以保存程序中已知的一个无错误状态,一旦发生错误,可以使用longjump()返回到该状态

下面的代码演示了setjump()和longjump()的使用方法(用C++描述)

  1. /*
  2. 对函数setjmp(),如果直接调用,便会将当前处理器相关的信息保存到jmp_buf中并返回0
  3. 但如果使用同一个jmp_buf调用longjmp(),则函数就会返回到setjmp刚刚返回的地方
  4. 这次的返回值是longjmp的第二个参数
  5. 与goto语句的差别是,使用longjmp()可以返回任何预先确定的位置
  6. */
  7. #include <iostream>
  8. #include <csetjmp>
  9. using namespace std;
  10.  
  11. class Rainbow
  12. {
  13. public:
  14. Rainbow(){cout<<"Rainbow()"<<endl;}
  15. ~Rainbow(){cout<<"~Rainbow()"<<endl;}
  16. };
  17.  
  18. jmp_buf kansas;
  19.  
  20. void oz()
  21. {
  22. Rainbow rb;
  23. for(int i=0;i<3;i++)
  24. cout<<"there's no place like home"<<endl;
  25. longjmp(kansas,47);
  26. }
  27.  
  28. int main()
  29. {
  30. if(setjmp(kansas)==0)
  31. {
  32. cout<<"toenado,witch,munchkins..."<<endl;
  33. oz();
  34. }
  35. else
  36. {
  37. cout<<"Auntie Em!"<<"I had the strangest dream..."<<endl;
  38. }
  39. return 0;
  40. }

程序的运行结果如下:

可以看到,程序并没有调用类的析构函数,而这样本身就是异常现象(C++定义的),所以,这些函数不适合C++。

2.抛出异常

当代码出现异常的时候,可以创建一个包含错误信息的对象并抛出当前语境,如下:

  1. #include <iostream>
  2.  
  3. using namespace std;
  4.  
  5. class MyError
  6. {
  7. const char* const data;
  8. public:
  9. MyError(const char* const msg=0):data(msg){}
  10. };
  11.  
  12. void f()
  13. {
  14. throw MyError("Something bad happen");
  15. }
  16.  
  17. /*
  18. 当然了。这里没有使用try,程序会报错
  19. */
  20. int main()
  21. {
  22. f();
  23. return 0;
  24. }

throw首先会创建程序所抛出对象的一个拷贝,包含throw表达式的函数返回了这个对象,异常发生之前所创建的局部对象被销毁,这种被称为“栈反解”。而程序员需要为每一种不同的异常抛出不同的对象。

3.捕获异常

就像前面所说的,如果一个函数通过throw出了一个对象,那么函数就会返回这个错误对象并退出。如果不想退出这个函数,,那么就可以设置一个try块。这个块被称作try的原因是程序需要在这里尝试调用各种函数。

当然,被抛出的异常会在某个地方被终止,这个地方就是异常处理器(catch)。

异常处理器紧跟在try之后,一旦某个异常被抛出,异常处理机制就会依次寻找参数类型与异常类型相匹配的异常处理器。找到后就会进入catch语句,于是系统就认为这个异常已经处理了。

下面通过对前面的setjump()和longjump()进行修改得到的程序:

  1. #include <iostream>
  2. using namespace std;
  3.  
  4. class Rainbow
  5. {
  6. public:
  7. Rainbow(){cout<<"Rainbow()"<<endl;}
  8. ~Rainbow(){cout<<"~Rainbow()"<<endl;}
  9. };
  10.  
  11. void oz()
  12. {
  13. Rainbow rb;
  14. for(int i=0;i<3;i++)
  15. cout<<"there's no place like home"<<endl;
  16. throw 47;
  17. }
  18.  
  19. int main()
  20. {
  21. try{
  22. cout<<"toenado,witch,munchkins..."<<endl;
  23. oz();
  24. }catch(int){
  25. cout<<"Auntie Em!"<<"I had the strangest dream..."<<endl;
  26. }
  27. return 0;
  28. }

程序的运行结果:


当执行throw语句时,程序的控制流程开始回溯,直到找到带有int参数的catch为止。程序在这里继续恢复执行。当然了,当程序从oz()中返回时,是会调用析构函数的。

在异常处理中有两个基本的模型:终止于恢复

终止:无论抛出了什么异常,程序都无法挽救,不需要返回发生异常的地方。

恢复:自动重新执行发生错误的代码。在C++中,必须显示的将程序的执行流程转移到错误发生的地方,通常是重新调用发生错误的函数,例如把try放到while循环中。

4.异常匹配

一个异常并不与其处理器完全相关,一个对象或者是指向派生类对象的引用都能与基类处理器匹配。最好是通过引用而不是通过值来匹配异常(防止再次拷贝)。如果一个指针被抛出,将使用通常的标准指针转换来匹配异常,但不会把一种异常类型自动转换为另一种异常类型:

  1. #include <iostream>
  2. using namespace std;
  3.  
  4. class Except1{};
  5.  
  6. class Except2
  7. {
  8. public:
  9. Except2(const Except1&){}
  10. };
  11.  
  12. void f(){throw Except1();}
  13.  
  14. /*这里的抛出的异常不会做隐式转换*/
  15. int main()
  16. {
  17. try{
  18. f();
  19. }catch(Except2&){
  20. cout<<"inside catch(Except2)"<<endl;
  21. }catch(Except1&){
  22. cout<<"inside catch(Except1)"<<endl;
  23. }
  24. return 0;
  25. }

下面的例子显示了基类的异常处理器怎样捕获派生类异常:

  1. #include <iostream>
  2. using namespace std;
  3.  
  4. class X
  5. {
  6. public:
  7. class Trouble{};
  8. class Small:public Trouble{};
  9. class Big:public Trouble{};
  10. void f(){throw Big();}
  11. };
  12.  
  13. /*
  14. 程序的结果就是捕获了第一个异常处理,因为第一个catch处理完了所有异常,所以其他catch不会继续处理
  15. */
  16. int main()
  17. {
  18. X x;
  19. try{
  20. x.f();
  21. }catch(X::Trouble&){
  22. cout<<"catch Trouble"<<endl;
  23. }catch(X::Small&){
  24. cout<<"catch Small"<<endl;
  25. }catch(X::Big&){
  26. cout<<"catch Big"<<endl;
  27. }
  28. return 0;
  29. }

一般来说,先捕获派生类的异常,最后捕获的是基类异常。

捕获所有异常:catch(...)可以捕获所有的异常。

重新抛出异常:需要释放某些资源时,例如网络连接或堆上的内存需要释放时,通常希望重新抛出一个异常(捕获异常之后,释放资源,然后重新抛出异常)

catch(...){

//释放一些资源

throw;

}

不捕获异常:无法匹配异常的话,异常就会传递到更高一层,直到能够处理这个异常。

1.terminate()函数

当没有任何一个层次的异常处理器能够处理异常时,这个函数就会调用。terminate()函数会调用abort()使函数终止,此时,函数不会调用正常的终止函数,析构函数不会执行。

2.set_terminate()函数

可以设置自己的terminate()函数

  1. #include <iostream>
  2. #include <exception>
  3. #include <stdlib.h>
  4. using namespace std;
  5.  
  6. void terminator()
  7. {
  8. cout<<"I'll be back!"<<endl;
  9. exit(0);
  10. }
  11.  
  12. /*set_terminate返回被替换的指向terminate()函数的指针
  13. 第一次调用时,返回的是指向原terminate函数的指针*/
  14. void (*old_terminate)()=set_terminate(terminator);
  15.  
  16. class Botch
  17. {
  18. public:
  19. class Fruit{};
  20. void f(){
  21. cout<<"Botch::f()"<<endl;
  22. throw Fruit();
  23. }
  24. ~Botch(){throw 'c';}
  25. };
  26.  
  27. /*
  28. 程序在处理一个异常的时候会释放在栈上分配的对象,这时,析构函数被调用,这时候产生了第二个异常
  29. 正是这个第二个以下航导致了terminate的调用
  30. */
  31. int main()
  32. {
  33. try{
  34. Botch b;
  35. b.f();
  36. }catch(...){
  37. cout<<"inside catch(...)"<<endl;
  38. }
  39. return 0;
  40. }

一般来说,不要在析构函数中抛出异常。

5.清理

C++的异常处理可以使得程序从正常的处理流程跳转到异常处理流程,此时,构造函数建立起来的所有对象,析构函数一定会被调用。

下面的例子展示了当构造函数没有正常结束是不会调用相关联的析构函数。

  1. #include <iostream>
  2.  
  3. using namespace std;
  4.  
  5. class Trace
  6. {
  7. static int counter;
  8. int objid;
  9. public:
  10. Trace(){
  11. objid=counter++;
  12. cout<<"construction Trace #"<<objid<<endl;
  13. if(objid==3)
  14. throw 3;
  15. }
  16. ~Trace(){
  17. cout<<"destruction Trace #"<<objid<<endl;
  18. }
  19. };
  20. int main()
  21. {
  22. try{
  23. Trace n1;
  24. Trace Array[5];
  25. Trace n2;
  26. }catch(int i){
  27. cout<<"caught "<<i<<endl;
  28. }
  29. return 0;
  30. }

如果一个对象的构造函数则执行时发生异常,那么这个对象的析构函数就不会被调用,因此,如果在构造函数中分配了资源却产生异常,析构函数是不能释放这些资源的。例如常说的“悬挂”指针。

下面是一个例子:

  1. #include <iostream>
  2. #include <cstddef>
  3. using namespace std;
  4.  
  5. class Cat
  6. {
  7. public:
  8. Cat(){cout<<"Cat()"<<endl;}
  9. ~Cat(){cout<<"~Cat()"<<endl;}
  10. };
  11.  
  12. /*这些语句用来模拟内存不足的情况,可以不用鸟他
  13. 但可以看到这里的new中抛出了一个异常*/
  14. class Dog
  15. {
  16. public:
  17. void* operator new(size_t sz){
  18. cout<<"allocating a Dog"<<endl;
  19. throw 47;
  20. }
  21. void operator delete(void* p){
  22. cout<<"deallocating a Dog"<<endl;
  23. ::operator delete(p);
  24. }
  25. };
  26.  
  27. class UseResources
  28. {
  29. Cat* bp;
  30. Dog* op;
  31. public:
  32. UseResources(int count=1){
  33. cout<<"UseResources()"<<endl;
  34. bp=new Cat[count];
  35. op=new Dog;
  36. }
  37. ~UseResources(){
  38. cout<<"~UseResources()"<<endl;
  39. delete [] bp;
  40. delete op;
  41. }
  42. };
  43.  
  44. int main()
  45. {
  46. try{
  47. UseResources ur(3);
  48. }catch(int){
  49. cout<<"inside handler"<<endl;
  50. }
  51. return 0;
  52. }

Resources的析构函数没有被调用,这是因为在构造函数的时候抛出了异常,这样,创建的Cat对象也无法被析构。

为了防止资源泄露,需要用以下方法防止不成熟的资源分配方式:

1、在构造函数中捕获异常,用于释放资源

2、在构造函数中分配资源,在析构函数中释放资源

这样使得资源的每一次分配都具有原子性,称为资源获得式初始化,使得对象对资源的控制的时间与对象的生命周期相等,下面对上述例子作一些修改:

  1. #include <iostream>
  2. #include <cstddef>
  3. using namespace std;
  4.  
  5. template<class T,int sz=1>
  6. class PWrap
  7. {
  8. T* ptr;
  9. public:
  10. class RangeeError{};
  11. PWrap(){
  12. ptr=new T[sz];
  13. cout<<"Pwrap constractor"<<endl;
  14. }
  15. ~PWrap(){
  16. delete[] ptr;
  17. cout<<"PWrap deconstracor"<<endl;
  18. }
  19. T& operator[](int i) throw(RangeeError){
  20. if(i>=0&&i<sz)
  21. return ptr[i];
  22. throw RangeeError();
  23. }
  24. };
  25.  
  26. class Cat
  27. {
  28. public:
  29. Cat(){cout<<"Cat()"<<endl;}
  30. ~Cat(){cout<<"~Cat()"<<endl;}
  31. void g(){}
  32. };
  33.  
  34. class Dog
  35. {
  36. public:
  37. void* operator new[](size_t sz){
  38. cout<<"allocating a Dog"<<endl;
  39. throw 47;
  40. }
  41. void operator delete[](void* p){
  42. cout<<"deallocating a Dog"<<endl;
  43. ::operator delete(p);
  44. }
  45. };
  46.  
  47. class UseResources
  48. {
  49. PWrap<Cat,3> cats;
  50. PWrap<Dog> dog;
  51. public:
  52. UseResources(){
  53. cout<<"UseResources()"<<endl;
  54. }
  55. ~UseResources(){
  56. cout<<"~UseResources()"<<endl;
  57. }
  58. void f(){cats[1].g();}
  59. };
  60.  
  61. int main()
  62. {
  63. try{
  64. UseResources ur;
  65. }catch(int){
  66. cout<<"inside handler"<<endl;
  67. }catch(...){
  68. cout<<"inside catch"<<endl;
  69. }
  70. return 0;
  71. }

这是运行结果:

使用这种方法与第一种的不同之处:使得每个指针都被嵌入到对象之中,这些对象的构造函数最先被调用,并且如果他们之中任何一个构造函数在抛出异常之前完成,那么这些对象的析构函数也会在栈反解的时候被调用。

程序中,operator[]使用了一个称作RangeeError的嵌套类,如果参数越界,那么就创建一个RangeeError的类型对象。

auto_ptr:

由于在C++中动态内存的分配非常频繁,所以C++提供了一个RALL封装类,用于指向分配的对内存:

  1. #include <iostream>
  2. #include <memory>
  3. #include <cstddef>
  4. using namespace std;
  5.  
  6. class TraceHeap
  7. {
  8. int i;
  9. public:
  10. static void* operator new(size_t siz){
  11. void* p=::operator new(siz);
  12. cout<<"Allocating TraceHeap object on the heap at address "<<p<<endl;
  13. return p;
  14. }
  15. static void operator delete(void* p){
  16. cout<<"Deleting TraceHeap object at address "<<p<<endl;
  17. ::operator delete(p);
  18. }
  19. TraceHeap(int i):i(i){}
  20. int getVal() const {return i;}
  21. };
  22.  
  23. int main()
  24. {
  25. auto_ptr<TraceHeap> pMyObject(new TraceHeap(5));
  26. cout<<pMyObject->getVal()<<endl;
  27. }

程序的运行结果为:

函数级的try块:

由于构造函数能够抛出异常,为了处理在对象的成员或者其基类子类被抛出的异常,可以把这些子对象的初始化放到try中:

  1. #include <iostream>
  2. using namespace std;
  3.  
  4. class Base
  5. {
  6. int i;
  7. public:
  8. class BaseExcept{};
  9. Base(int i):i(i){throw BaseExcept();}
  10. };
  11.  
  12. class Dirived:public Base
  13. {
  14. public:
  15. class DirivedExcept{
  16. const char* msg;
  17. public:
  18. DirivedExcept(const char* msg):msg(msg){}
  19. const char* what() const{return msg;}
  20. };
  21. Dirived(int j) try : Base(j){
  22. cout<<"this won't print"<<endl;
  23. }catch(BaseExcept&){
  24. throw DirivedExcept("Base subobject threw");
  25. }
  26. };
  27.  
  28. int main()
  29. {
  30. try{
  31. Dirived d(3);
  32. }catch(Dirived::DirivedExcept& d){
  33. cout<<d.what()<<endl;
  34. }
  35. }

C++编程技术之 异常处理(上)的更多相关文章

  1. 关于如何提高Web服务端并发效率的异步编程技术

    最近我研究技术的一个重点是java的多线程开发,在我早期学习java的时候,很多书上把java的多线程开发标榜为简单易用,这个简单易用是以C语言作为参照的,不过我也没有使用过C语言开发过多线程,我只知 ...

  2. Atitit.异步编程技术原理与实践attilax总结

    Atitit.异步编程技术原理与实践attilax总结 1. 俩种实现模式 类库方式,以及语言方式,java futuretask ,c# await1 2. 事件(中断)机制1 3. Await 模 ...

  3. Java Web编程技术学习要点及方向

    学习编程技术要点及方向亮点: 传统学习编程技术落后,应跟著潮流,要对业务聚焦处理.要Jar, 不要War:以小为主,以简为宝,集堆而成.去繁取简 Spring Boot,明日之春(future of ...

  4. javascript学习 真正理解DOM脚本编程技术背后的思路和原则

    本文学习来源于<javascriptDOM编程艺术>仅作笔记 学会怎样才能利用DOM脚本编程技术以一种既方便自己更体贴用户的方式去充实和完善你们的网页. 循序渐进:从最核心的内容开始,逐步 ...

  5. 转载--提高C++性能的编程技术

    读书笔记:提高C++性能的编程技术   第1章 跟踪范例 1.1 关注点 本章引入的实际问题为:定义一个简单的Trace类,将当前函数名输出到日志文件中.Trace对象会带来一定的开销,因此在默认情况 ...

  6. linux脚本编程技术

    linux脚本编程技术 一.什么是脚本 脚本是一个包含一系列命令序列的可执行(777)文本文件.当运行这个脚本文件时,文件中包含的命令序列将得到自动执行. 二.脚本编程 #!/bin/sh 首行固定格 ...

  7. 如何提高Web服务端并发效率的异步编程技术

    作为一名web工程师都希望自己做的web应用能被越来越多的人使用,如果我们所做的web应用随着用户的增多而宕机了,那么越来越多的人就会变得越来越少了,为了让我们的web应用能有更多人使用,我们就得提升 ...

  8. 转载:10个实用的但偏执的Java编程技术

    在沉浸于编码一段时间以后(比如说我已经投入近20年左右的时间在程序上了),你会渐渐对这些东西习以为常.因为,你知道的…… 任何事情有可能出错,没错,的确如此. 这就是为什么我们要采用“防御性编程”,即 ...

  9. 第一篇:GPU 编程技术的发展历程及现状

    前言 本文通过介绍 GPU 编程技术的发展历程,让大家初步地了解 GPU 编程,走进 GPU 编程的世界. 冯诺依曼计算机架构的瓶颈 曾经,几乎所有的处理器都是以冯诺依曼计算机架构为基础的.该系统架构 ...

随机推荐

  1. UITabBarController使用详解

    UITabBarController是IOS中很常用的一个viewController,例如系统的闹钟程序,ipod 程序等.UITabBarController通常作为整个程序的rootViewCo ...

  2. wcf 速成,转的啊 第一天

    作为WCF速成系列,只介绍些项目开发中常用到的实战知识. 学习wcf,还是对其中的几个术语要了解一下.wcf中有一个ABC的概念,就是 第一: "A" 是地址,就是告诉别人我wcf ...

  3. Android API Level在11前后及16之后时Notification的不同用法

    作为刚入门Android的小白,最近在按照郭大神的<第一行代码>在练习,在用到Notification时遇到了一些问题,网上资料比较零散,我这里做了一个总结分析给各位,若有错误,恳请指正~ ...

  4. JavaScript习题

    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding= ...

  5. WPF子界面向父界面传递带参数的委托

    需求如下: 父界面上有几个按钮,需要在点击按钮的时候向父界面传值,以知道该控制哪个界面显示. 父界面:WIN1 子界面:WIN2 此处或者说是子控件向父控件传值 1.子界面定义委托 WIN2里写: p ...

  6. JS 精粹( 函数)

    函数是对象,它与其它对象唯一的不同是它可以调用.函数可实现:代码复用.信息隐藏.代码组合调用. 建立函数时会建立:上下文.调用函数的代码.每个函数(除Function.prototype)都会有一个原 ...

  7. JS 精粹(三)

    (一)基本问题 JS的数据类型(不是数据结构)分:简单数据类型(undefined\null\boolean\string\number\symbol).复杂数据类型(object). 对象是可变的键 ...

  8. 64位ubuntu编译32位程序

      最近在64位ubuntu上开发,需要编译32位程序,需要安装这两个包,然后在编译器参数加上-m32.不放心的话可以用ldd或file查看一下是否生成了对应位数的程序. $ apt-get inst ...

  9. [C#参考]属性

    属性和字段不同,属性是一个函数成员:它提供灵活的机制来读取.编写或计算某个私有字段的值. 可以像使用公共数据成员一样使用属性,但实际上它们是称作“访问器”的特殊方法. 这使得可以轻松访问数据,此外还有 ...

  10. Linux学习之head命令

    head 与 tail 就像它的名字一样的浅显易懂,它是用来显示开头或结尾某个数量的文字区块,head 用来显示档案的开头至标准输出中,而 tail 想当然尔就是看档案的结尾. 1.命令格式: hea ...