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

之所以平时编写代码的时候不愿意去写错误处理,主要是由于这项工作及其无聊并可能导致代码膨胀,导致的结果就是本来就比较复杂的程序变得更加复杂。当然了,前面的缘由主要是针对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. 关于如何提高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. Formiko总结整数十进制转换二进制原理

    引子: 为什么十进制转二进制的“辗转相除记录余数倒序输出”的算法是正确的?这个问题陪伴了Formiko半年. 实践: 实践一:把十进制数100转换成二进制数的图   上图和和下图唯一的区别在最后一位上 ...

  2. Aix6.1安装openssh

    一.软件下载 1.官方网站下载: openssl IBM官方网站下载:https://www14.software.ibm.com/webapp/iwm/web/reg/download.do?sou ...

  3. linux学习之(四)-用户、组的操作,给文件文件夹设置组,更改目录权限、文件权限

    命令帮助查看: man 命令(查看一个命令的详细帮助信息) 例:man useradd 或者用  -h   格式   命令 -h(查看一个命令的简要帮助) 例:useradd -h 用户: 在user ...

  4. 负margin新解

    第一篇 我知道你不知道的负Margin 分类: Html/CSS | 转载请注明: 出自 海玉的博客 本文地址: http://www.hicss.net/i-know-you-do-not-know ...

  5. 从汇编来看i++与++i

    故事背景,一个正在c语言的家伙,问我++i 和 i++的问题,我当时由于要去上课没给他说,正好今晚有空就測试了一下例如以下代码: 编译环境:VS2010  语言:C++ #include <io ...

  6. wamp 虚拟目录的设置(转载)

    现在先来配置虚拟主机:1.先打开apache的配置文件httpd.conf,并去掉#Include conf/extra/httpd-vhosts.conf前面的#!!2.打开apache的apach ...

  7. log4j日志输出配置

    # Configure logging for testing: optionally with log filelog4j.rootLogger=WARN, stdoutlog4j.rootLogg ...

  8. 根据老赵轻量级Actor进行修改的Actor模型

    学习了老赵轻量级Actor模型,并在实际中使用,效果不错. 老赵轻量级Actor模型: ActorLite:一个轻量级Actor模型实现(上) ActorLite:一个轻量级Actor模型实现(中) ...

  9. JAVA GC之标记 第五节

    JAVA GC之标记  第五节 OK,我们继续昨天最后留下的问题,什么是标记?怎么标记? 第一个问题相信大家都知道,标记就是对一些已死的对象打上记号,方便垃圾收集器的清理. 至于怎么标记,一般有两种方 ...

  10. getchar()用法

    getchar() .从缓冲区读走一个字符,相当于清除缓冲区 .前面的scanf()在读取输入时会在缓冲区中留下一个字符'\n'(输入完s[i]的值后按回车键所致),所以如果不在此加一个getchar ...