VC++的异常.docx

上传人:b****7 文档编号:10614751 上传时间:2023-02-21 格式:DOCX 页数:43 大小:53.68KB
下载 相关 举报
VC++的异常.docx_第1页
第1页 / 共43页
VC++的异常.docx_第2页
第2页 / 共43页
VC++的异常.docx_第3页
第3页 / 共43页
VC++的异常.docx_第4页
第4页 / 共43页
VC++的异常.docx_第5页
第5页 / 共43页
点击查看更多>>
下载资源
资源描述

VC++的异常.docx

《VC++的异常.docx》由会员分享,可在线阅读,更多相关《VC++的异常.docx(43页珍藏版)》请在冰豆网上搜索。

VC++的异常.docx

VC++的异常

异常处理

作者:

未知来源:

月光软件站加入时间:

2005-2-28 月光软件站

-

C++ExceptionHandler

2001-12-11

异常处理的基本思想是简化程序的错误代码,为程序键壮性提供一个标准检测机制。

也许我们已经使用过异常,但是你会是一种习惯吗,不要老是想着当我打开一个文件的时候才用异常判断一下,我知道对你来说你喜欢用returnvalue或者是printerrormessage来做,你想过这样做会导致MemoryLeak,系统退出,代码重复/难读,垃圾一堆…..吗?

现在的软件已经是n*365*24小时的运行了,软件的健壮已经是一个很要考虑的时候了。

自序:

对写程序来说异常真的是很重要,一个稳健的代码不是靠返回ErrorMessage/returnValue来解决的,可是往往我们从C走过来,习惯了这样的方式。

仅以本文献给今天将要来临的流星雨把,还好我能在今天白天把这写完,否则会是第4个通宵了;同时感谢Jeffrey大师,没有他的SEH理论这篇文章只能完成一半,而且所有SEH列子的构想都来自他的指导;另外要感谢ScottMeyers大师,我是看他的书长大的;还要感谢Adamc/Darwin/Julian,当然还有Nick的Coffee

内容导读:

(请打开文档结构图来读这篇文章。

本文包括2个大的异常实现概念:

C++的标准异常和SHE异常。

C++标准异常:

也许我们了解过他,但你有考虑过,其实你根本不会使用,你不相信,那我问你:

垃圾回收在C++中怎么实现?

其实不需要实现,C++已经有了,但是你不会用,那么从<构造和析构中的异常抛出>开始看把。

也许很高兴看到错误之后的Heap/Stack中对象被释放,可是如果没有呢?

有或者试想一下一个能解决的错误,需要我们把整个程序Kill掉吗?

             在C++标准异常中我向你推荐这几章:

<使用异常规格编程><构造和析构中的异常抛出> <使用析构函数防止资源泄漏>以及一个深点的<抛出一个异常的行为>

SHE异常:

我要问你你是一个WIN32程序员吗?

如果不是,那么也许你真的不需要看

这块内容了,SHE是Windows的结构化异常,每一个WIN32程序员都应该要掌握它。

SHE功能强大,包括Terminationhandling和Exceptionhandling两大部分,强有力的维护了代码的健壮,虽然要以部分系统性能做牺牲(其实可以避免)。

在SHE中有大量的代码,已经在Win平台上测试过了。

这里要提一下:

在__finally处理中编译器参与了绝大多数的工作,而Exception则是OS接管了几乎所有的工作,也许我没有提到的是:

对__finally来说当遇到ExitThread/ExitProcess/abort等函数时,finally块不会被执行。

另,我们的代码使用软件异常是比returnerrormessage好2**32的方法。

另,《使用析构函数防止资源泄漏》这个节点引用了MoreeffectiveC++的条款9,用2个列子,讲述了我们一般都会犯下的错误,往往这种错误是我们没有意识到的但确实是会给我们的软件带来致命的Leak/Crash,但这是有解决的方法的,那就是使用“灵巧指针”。

如果对照的37条条款,关于异常的高级使用,有以下内容是没有完成的:

l 使用构造函数防止资源Leak(MoreeffectiveC++#10)

l 禁止异常信息传递到析构Function外(MoreeffectiveC++#11)

l 通过引用捕获异常(MoreeffectiveC++#13)

l 谨慎使用异常规格 (MoreeffectiveC++#14)

l 了解异常处理造成的系统开销(MoreeffectiveC++#15)

l 限制对象数量(MoreeffectiveC++#26)

l 灵巧指针(MoreeffectiveC++#28)

[声明:

节点:

<使用析构函数防止资源泄漏>和节点:

<抛出一个异常的行为>中有大量的关于MoreeffectiveC++的条款,所以本文挡只用于自我阅读和内部交流,任何公开化和商业化,事先声明与本人无关。

]

C++异常

C++引入异常的原因

C++新增的异常机制改变了某些事情,这些改变是彻底的,但这些改变也可能让我们不舒服。

例如使用未经处理的pointer变的很危险,Memory/ResourceLeak变的更有可能了(别说什么Memory便宜了,那不是一个优秀的程序员说的话。

),写出一个具有你希望的行为的构造函数和析构函数也变的困难(不可预测),当然最危险的也许是我们写出的东东狗屁了,或者是速度变慢了。

大多数的程序员知道Howtouseexception来处理我们的代码,可是很多人并不是很重视异常的处理(国外的很多Code倒是处理的很好,Java的Exception机制很不错)。

异常处理机制是解决某些问题的上佳办法,但同时它也引入了许多隐藏的控制流程;有时候,要正确无误的使用它并不容易。

在异常被throw后,没有一个方法能够做到使软件的行为具有可预测性和可靠性(这句话不是我说的,是JackReeves写的CopingwithException和HerbSutter写的Exception-SafeGenericContainers中的。

)一个没有按照异常安全设计的程序想Run正常,是做梦,别去想没有异常出现的可能,

对C程序来说,使用ErrorCode就可以了,为什么还要引入异常?

因为异常不能被忽略。

如果一个函数通过设置一个状态变量或返回错误代码来表示一个异常状态,没有办法保证函数调用者将一定检测变量或测试错误代码。

结果程序会从它遇到的异常状态继续运行,异常没有被捕获,程序立即会终止执行。

在C程序中,我们可以用intsetjmp(jmp_bufenv);和voidlongjmp(jmp_bufenv,intvalue);这2个函数来完成和异常处理相识的功能,但是MSDN中介绍了在C++中使用longjmp来调整stack时不能够对局部的对象调用析构函数,但是对C++程序来说,析构函数是重要的(我就一般都把对象的Delete放在析构函数中)。

所以我们需要一个方法:

①能够通知异常状态,又不能忽略这个通知,②并且Searchingthestack以便找到异常代码时,③还要确保局部对象的析构函数被Call。

而C++的异常处理刚好就是来解决这些问题的。

有的地方只有用异常才能解决问题,比如说,在当前上下文环境中,无法捕捉或确定的错误类型,我们就得用一个异常抛出到更大的上下文环境当中去。

还有,异常处理的使用呢,可以使出错处理程序与“通常”代码分离开来,使代码更简洁更灵活。

另外就是程序必不可少的健壮性了,异常处理往往在其中扮演着重要的角色。

C++使用throw关键字来产生异常,try关键字用来检测的程序块,catch关键字用来填写异常处理的代码。

异常可以由一个确定类或派生类的对象产生。

C++能释放堆栈,并可清除堆栈中所有的对象。

C++的异常和pascal不同,是要程序员自己去实现的,编译器不会做过多的动作。

throw异常类编程

抛出异常用throw,如:

throwExceptionClass(“mythrow“);

例句中,ExceptionClass是一个类,它的构造函数以一个字符串做为参数。

也就是说,在throw的时候,C++的编译器先构造一个ExceptionClass的对象,让它作为throw的值抛出去。

同时,程序返回,调用析构。

看下面这个程序:

#include

classExceptionClass{

      char*name;

public:

ExceptionClass(constchar*name="defaultname")

{

            cout<<"Construct"<

            this->name=name;

      }

~ExceptionClass()

{

            cout<<"Destruct"<

}

voidmythrow()

{

      throwExceptionClass("mythrow");

}

}

voidmain(){

      ExceptionClasse("Test");

      try{

          e.mythrow();

    }

    catch(...)

   {

        cout<<”*********”<

      }

}

这是输出信息:

ConstructTest

Constructmythrow

Destructmythrow

****************

Destructmythrow  (这里是异常处理空间中对异常类的拷贝的析构)

DestructTest

======================================

不过一般来说我们可能更习惯于把会产生异常的语句和要throw的异常类分成不同的类来写,下面的代码可以是我们更愿意书写的:

………..

classExceptionClass{

public:

 ExceptionClass(constchar*name="ExceptionDefaultClass"){

  cout<<"ExceptionClassConstructString"<

 }

 ~ExceptionClass(){

  cout<<"ExceptionClassDestructString"<

 }

 voidReportError() {

  cout<<"ExceptionClass:

:

ThisisReportErrorMessage"<

 }

};

classArguClass{

 char*name;

public:

 ArguClass(char*name="defaultname"){

  cout<<"ConstructString:

:

"<

  this->name=name;

 }

 ~ArguClass(){

  cout<<"DestructString:

:

"<

 }

 voidmythrow(){

  throwExceptionClass("mythrow");

 }      

};

_tmain()

{

 ArguClasse("haha");

 try{

  e.mythrow();

 }

 catch(int)

 {

  cout<<"IfThisisMessagedisplayscreen,ThisisaError!

!

"<

 }

 catch(ExceptionClasspTest)

 {

  pTest.ReportError();

 }

 catch(...){

  cout<<"***************"<

 }

}

输出Message:

ConstructString:

:

haha

ExceptionClassConstructString

ExceptionClassDestructString

ExceptionClass:

:

ThisisReportErrorMessage

ExceptionClassDestructString

DestructString:

:

haha

使用异常规格编程

如果我们调用别人的函数,里面有异常抛出,用去查看它的源代码去看看都有什么异常抛出吗?

这样就会很烦琐。

比较好的解决办法,是编写带有异常抛出的函数时,采用异常规格说明,使我们看到函数声明就知道有哪些异常出现。

异常规格说明大体上为以下格式:

voidExceptionFunction(argument…)throw(ExceptionClass1,ExceptionClass2,….)

所有异常类都在函数末尾的throw()的括号中得以说明了,这样,对于函数调用者来说,是一清二楚的。

注意下面一种形式:

voidExceptionFunction(argument…)throw()

表明没有任何异常抛出。

而正常的voidExceptionFunction(argument…)则表示:

可能抛出任何一种异常,当然,也可能没有异常,意义是最广泛的。

异常捕获之后,可以再次抛出,就用一个不带任何参数的throw语句就可以了。

构造和析构中的异常抛出

这是异常处理中最要注意的地方了

先看个程序,假如我在构造函数的地方抛出异常,这个类的析构会被调用吗?

可如果不调用,那类里的东西岂不是不能被释放了?

#include

#include

classExceptionClass1

{

      char*s;

public:

      ExceptionClass1(){

             cout<<"ExceptionClass1()"<

             s=newchar[4];

             cout<<"throwaexception"<

             throw18;

      }

      ~ExceptionClass1(){

             cout<<"~ExceptionClass1()"<

             delete[]s;

      }

};

voidmain(){

      try{

            ExceptionClass1e;

      }catch(...)

      {}

}

结果为:

ExceptionClass1()

throwaexception

在这两句输出之间,我们已经给S分配了内存,但内存没有被释放(因为它是在析构函数中释放的)。

应该说这符合实际现象,因为对象没有完整构造。

为了避免这种情况,我想你也许会说:

应避免对象通过本身的构造函数涉及到异常抛出。

即:

既不在构造函数中出现异常抛出,也不应在构造函数调用的一切东西中出现异常抛出。

但是在C++中可以在构造函数中抛出异常,经典的解决方案是使用STL的标准类auto_ptr。

其实我们也可以这样做来实现:

在类中增加一个Init();以及UnInit();成员函数用于进行容易产生错误的资源分配工作,而真正的构造函数中先将所有成员置为NULL,然后调用Init();并判断其返回值/或者捕捉Init()抛出的异常,如果Init();失败了,则在构造函数中调用UnInit();并设置一个标志位表明构造失败。

UnInit()中按照成员是否为NULL进行资源的释放工作。

那么,在析构函数中的情况呢?

我们已经知道,异常抛出之后,就要调用本身的析构函数,如果这析构函数中还有异常抛出的话,则已存在的异常尚未被捕获,会导致异常捕捉不到。

标准C++异常类

C++有自己的标准的异常类。

① 一个基类:

exception  是所有C++异常的基类。

classexception{

public:

   exception()throw();

   exception(constexception&rhs)throw();

   exception&operator=(constexception&rhs)throw();

   virtual~exception()throw();

   virtualconstchar*what()constthrow();

};

②下面派生了两个异常类:

logic_erro      报告程序的逻辑错误,可在程序执行前被检测到。

runtime_erro    报告程序运行时的错误,只有在运行的时候才能检测到。

以上两个又分别有自己的派生类:

③ 由logic_erro派生的异常类

domain_error           报告违反了前置条件

invalid_argument        指出函数的一个无效参数

length_error指出有一个产生超过NPOS长度的对象的企图(NPOS为size_t的最大可表现值

out_of_range报告参数越界

bad_cast                     在运行时类型识别中有一个无效的dynamic_cast表达式

bad_typeid报告在表达式typeid(*p)中有一个空指针P

④ 由runtime_error派生的异常

range_error报告违反了后置条件

overflow_error报告一个算术溢出

bad_alloc                    报告一个存储分配错误

使用析构函数防止资源泄漏

这部分是一个经典和很平常就会遇到的实际情况,下面的内容大部分都是从MoreEffectiveC++条款中得到的。

假设,你正在为一个小动物收容所编写软件,小动物收容所是一个帮助小狗小猫寻找主人的组织。

每天收容所建立一个文件,包含当天它所管理的收容动物的资料信息,你的工作是写一个程序读出这些文件然后对每个收容动物进行适当的处理(appropriateprocessing)。

完成这个程序一个合理的方法是定义一个抽象类,ALA("AdorableLittleAnimal"),然后为小狗和小猫建立派生类。

一个虚拟函数processAdoption分别对各个种类的动物进行处理:

classALA{

public:

 virtualvoidprocessAdoption()=0;

 ...

};

classPuppy:

publicALA{

public:

 virtualvoidprocessAdoption();

 ...

};

classKitten:

publicALA{

public:

 virtualvoidprocessAdoption();

 ...

};

你需要一个函数从文件中读信息,然后根据文件中的信息产生一个puppy(小狗)对象或者kitten(小猫)对象。

这个工作非常适合于虚拟构造器(virtualconstructor),在条款25详细描述了这种函数。

为了完成我们的目标,我们这样声明函数:

//从s中读动物信息,然后返回一个指针

//指向新建立的某种类型对象

ALA*readALA(istream&s);

你的程序的关键部分就是这个函数,如下所示:

voidprocessAdoptions(istream&dataSource)

{

 while(dataSource){         //还有数据时,继续循环

  ALA*pa=readALA(dataSource);   file:

//得到下一个动物

  pa->processAdoption();      file:

//处理收容动物

  deletepa;            file:

//删除readALA返回的对象

 }                 

}

 

这个函数循环遍历dataSource内的信息,处理它所遇到的每个项目。

唯一要记住的一点是在每次循环结尾处删除ps。

这是必须的,因为每次调用readALA都建立一个堆对象。

如果不删除对象,循环将产生资源泄漏。

现在考虑一下,如果pa->processAdoption抛出了一个异常,将会发生什么?

processAdoptions没有捕获异常,所以异常将传递给processAdoptions的调用者。

转递中,processAdoptions函数中的调用pa->processAdoption语句后的所有语句都被跳过,这就是说pa没有被删除。

结果,任何时候pa->processAdoption抛出一个异常都会导致processAdoptions内存泄漏。

堵塞泄漏很容易,

voidprocessAdoptions(istream&dataSource)

{

 while(dataSource){

  ALA*pa=readALA(dataSource);

 try{

   pa->processAdoption();

 }

 catch(...){       //捕获所有异常

  deletepa;       //避免内存泄漏

              //当异常抛出时

  throw;         //传送异常给调用者

 }

 deletepa;        //避免资源泄漏

}             //当没有异常抛出时

}

但是你必须用try和catch对你的代码进行小改动。

更重要的是你必须写双份清除代码,一个为正常的运行准备,一个为异常发生时准备。

在这种情况下,必须写两个delete代码。

象其它重复代码一样,这种代码写起来令人心烦又难于维护,而且它看上去好像存在着问题。

不论我们是让processAdoptions正常返回还是抛出异常,我们都需要删除pa,所以为什么我们必须要在多个地方编写删除代码呢?

我们可以把总被执行的清除代码放入processAdoptions函数内的局部对象的析构函数里,这样可以避免重复书写清除代码。

因为当函数返回时局部对象总是被释放,无论函数是如何退出的。

(仅有一种例外就是当你调用longjmp时。

Longjmp的这个缺点是C++率先支持异常处理的主要原因)

具体方法是用一个对象代替指针pa,这个对象的行为与指针相似。

当pointer-like(类指针)对象被释放时,我们能让它的析构函数调用delete。

替代指针的对象被称为smartpointers(灵巧指针),下面有解释,你能使得pointer-like对象非常灵巧。

在这里,我们用不着这么聪明的指针,我们只需要一个pointer-lik对象,当它离开生存空间时知道删除它指向的对象。

写出这样一个类并不困难,但是我们不需要自己去写。

标准C++库函数包含一个类模板,叫做auto_ptr,这正是我们想要的。

每一个auto_ptr类的构造函数里,让一个指针指向一个堆对象(heapobject),并且在它的析构函数里删除这个对象。

下面所示的是auto_ptr类的一些重要的部分:

template

classauto_ptr{

public:

 auto_ptr(T*p=0):

ptr(p){}    //保存ptr,指向对象

 ~auto_ptr(){d

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 医药卫生 > 基础医学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1