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

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

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

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

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

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

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

/*
对函数setjmp(),如果直接调用,便会将当前处理器相关的信息保存到jmp_buf中并返回0
但如果使用同一个jmp_buf调用longjmp(),则函数就会返回到setjmp刚刚返回的地方
这次的返回值是longjmp的第二个参数
与goto语句的差别是,使用longjmp()可以返回任何预先确定的位置
*/
#include <iostream>
#include <csetjmp>
using namespace std;class Rainbow
{
public:Rainbow(){cout<<"Rainbow()"<<endl;}~Rainbow(){cout<<"~Rainbow()"<<endl;}
};jmp_buf kansas;void oz()
{Rainbow rb;for(int i=0;i<3;i++)cout<<"there's no place like home"<<endl;longjmp(kansas,47);
}int main()
{if(setjmp(kansas)==0){cout<<"toenado,witch,munchkins..."<<endl;oz();}else{cout<<"Auntie Em!"<<"I had the strangest dream..."<<endl;}return 0;
}

程序的运行结果如下:

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

2.抛出异常

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

#include <iostream>using namespace std;class MyError
{const char* const data;
public:MyError(const char* const msg=0):data(msg){}
};void f()
{throw MyError("Something bad happen");
}/*
当然了。这里没有使用try,程序会报错
*/
int main()
{f();return 0;
}

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

3.捕获异常

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

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

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

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

#include <iostream>
using namespace std;class Rainbow
{
public:Rainbow(){cout<<"Rainbow()"<<endl;}~Rainbow(){cout<<"~Rainbow()"<<endl;}
};void oz()
{Rainbow rb;for(int i=0;i<3;i++)cout<<"there's no place like home"<<endl;throw 47;
}int main()
{try{cout<<"toenado,witch,munchkins..."<<endl;oz();}catch(int){cout<<"Auntie Em!"<<"I had the strangest dream..."<<endl;}return 0;
}

程序的运行结果:


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

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

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

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

4.异常匹配

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

#include <iostream>
using namespace std;class Except1{};class Except2
{
public:Except2(const Except1&){}
};void f(){throw Except1();}/*这里的抛出的异常不会做隐式转换*/
int main()
{try{f();}catch(Except2&){cout<<"inside catch(Except2)"<<endl;}catch(Except1&){cout<<"inside catch(Except1)"<<endl;}return 0;
}

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

#include <iostream>
using namespace std;class X
{
public:class Trouble{};class Small:public Trouble{};class Big:public Trouble{};void f(){throw Big();}
};/*
程序的结果就是捕获了第一个异常处理,因为第一个catch处理完了所有异常,所以其他catch不会继续处理
*/
int main()
{X x;try{x.f();}catch(X::Trouble&){cout<<"catch Trouble"<<endl;}catch(X::Small&){cout<<"catch Small"<<endl;}catch(X::Big&){cout<<"catch Big"<<endl;}return 0;
}

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

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

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

catch(...){

//释放一些资源

throw;

}

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

1.terminate()函数

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

2.set_terminate()函数

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

#include <iostream>
#include <exception>
#include <stdlib.h>
using namespace std;void terminator()
{cout<<"I'll be back!"<<endl;exit(0);
}/*set_terminate返回被替换的指向terminate()函数的指针
第一次调用时,返回的是指向原terminate函数的指针*/
void (*old_terminate)()=set_terminate(terminator);class Botch
{
public:class Fruit{};void f(){cout<<"Botch::f()"<<endl;throw Fruit();}~Botch(){throw 'c';}
};/*
程序在处理一个异常的时候会释放在栈上分配的对象,这时,析构函数被调用,这时候产生了第二个异常
正是这个第二个以下航导致了terminate的调用
*/
int main()
{try{Botch b;b.f();}catch(...){cout<<"inside catch(...)"<<endl;}return 0;
}

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

5.清理

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

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

#include <iostream>using namespace std;class Trace
{static int counter;int objid;
public:Trace(){objid=counter++;cout<<"construction Trace #"<<objid<<endl;if(objid==3)throw 3;}~Trace(){cout<<"destruction Trace #"<<objid<<endl;}
};
int main()
{try{Trace n1;Trace Array[5];Trace n2;}catch(int i){cout<<"caught "<<i<<endl;}return 0;
}

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

下面是一个例子:

#include <iostream>
#include <cstddef>
using namespace std;class Cat
{
public:Cat(){cout<<"Cat()"<<endl;}~Cat(){cout<<"~Cat()"<<endl;}
};/*这些语句用来模拟内存不足的情况,可以不用鸟他
但可以看到这里的new中抛出了一个异常*/
class Dog
{
public:void* operator new(size_t sz){cout<<"allocating a Dog"<<endl;throw 47;}void operator delete(void* p){cout<<"deallocating a Dog"<<endl;::operator delete(p);}
};class UseResources
{Cat* bp;Dog* op;
public:UseResources(int count=1){cout<<"UseResources()"<<endl;bp=new Cat[count];op=new Dog;}~UseResources(){cout<<"~UseResources()"<<endl;delete [] bp;delete op;}
};int main()
{try{UseResources ur(3);}catch(int){cout<<"inside handler"<<endl;}return 0;
}

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

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

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

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

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

#include <iostream>
#include <cstddef>
using namespace std;template<class T,int sz=1>
class PWrap
{T* ptr;
public:class RangeeError{};PWrap(){ptr=new T[sz];cout<<"Pwrap constractor"<<endl;}~PWrap(){delete[] ptr;cout<<"PWrap deconstracor"<<endl;}T& operator[](int i) throw(RangeeError){if(i>=0&&i<sz)return ptr[i];throw RangeeError();}
};class Cat
{
public:Cat(){cout<<"Cat()"<<endl;}~Cat(){cout<<"~Cat()"<<endl;}void g(){}
};class Dog
{
public:void* operator new[](size_t sz){cout<<"allocating a Dog"<<endl;throw 47;}void operator delete[](void* p){cout<<"deallocating a Dog"<<endl;::operator delete(p);}
};class UseResources
{PWrap<Cat,3> cats;PWrap<Dog> dog;
public:UseResources(){cout<<"UseResources()"<<endl;}~UseResources(){cout<<"~UseResources()"<<endl;}void f(){cats[1].g();}
};int main()
{try{UseResources ur;}catch(int){cout<<"inside handler"<<endl;}catch(...){cout<<"inside catch"<<endl;}return 0;
}

这是运行结果:

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

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

auto_ptr:

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

#include <iostream>
#include <memory>
#include <cstddef>
using namespace std;class TraceHeap
{int i;
public:static void* operator new(size_t siz){void* p=::operator new(siz);cout<<"Allocating TraceHeap object on the heap at address "<<p<<endl;return p;}static void operator delete(void* p){cout<<"Deleting TraceHeap object at address "<<p<<endl;::operator delete(p);}TraceHeap(int i):i(i){}int getVal() const {return i;}
};int main()
{auto_ptr<TraceHeap> pMyObject(new TraceHeap(5));cout<<pMyObject->getVal()<<endl;
}

程序的运行结果为:

函数级的try块:

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

#include <iostream>
using namespace std;class Base
{int i;
public:class BaseExcept{};Base(int i):i(i){throw BaseExcept();}
};class Dirived:public Base
{
public:class DirivedExcept{const char* msg;public:DirivedExcept(const char* msg):msg(msg){}const char* what() const{return msg;}};Dirived(int j) try : Base(j){cout<<"this won't print"<<endl;}catch(BaseExcept&){throw DirivedExcept("Base subobject threw");}
};int main()
{try{Dirived d(3);}catch(Dirived::DirivedExcept& d){cout<<d.what()<<endl;}
}

C++编程技术之 异常处理(上)相关推荐

  1. 运行时:Linux 和 Windows 2000上的高性能编程技术

    运行时:Linux 和 Windows 2000上的高性能编程技术 建立计时例程       级别: 初级 Edward G. Bradford, 高级程序员, IBM 2001 年 4 月 01 日 ...

  2. TCP:利用Socket编程技术实现客户端向服务端上传一个图片。

    问题: 利用Socket编程技术实现客户端向服务端上传一个图片的程序. 客户端: import java.io.*; import java.net.Socket;public class clien ...

  3. 提高C++性能的编程技术笔记:内联+测试代码

    内联类似于宏,在调用方法内部展开被调用方法,以此来代替方法的调用.一般来说表达内联意图的方式有两种:一种是在定义方法时添加内联保留字的前缀:另一种是在类的头部声明中定义方法. 虽然内联方法的调用方式和 ...

  4. 程序猿充电的五本优质编程技术书

    导语: 知识更新那么快,跟随不上怎么办? 碎片信息那么多,选择恐惧怎么办? 别担心,我用心挑选了几本编程技术书. 长期推送干货.职场心得,让你收获满满. 话休絮烦,点击关注,长期推送干货! 第一行代码 ...

  5. Windows下多线程编程技术及其实现

    本文首先讨论16位Windows下不具备的线程的概念,然后着重讲述在32位Windows 95环境下多线程的编程技术,最后给出利用该技术的一个实例,即基于Windows95下TCP/IP的可视电话的实 ...

  6. java实验指导答案华软_Java核心编程技术实验指导教程

    软件工程类 Java核心编程技术实验指导教程 作者:张屹, 蔡木生 所属类别:新世纪应用型高等教育软件专业系列规划教材 出版时间:2010年10月 ISBN:978-7-5611-5839-5前言 本 ...

  7. 游戏编程精粹1-6分类目录之通用编程技术部分

    游戏编程精粹1-6分类目录之通用编程技术部分 ----------第一册------------------------- 第1章 通用编程技术 1.0 神奇的数据驱动设计(Steve Rabin) ...

  8. JNI_编程技术__网文整理

    Chap1:JNI完全手册... 3 Chap2:JNI-百度百科... 11 Chap 3:javah命令帮助信息... 16 Chap 4:用javah产生一个.h文件... 17 Chap5:j ...

  9. JNI_编程技术__网文整理(下)

    1.5.使用大量本地引用而未通知 JVM JNI 函数返回的任何对象都会创建本地引用.举例来说,当您调用 GetObjectArrayElement() 时,将返回对数组中对象的本地引用.考虑清单 8 ...

最新文章

  1. android 修改ramdisk.img和init.rc android启动后设置/data权限为770
  2. pb中将已经建好的Grid类型转为Tabular类型
  3. rocketmq 启动_016【windows版Rocketmq】小白学习Rocketmq单机部署
  4. ubuntu等linux系统给windows共享文件
  5. python发送邮件并返回提示_python-邮件提醒功能
  6. python 登录接口测试_Python接口测试——post请求(登录接口)
  7. windows10系统下设置mtu值的方法
  8. java计算机毕业设计大数据在线考试系统在线阅卷系统及大数据统计分析MyBatis+系统+LW文档+源码+调试部署
  9. C语言中三目运算符的结合性问题
  10. win32gui恢复小化窗口,前置窗口
  11. 计算机网络之DNS面试题
  12. 工业强国机械制造增长新方向 工业机器人产业发展迅速
  13. 【面试/笔试】—— 数学
  14. 常用函数+星期+月份+缩写+四季
  15. 华为android系统是什么意思,华为HarmonyOS与安卓系统有什么区别?一文了解
  16. 微信支付服务商模式签名错误小坑
  17. 细胞系鉴定和表征测试-市场现状及未来发展趋势
  18. 乡村少年宫计算机课程,乡村学校少年宫计算机课程安排(5页)-原创力文档
  19. 7的意志 (数位DP)
  20. erp登入显示查找服务器地址,如何查询软件的登录服务器地址

热门文章

  1. c语言心形编程代码_做游戏,学编程(C语言) 7 学习EasyX图形交互功能----flappy bird源代码...
  2. exchange服务器磁盘性能,如何解决Exchange磁盘空间问题
  3. python cnn程序_python cnn训练(针对Fashion MNIST数据集)
  4. android支持平台,Android 平台功能
  5. 已解决:fastclick插件在IOS系统上点击input需要双击或长按才有效
  6. php随机数字不重复使等式成立_Schur补与矩阵打洞,SMW求逆公式,分块矩阵与行列式(不)等式...
  7. 20190901:(leetcode习题)缺失数字
  8. jquery 扩展ajax请求,jQuery如何管理、扩展AJAX请求
  9. Enjoy模板里使用layui模板引擎laytpl
  10. 8202v/8202ka/8202kd/8202rd 无线游戏手柄方案