try catch
来源:互联网 发布:简单的java项目实例 编辑:程序博客网 时间:2024/06/10 06:14
原文:
http://gcclife.blog.163.com/blog/static/1816971332011523113035245/
函数后面的 throw 分三种:
1. throw() 表示这个函数不会抛出异常。注意这个是“不会”。
2. throw(...) 表示这个函数可能会抛出异常。
3. throw( type ) 表示这个函数可能会抛出指定类型的异常。但VC中type不起作用,相当于throw(...)。
原文:
http://wenku.baidu.com/view/f34ec518650e52ea55189810.html
C++异常C++引入异常的原因C++ 新增的异常机制改变了某些事情,这些改变是彻底的,但这些改变也可能让我们不舒服。例如使用未经处理的pointer变的很危 险,Memory/Resource Leak变的更有可能了(别说什么Memory便宜了,那不是一个优秀的程序员说的话。),写出一个具有你希望的行为的构造函数和析构函数也变的困难(不 可预测),当然最危险的也许是我们写出的东东狗屁了,或者是速度变慢了。
大 多数的程序员知道Howto use exception 来处理我们的代码,可是很多人并不是很重视异常的处理(国外的很多Code倒是处理的很好,Java的Exception机制很不错)。异常处理机制是解 决某些问题的上佳办法,但同时它也引入了许多隐藏的控制流程;有时候,要正确无误的使用它并不容易。
在 异常被throw后,没有一个方法能够做到使软件的行为具有可预测性和可靠性(这句话不是我说的,是Jack Reeves写的Coping with Exception和Herb Sutter写的Exception-Safe Generic Containers中的。)一个没有按照异常安全设计的程序想Run 正常,是做梦,别去想没有异常出现的可能,
对 C程序来说,使用Error Code就可以了,为什么还要引入异常?因为异常不能被忽略。如果一个函数通过设置一个状态变量或返回错误代码来表示一个异常状态,没有办法保证函数调用 者将一定检测变量或测试错误代码。结果程序会从它遇到的异常状态继续运行,异常没有被捕获,程序立即会终止执行。
在 C程序中,我们可以用int setjmp( jmp_buf env );和 void longjmp( jmp_buf env, int value );这2个函数来完成和异常处理相识的功能,但是MSDN中介绍了在C++中使用longjmp来调整stack时不能够对局部的对象调用析构函数,但是 对C++程序来说,析构函数是重要的(我就一般都把对象的Delete放在析构函数中)。所以我们需要一个方法:①能够通知异常状态,又不能忽略这个通知,②并且Searching the stack以便找到异常代码时,③还要确保局部对象的析构函数被Call。而C++的异常处理刚好就是来解决这些问题的。
有 的地方只有用异常才能解决问题,比如说,在当前上下文环境中,无法捕捉或确定的错误类型,我们就得用一个异常抛出到更大的上下文环境当中去。还有,异常处 理的使用呢,可以使出错处理程序与“通常”代码分离开来,使代码更简洁更灵活。另外就是程序必不可少的健壮性了,异常处理往往在其中扮演着重要的角色。
C++使用throw关键字来产生异常,try关键字用来检测的程序块,catch关键字用来填写异常处理的代码。异常可以由一个确定类或派生类的对象产生。C++能释放堆栈,并可清除堆栈中所有的对象。
C++的异常和pascal不同,是要程序员自己去实现的,编译器不会做过多的动作。
throw异常类编程抛出异常用throw, 如:throw ExceptionClass(“my throw“);
例句中,ExceptionClass是一个类,它的构造函数以一个字符串做为参数。也就是说,在throw的时候,C++的编译器先构造一个ExceptionClass的对象,让它作为throw的值抛出去。同时,程序返回,调用析构。看下面这个程序:#include <iostream.h>class ExceptionClass{ char* name;public:ExceptionClass(const char* name="default name") { cout<<"Construct "<<name<<endl; this->name=name; }~ExceptionClass(){ cout<<"Destruct "<<name<<endl;}void mythrow(){ throw ExceptionClass("my throw");
}}
void main(){ ExceptionClass e("Test"); try{ e.mythrow(); } catch(...) { cout<<”*********”<<endl; }}这是输出信息:Construct TestConstruct my throwDestruct my throw****************Destruct my throw (这里是异常处理空间中对异常类的拷贝的析构)Destruct Test======================================不过一般来说我们可能更习惯于把会产生异常的语句和要throw的异常类分成不同的类来写,下面的代码可以是我们更愿意书写的:………..class ExceptionClass{public: ExceptionClass(const char* name="Exception Default Class"){ cout<<"Exception Class Construct String"<<endl; } ~ExceptionClass(){ cout<<"Exception Class Destruct String"<<endl; } void ReportError() { cout<<"Exception Class:: This is Report Error Message"<<endl; }};
class ArguClass{ char* name;public: ArguClass(char* name="default name"){ cout<<"Construct String::"<<name<<endl; this->name=name; } ~ArguClass(){ cout<<"Destruct String::"<<name<<endl; } void mythrow(){ throw ExceptionClass("my throw"); } };
_tmain(){ ArguClass e("haha"); try { e.mythrow(); } catch(int) { cout<<"If This is Message display screen, This is a Error!!"<<endl; } catch(ExceptionClass pTest) { pTest.ReportError(); } catch(...){ cout<<"***************"<<endl; }}输出Message:Construct String::hahaException Class Construct StringException Class Destruct StringException Class:: This is Report Error MessageException Class Destruct StringDestruct String::haha
使用异常规格编程如果我们调用别人的函数,里面有异常抛出,用去查看它的源代码去看看都有什么异常抛出吗?这样就会很烦琐。比较好的解决办法,是编写带有异常抛出的函数时,采用异常规格说明,使我们看到函数声明就知道有哪些异常出现。
异常规格说明大体上为以下格式:
void ExceptionFunction(argument…) throw(ExceptionClass1, ExceptionClass2, ….)
所有异常类都在函数末尾的throw()的括号中得以说明了,这样,对于函数调用者来说,是一清二楚的。
注意下面一种形式:
void ExceptionFunction(argument…) throw()
表明没有任何异常抛出。
而正常的void ExceptionFunction(argument…)则表示:可能抛出任何一种异常,当然,也可能没有异常,意义是最广泛的。
异常捕获之后,可以再次抛出,就用一个不带任何参数的throw语句就可以了。构造和析构中的异常抛出这是异常处理中最要注意的地方了
先看个程序,假如我在构造函数的地方抛出异常,这个类的析构会被调用吗?可如果不调用,那类里的东西岂不是不能被释放了?
#include <iostream.h>#include <stdlib.h>
class ExceptionClass1{ char* s;public: ExceptionClass1(){ cout<<"ExceptionClass1()"<<endl; s=new char[4]; cout<<"throw a exception"<<endl; throw 18; } ~ExceptionClass1(){ cout<<"~ExceptionClass1()"<<endl; delete[] s; }};
void main(){ try{ ExceptionClass1 e; }catch(...) {}}
结果为:
ExceptionClass1()throw a exception
在这两句输出之间,我们已经给S分配了内存,但内存没有被释放(因为它是在析构函数中释放的)。应该说这符合实际现象,因为对象没有完整构造。
为了避免这种情况,我想你也许会说:应避免对象通过本身的构造函数涉及到异常抛出。即:既不在构造函数中出现异常抛出,也不应在构造函数调用的一切东西中出现异常抛出。
但是在C++中可以在构造函数中抛出异常,经典的解决方案是使用STL的标准类auto_ptr。其 实我们也可以这样做来实现:在类中增加一个 Init(); 以及 UnInit();成员函数用于进行容易产生错误的资源分配工作,而真正的构造函数中先将所有成员置为NULL,然后调用 Init(); 并判断其返回值/或者捕捉 Init()抛出的异常,如果Init();失败了,则在构造函数中调用 UnInit(); 并设置一个标志位表明构造失败。UnInit()中按照成员是否为NULL进行资源的释放工作。
那么,在析构函数中的情况呢?我们已经知道,异常抛出之后,就要调用本身的析构函数,如果这析构函数中还有异常抛出的话,则已存在的异常尚未被捕获,会导致异常捕捉不到。
标准C++异常类C++有自己的标准的异常类。
① 一个基类:exception 是所有C++异常的基类。class exception {public: exception() throw(); exception(const exception& rhs) throw(); exception& operator=(const exception& rhs) throw(); virtual ~exception() throw(); virtual const char *what() const throw();};
② 下面派生了两个异常类:
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 报告一个存储分配错误
使用析构函数防止资源泄漏这部分是一个经典和很平常就会遇到的实际情况,下面的内容大部分都是从More Effective C++条款中得到的。
假 设,你正在为一个小动物收容所编写软件,小动物收容所是一个帮助小狗小猫寻找主人的组织。每天收容所建立一个文件,包含当天它所管理的收容动物的资料信 息,你的工作是写一个程序读出这些文件然后对每个收容动物进行适当的处理(appropriate processing)。
完成这个程序一个合理的方法是定义一个抽象类,ALA("Adorable Little Animal"),然后为小狗和小猫建立派生类。一个虚拟函数processAdoption分别对各个种类的动物进行处理:
class ALA {
public:
virtual void processAdoption() = 0;
...
};
class Puppy: public ALA {
public:
virtual void processAdoption();
...
};
class Kitten: public ALA {
public:
virtual void processAdoption();
...
};
你需要一个函数从文件中读信息,然后根据文件中的信息产生一个puppy(小狗)对象或者kitten(小猫)对象。这个工作非常适合于虚拟构造器(virtual constructor),在条款25详细描述了这种函数。为了完成我们的目标,我们这样声明函数:
// 从s中读动物信息, 然后返回一个指针
// 指向新建立的某种类型对象
ALA * readALA(istream& s);
你的程序的关键部分就是这个函数,如下所示:void processAdoptions(istream& dataSource)
{
while (dataSource) {// 还有数据时,继续循环
ALA *pa = readALA(dataSource);file://得到下一个动物
pa->processAdoption(); file://处理收容动物
delete pa; file://删除readALA返回的对象
}
}
这个函数循环遍历dataSource内的信息,处理它所遇到的每个项目。唯一要记住的一点是在每次循环结尾处删除ps。这是必须的,因为每次调用readALA都建立一个堆对象。如果不删除对象,循环将产生资源泄漏。
现在考虑一下,如果pa->processAdoption抛出了一个异常,将会发生什么?processAdoptions 没有捕获异常,所以异常将传递给processAdoptions的调用者。转递中,processAdoptions函数中的调用 pa->processAdoption语句后的所有语句都被跳过,这就是说pa没有被删除。结果,任何时候 pa->processAdoption抛出一个异常都会导致processAdoptions内存泄漏。
堵塞泄漏很容易,
void processAdoptions(istream& dataSource)
{
while (dataSource) {
ALA *pa = readALA(dataSource);
try {
pa->processAdoption();
}
catch (...) {// 捕获所有异常
delete pa; // 避免内存泄漏
// 当异常抛出时
throw; // 传送异常给调用者
}
delete pa; // 避免资源泄漏
} // 当没有异常抛出时
}
但 是你必须用try和catch对你的代码进行小改动。更重要的是你必须写双份清除代码,一个为正常的运行准备,一个为异常发生时准备。在这种情况下,必须 写两个delete代码。象其它重复代码一样,这种代码写起来令人心烦又难于维护,而且它看上去好像存在着问题。不论我们是让 processAdoptions正常返回还是抛出异常,我们都需要删除pa,所以为什么我们必须要在多个地方编写删除代码呢?
我 们可以把总被执行的清除代码放入processAdoptions函数内的局部对象的析构函数里,这样可以避免重复书写清除代码。因为当函数返回时局部对 象总是被释放,无论函数是如何退出的。(仅有一种例外就是当你调用longjmp时。Longjmp的这个缺点是C++率先支持异常处理的主要原因)
具 体方法是用一个对象代替指针pa,这个对象的行为与指针相似。当pointer-like(类指针)对象被释放时,我们能让它的析构函数调用 delete。替代指针的对象被称为smart pointers(灵巧指针),下面有解释,你能使得pointer-like对象非常灵巧。在这里,我们用不着这么聪明的指针,我们只需要一个 pointer-lik对象,当它离开生存空间时知道删除它指向的对象。
写 出这样一个类并不困难,但是我们不需要自己去写。标准C++库函数包含一个类模板,叫做auto_ptr,这正是我们想要的。每一个auto_ptr类的 构造函数里,让一个指针指向一个堆对象(heap object),并且在它的析构函数里删除这个对象。下面所示的是auto_ptr类的一些重要的部分:
template<class T>
class auto_ptr {
public:
auto_ptr(T *p = 0): ptr(p) {}// 保存ptr,指向对象
~auto_ptr() { delete ptr; }// 删除ptr指向的对象
private:
T *ptr;// raw ptr to object
};
auto_ptr 类的完整代码是非常有趣的,上述简化的代码实现不能在实际中应用。(我们至少必须加上拷贝构造函数,赋值operator以及下面将要讲到的 pointer-emulating函数),但是它背后所蕴含的原理应该是清楚的:用auto_ptr对象代替raw指针,你将不再为堆对象不能被删除而 担心,即使在抛出异常时,对象也能被及时删除。(因为auto_ptr的析构函数使用的是单对象形式的delete,所以auto_ptr不能用于指向对 象数组的指针。如果想让auto_ptr类似于一个数组模板,你必须自己写一个。在这种情况下,用vector代替array可能更好)
auto_ptrtemplate<class T> class auto_ptr {public: typedef T element_type; explicit auto_ptr(T *p = 0) throw(); auto_ptr(const auto_ptr<T>& rhs) throw(); auto_ptr<T>& operator=(auto_ptr<T>& rhs) throw(); ~auto_ptr(); T& operator*() const throw(); T *operator->() const throw(); T *get() const throw(); T *release() const throw(); };
使用auto_ptr对象代替raw指针,processAdoptions如下所示:
void processAdoptions(istream& dataSource)
{
while (dataSource) {
auto_ptr<ALA> pa(readALA(dataSource));
pa->processAdoption();
}
}
这个版本的processAdoptions在两个方面区别于原来的processAdoptions函数。第一, pa被声明为一个auto_ptr<ALA>对象,而不是一个raw ALA*指针。第二, 在循环的结尾没有delete语句。其余部分都一样,因为除了析构的方式,auto_ptr对象的行为就象一个普通的指针。是不是很容易。
隐藏在auto_ptr后的思想是:用一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源,这种思想不只是可以运用在指针上,还能用在其它资源的分配和释放上。想一下这样一个在GUI程序中的函数,它需要建立一个window来显式一些信息:
// 这个函数会发生资源泄漏,如果一个异常抛出
void displayInfo(const Information& info){
WINDOW_HANDLE w(createWindow());
在w对应的window中显式信息
destroyWindow(w);
}
很 多window系统有C-like接口,使用象like createWindow 和 destroyWindow函数来获取和释放window资源。如果在w对应的window中显示信息时,一个异常被抛出,w所对应的window将被丢 失,就象其它动态分配的资源一样。
解决方法与前面所述的一样,建立一个类,让它的构造函数与析构函数来获取和释放资源:
file://一个类,获取和释放一个window 句柄
class WindowHandle {
public:
WindowHandle(WINDOW_HANDLE handle): w(handle) {}
~WindowHandle() { destroyWindow(w); }
operator WINDOW_HANDLE() { return w; }// see below
private:
WINDOW_HANDLE w;
// 下面的函数被声明为私有,防止建立多个WINDOW_HANDLE拷贝
file://有关一个更灵活的方法的讨论请参见下面的灵巧指针
WindowHandle(const WindowHandle&);
WindowHandle& operator=(const WindowHandle&);
};
这 看上去有些象auto_ptr,只是赋值操作与拷贝构造被显式地禁止(参见More effective C++条款27),有一个隐含的转换操作能把WindowHandle转换为WINDOW_HANDLE。这个能力对于使用WindowHandle对象 非常重要,因为这意味着你能在任何地方象使用raw WINDOW_HANDLE一样来使用WindowHandle。(参见More effective C++条款5 ,了解为什么你应该谨慎使用隐式类型转换操作)
通过给出的WindowHandle类,我们能够重写displayInfo函数,如下所示:
// 如果一个异常被抛出,这个函数能避免资源泄漏
void displayInfo(const Information& info)
{
WindowHandle w(createWindow());
在w对应的window中显式信息;
}
即使一个异常在displayInfo内被抛出,被createWindow 建立的window也能被释放。
资 源应该被封装在一个对象里,遵循这个规则,你通常就能避免在存在异常环境里发生资源泄漏。但是如果你正在分配资源时一个异常被抛出,会发生什么情况呢?例 如当你正处于resource-acquiring类的构造函数中。还有如果这样的资源正在被释放时,一个异常被抛出,又会发生什么情况呢?构造函数和析 构函数需要特殊的技术。你能在More effective C++条款10和More effective C++条款11中获取有关的知识。
抛出一个异常的行为个人认为接下来的这部分其实说的很经典,对我们理解异常行为/异常拷贝是很有帮助的。
条款12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
从语法上看,在函数里声明参数与在catch子句中声明参数几乎没有什么差别:
class Widget { ... }; file://一个类,具体是什么类 // 在这里并不重要void f1(Widget w); // 一些函数,其参数分别为void f2(Widget& w); // Widget, Widget&,或void f3(const Widget& w); // Widget* 类型void f4(Widget *pw); void f5(const Widget *pw);catch (Widget w) ... file://一些catch 子句,用来catch (Widget& w) ... file://捕获异常,异常的类型为catch (const Widget& w) ... // Widget, Widget&, 或catch (Widget *pw) ... // Widget*catch (const Widget *pw) ...
你因此可能会认为用throw抛出一个异常到catch子句中与通过函数调用传递一个参数两者基本相同。这里面确有一些相同点,但是他们也存在着巨大的差异。
让 我们先从相同点谈起。你传递函数参数与异常的途径可以是传值、传递引用或传递指针,这是相同的。但是当你传递参数和异常时,系统所要完成的操作过程则是完
这两天要处理一个异常的问题,刚好查了些相关的资料。在网上看到了一个不错的贴子,就转了过来,方便本人,以及来此旅游的朋友学习。源地址:http://www.host01.com/Print.html?91983,1
异常处理的基本思想是简化程序的错误代码,为程序键壮性提供一个标准检测机制。
也 许我们已经使用过异常,但是你会是一种习惯吗,不要老是想着当我打开一个文件的时候才用异常判断一下,我知道对你来说你喜欢用return value或者是print error message来做,你想过这样做会导致Memory Leak,系统退出,代码重复/难读,垃圾一堆…..吗?现在的软件已经是n*365*24小时的运行了,软件的健壮已经是一个很要考虑的时候了。 自序:对写程序来说异常真的是很重要,一个稳健的代码不是靠返回Error Message/return Value来解决的,可是往往我们从C走过来,习惯了这样的方式。 仅 以本文献给今天将要来临的流星雨把,还好我能在今天白天把这写完,否则会是第4个通宵了;同时感谢Jeffrey大师,没有他的SEH理论这篇文章只能完 成一半,而且所有SEH列子的构想都来自他的指导;另外要感谢Scott Meyers大师,我是看他的书长大的;还要感谢Adamc / Darwin / Julian ,当然还有Nick的Coffee
内容导读:(请打开文档结构图来读这篇文章。)本文包括2个大的异常实现概念:C++的标准异常和SHE异常。
C++ 标准异常:也许我们了解过他,但你有考虑过,其实你根本不会使用,你不相信,那我问你:垃圾回收在C++中怎么实现?其实不需要实现,C++已经有了,但 是你不会用,那么从<构造和析构中的异常抛出>开始看把。也许很高兴看到错误之后的Heap/Stack中对象被释放,可是如果没有呢?有或 者试想一下一个能解决的错误,需要我们把整个程序Kill掉吗? 在C++标准异常中我向你推荐这几章:<使用异常规格编程> <构造和析构中的异常抛出> <使用析构函数防止资源泄漏> 以及一个深点的<抛出一个异常的行为>
SHE异常: 我要问你你是一个WIN32程序员吗?如果不是,那么也许你真的不需要看 这 块内容了,SHE是Windows的结构化异常,每一个WIN32程序员都应该要掌握它。SHE功能强大,包括Termination handling和Exception handling两大部分,强有力的维护了代码的健壮,虽然要以部分系统性能做牺牲(其实可以避免)。在SHE中有大量的代码,已经在Win平台上测试过 了。 这里要提一下:在__finally处理中编译器参与了绝大多数的工作,而Exception则是OS接管了几乎所有的工作,也许我没有提到 的是:对__finally来说当遇到ExitThread/ExitProcess/abort等函数时,finally块不会被执行。另,我们的代码 使用软件异常是比return error message好2**32的方法。
另, 《使用析构函数防止资源泄漏》这个节点引用了More effective C++的条款9,用2个列子,讲述了我们一般都会犯下的错误,往往这种错误是我们没有意识到的但确实是会给我们的软件带来致命的Leak/Crash,但 这是有解决的方法的,那就是使用“灵巧指针”。
如果对照<More effective C++>的37条条款,关于异常的高级使用,有以下内容是没有完成的: l 使用构造函数防止资源Leak(More effective C++ #10) l 禁止异常信息传递到析构Function外 (More effective C++ #11) l 通过引用捕获异常 (More effective C++ #13) l 谨慎使用异常规格 (More effective C++ #14) l 了解异常处理造成的系统开销 (More effective C++ #15) l 限制对象数量 (More effective C++ #26) l 灵巧指针 (More effective C++ #28) [声明:节点:<使用析构函数防止资源泄漏> 和 节点:<抛出一个异常的行为>中有大量的关于More effective C++的条款,所以本文挡只用于自我阅读和内部交流,任何公开化和商业化,事先声明与本人无关。]
C++异常 C++引入异常的原因 C++ 新增的异常机制改变了某些事情,这些改变是彻底的,但这些改变也可能让我们不舒服。例如使用未经处理的pointer变的很危 险,Memory/Resource Leak变的更有可能了(别说什么Memory便宜了,那不是一个优秀的程序员说的话。),写出一个具有你希望的行为的构造函数和析构函数也变的困难(不 可预测),当然最危险的也许是我们写出的东东狗屁了,或者是速度变慢了。
大 多数的程序员知道Howto use exception 来处理我们的代码,可是很多人并不是很重视异常的处理(国外的很多Code倒是处理的很好,Java的Exception机制很不错)。异常处理机制是解 决某些问题的上佳办法,但同时它也引入了许多隐藏的控制流程;有时候,要正确无误的使用它并不容易。
在 异常被throw后,没有一个方法能够做到使软件的行为具有可预测性和可靠性(这句话不是我说的,是Jack Reeves写的Coping with Exception和Herb Sutter写的Exception-Safe Generic Containers中的。)一个没有按照异常安全设计的程序想Run 正常,是做梦,别去想没有异常出现的可能,
对 C程序来说,使用Error Code就可以了,为什么还要引入异常?因为异常不能被忽略。如果一个函数通过设置一个状态变量或返回错误代码来表示一个异常状态,没有办法保证函数调用 者将一定检测变量或测试错误代码。结果程序会从它遇到的异常状态继续运行,异常没有被捕获,程序立即会终止执行。
在 C程序中,我们可以用int setjmp( jmp_buf env );和 void longjmp( jmp_buf env, int value );这2个函数来完成和异常处理相识的功能,但是MSDN中介绍了在C++中使用longjmp来调整stack时不能够对局部的对象调用析构函数,但是 对C++程序来说,析构函数是重要的(我就一般都把对象的Delete放在析构函数中)。 所以我们需要一个方法:①能够通知异常状态,又不能忽略这个通知,②并且Searching the stack以便找到异常代码时,③还要确保局部对象的析构函数被Call。而C++的异常处理刚好就是来解决这些问题的。
有 的地方只有用异常才能解决问题,比如说,在当前上下文环境中,无法捕捉或确定的错误类型,我们就得用一个异常抛出到更大的上下文环境当中去。还有,异常处 理的使用呢,可以使出错处理程序与“通常”代码分离开来,使代码更简洁更灵活。另外就是程序必不可少的健壮性了,异常处理往往在其中扮演着重要的角色。
C++使用throw关键字来产生异常,try关键字用来检测的程序块,catch关键字用来填写异常处理的代码。异常可以由一个确定类或派生类的对象产生。C++能释放堆栈,并可清除堆栈中所有的对象。
C++的异常和pascal不同,是要程序员自己去实现的,编译器不会做过多的动作。
throw异常类编程 抛出异常用throw, 如: throw ExceptionClass(“my throw“);
例句中,ExceptionClass是一个类,它的构造函数以一个字符串做为参数。也就是说,在throw的时候,C++的编译器先构造一个ExceptionClass的对象,让它作为throw的值抛出去。同时,程序返回,调用析构。看下面这个程序: #include <iostream.h> class ExceptionClass{ char* name; public: ExceptionClass(const char* name="default name") { cout<<"Construct "<<name<<endl; this->name=name; } ~ExceptionClass() { cout<<"Destruct "<<name<<endl; } void mythrow() { throw ExceptionClass("my throw");
} }
void main(){ ExceptionClass e("Test"); try{ e.mythrow(); } catch(...) { cout<<”*********”<<endl; } } 这是输出信息: Construct Test Construct my throw Destruct my throw **************** Destruct my throw (这里是异常处理空间中对异常类的拷贝的析构) Destruct Test ====================================== 不过一般来说我们可能更习惯于把会产生异常的语句和要throw的异常类分成不同的类来写,下面的代码可以是我们更愿意书写的: ……….. class ExceptionClass{ public: ExceptionClass(const char* name="Exception Default Class"){ cout<<"Exception Class Construct String"<<endl; } ~ExceptionClass(){ cout<<"Exception Class Destruct String"<<endl; } void ReportError() { cout<<"Exception Class:: This is Report Error Message"<<endl; } };
class ArguClass{ char* name; public: ArguClass(char* name="default name"){ cout<<"Construct String::"<<name<<endl; this->name=name; } ~ArguClass(){ cout<<"Destruct String::"<<name<<endl; } void mythrow(){ throw ExceptionClass("my throw"); } };
_tmain() { ArguClass e("haha"); try { e.mythrow(); } catch(int) { cout<<"If This is Message display screen, This is a Error!!"<<endl; } catch(ExceptionClass pTest) { pTest.ReportError(); } catch(...){ cout<<"***************"<<endl; } } 输出Message: Construct String::haha Exception Class Construct String Exception Class Destruct String Exception Class:: This is Report Error Message Exception Class Destruct String Destruct String::haha
使用异常规格编程如果我们调用别人的函数,里面有异常抛出,用去查看它的源代码去看看都有什么异常抛出吗?这样就会很烦琐。比较好的解决办法,是编写带有异常抛出的函数时,采用异常规格说明,使我们看到函数声明就知道有哪些异常出现。
异常规格说明大体上为以下格式:
void ExceptionFunction(argument…) throw(ExceptionClass1, ExceptionClass2, ….)
所有异常类都在函数末尾的throw()的括号中得以说明了,这样,对于函数调用者来说,是一清二楚的。
注意下面一种形式:
void ExceptionFunction(argument…) throw()
表明没有任何异常抛出。
而正常的void ExceptionFunction(argument…)则表示:可能抛出任何一种异常,当然,也可能没有异常,意义是最广泛的。
异常捕获之后,可以再次抛出,就用一个不带任何参数的throw语句就可以了。 构造和析构中的异常抛出这是异常处理中最要注意的地方了
先看个程序,假如我在构造函数的地方抛出异常,这个类的析构会被调用吗?可如果不调用,那类里的东西岂不是不能被释放了?
#include <iostream.h> #include <stdlib.h>
class ExceptionClass1 { char* s; public: ExceptionClass1(){ cout<<"ExceptionClass1()"<<endl; s=new char[4]; cout<<"throw a exception"<<endl; throw 18; } ~ExceptionClass1(){ cout<<"~ExceptionClass1()"<<endl; delete[] s; } };
void main(){ try{ ExceptionClass1 e; }catch(...) {} }
结果为:
ExceptionClass1() throw a exception
在这两句输出之间,我们已经给S分配了内存,但内存没有被释放(因为它是在析构函数中释放的)。应该说这符合实际现象,因为对象没有完整构造。
为了避免这种情况,我想你也许会说:应避免对象通过本身的构造函数涉及到异常抛出。即:既不在构造函数中出现异常抛出,也不应在构造函数调用的一切东西中出现异常抛出。
但是在C++中可以在构造函数中抛出异常,经典的解决方案是使用STL的标准类auto_ptr。 其 实我们也可以这样做来实现:在类中增加一个 Init(); 以及 UnInit();成员函数用于进行容易产生错误的资源分配工作,而真正的构造函数中先将所有成员置为NULL,然后调用 Init(); 并判断其返回值/或者捕捉 Init()抛出的异常,如果Init();失败了,则在构造函数中调用 UnInit(); 并设置一个标志位表明构造失败。UnInit()中按照成员是否为NULL进行资源的释放工作。
那么,在析构函数中的情况呢?我们已经知道,异常抛出之后,就要调用本身的析构函数,如果这析构函数中还有异常抛出的话,则已存在的异常尚未被捕获,会导致异常捕捉不到。
标准C++异常类 C++有自己的标准的异常类。
① 一个基类: exception 是所有C++异常的基类。 class exception { public: exception() throw(); exception(const exception& rhs) throw(); exception& operator=(const exception& rhs) throw(); virtual ~exception() throw(); virtual const char *what() const throw(); };
② 下面派生了两个异常类:
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 报告一个存储分配错误
使用析构函数防止资源泄漏这部分是一个经典和很平常就会遇到的实际情况,下面的内容大部分都是从More Effective C++条款中得到的。
假 设,你正在为一个小动物收容所编写软件,小动物收容所是一个帮助小狗小猫寻找主人的组织。每天收容所建立一个文件,包含当天它所管理的收容动物的资料信 息,你的工作是写一个程序读出这些文件然后对每个收容动物进行适当的处理(appropriate processing)。
完成这个程序一个合理的方法是定义一个抽象类,ALA("Adorable Little Animal"),然后为小狗和小猫建立派生类。一个虚拟函数processAdoption分别对各个种类的动物进行处理:
class ALA {
public:
virtual void processAdoption() = 0;
...
};
class Puppy: public ALA {
public:
virtual void processAdoption();
...
};
class Kitten: public ALA {
public:
virtual void processAdoption();
...
};
你需要一个函数从文件中读信息,然后根据文件中的信息产生一个puppy(小狗)对象或者kitten(小猫)对象。这个工作非常适合于虚拟构造器(virtual constructor),在条款25详细描述了这种函数。为了完成我们的目标,我们这样声明函数:
// 从s中读动物信息, 然后返回一个指针
// 指向新建立的某种类型对象
ALA * readALA(istream& s);
你的程序的关键部分就是这个函数,如下所示: void processAdoptions(istream& dataSource)
{
while (dataSource) {// 还有数据时,继续循环
ALA *pa = readALA(dataSource);file://得到下一个动物
pa->processAdoption(); file://处理收容动物
delete pa; file://删除readALA返回的对象
}
}
这个函数循环遍历dataSource内的信息,处理它所遇到的每个项目。唯一要记住的一点是在每次循环结尾处删除ps。这是必须的,因为每次调用readALA都建立一个堆对象。如果不删除对象,循环将产生资源泄漏。
现在考虑一下,如果pa->processAdoption抛出了一个异常,将会发生什么? processAdoptions 没有捕获异常,所以异常将传递给processAdoptions的调用者。转递中,processAdoptions函数中的调用 pa->processAdoption语句后的所有语句都被跳过,这就是说pa没有被删除。结果,任何时候 pa->processAdoption抛出一个异常都会导致processAdoptions内存泄漏。
堵塞泄漏很容易,
void processAdoptions(istream& dataSource)
{
while (dataSource) {
ALA *pa = readALA(dataSource);
try {
pa->processAdoption();
}
catch (...) {// 捕获所有异常
delete pa; // 避免内存泄漏
// 当异常抛出时
throw; // 传送异常给调用者
}
delete pa; // 避免资源泄漏
} // 当没有异常抛出时
}
但 是你必须用try和catch对你的代码进行小改动。更重要的是你必须写双份清除代码,一个为正常的运行准备,一个为异常发生时准备。在这种情况下,必须 写两个delete代码。象其它重复代码一样,这种代码写起来令人心烦又难于维护,而且它看上去好像存在着问题。不论我们是让 processAdoptions正常返回还是抛出异常,我们都需要删除pa,所以为什么我们必须要在多个地方编写删除代码呢?
我 们可以把总被执行的清除代码放入processAdoptions函数内的局部对象的析构函数里,这样可以避免重复书写清除代码。因为当函数返回时局部对 象总是被释放,无论函数是如何退出的。(仅有一种例外就是当你调用longjmp时。Longjmp的这个缺点是C++率先支持异常处理的主要原因)
具 体方法是用一个对象代替指针pa,这个对象的行为与指针相似。当pointer-like(类指针)对象被释放时,我们能让它的析构函数调用 delete。替代指针的对象被称为smart pointers(灵巧指针),下面有解释,你能使得pointer-like对象非常灵巧。在这里,我们用不着这么聪明的指针,我们只需要一个 pointer-lik对象,当它离开生存空间时知道删除它指向的对象。
写 出这样一个类并不困难,但是我们不需要自己去写。标准C++库函数包含一个类模板,叫做auto_ptr,这正是我们想要的。每一个auto_ptr类的 构造函数里,让一个指针指向一个堆对象(heap object),并且在它的析构函数里删除这个对象。下面所示的是auto_ptr类的一些重要的部分:
template<class T>
class auto_ptr {
public:
auto_ptr(T *p = 0): ptr(p) {}// 保存ptr,指向对象
~auto_ptr() { delete ptr; }// 删除ptr指向的对象
private:
T *ptr;// raw ptr to object
};
auto_ptr 类的完整代码是非常有趣的,上述简化的代码实现不能在实际中应用。(我们至少必须加上拷贝构造函数,赋值operator以及下面将要讲到的 pointer-emulating函数),但是它背后所蕴含的原理应该是清楚的:用auto_ptr对象代替raw指针,你将不再为堆对象不能被删除而 担心,即使在抛出异常时,对象也能被及时删除。(因为auto_ptr的析构函数使用的是单对象形式的delete,所以auto_ptr不能用于指向对 象数组的指针。如果想让auto_ptr类似于一个数组模板,你必须自己写一个。在这种情况下,用vector代替array可能更好)
auto_ptr template<class T> class auto_ptr { public: typedef T element_type; explicit auto_ptr(T *p = 0) throw(); auto_ptr(const auto_ptr<T>& rhs) throw(); auto_ptr<T>& operator=(auto_ptr<T>& rhs) throw(); ~auto_ptr(); T& operator*() const throw(); T *operator->() const throw(); T *get() const throw(); T *release() const throw(); };
使用auto_ptr对象代替raw指针,processAdoptions如下所示:
void processAdoptions(istream& dataSource)
{
while (dataSource) {
auto_ptr<ALA> pa(readALA(dataSource));
pa->processAdoption();
}
}
这个版本的processAdoptions在两个方面区别于原来的processAdoptions函数。 第一, pa被声明为一个auto_ptr<ALA>对象,而不是一个raw ALA*指针。 第二, 在循环的结尾没有delete语句。 其余部分都一样,因为除了析构的方式,auto_ptr对象的行为就象一个普通的指针。是不是很容易。
隐藏在auto_ptr后的思想是:用一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源,这种思想不只是可以运用在指针上,还能用在其它资源的分配和释放上。想一下这样一个在GUI程序中的函数,它需要建立一个window来显式一些信息:
// 这个函数会发生资源泄漏,如果一个异常抛出
void displayInfo(const Information& info) {
WINDOW_HANDLE w(createWindow());
在w对应的window中显式信息
destroyWindow(w);
}
很 多window系统有C-like接口,使用象like createWindow 和 destroyWindow函数来获取和释放window资源。如果在w对应的window中显示信息时,一个异常被抛出,w所对应的window将被丢 失,就象其它动态分配的资源一样。
解决方法与前面所述的一样,建立一个类,让它的构造函数与析构函数来获取和释放资源:
file://一个类,获取和释放一个window 句柄
class WindowHandle {
public:
WindowHandle(WINDOW_HANDLE handle): w(handle) {}
~WindowHandle() { destroyWindow(w); }
operator WINDOW_HANDLE() { return w; }// see below
private:
WINDOW_HANDLE w;
// 下面的函数被声明为私有,防止建立多个WINDOW_HANDLE拷贝
file://有关一个更灵活的方法的讨论请参见下面的灵巧指针
WindowHandle(const WindowHandle&);
WindowHandle& operator=(const WindowHandle&);
};
这 看上去有些象auto_ptr,只是赋值操作与拷贝构造被显式地禁止(参见More effective C++条款27),有一个隐含的转换操作能把WindowHandle转换为WINDOW_HANDLE。这个能力对于使用WindowHandle对象 非常重要,因为这意味着你能在任何地方象使用raw WINDOW_HANDLE一样来使用WindowHandle。(参见More effective C++条款5 ,了解为什么你应该谨慎使用隐式类型转换操作)
通过给出的WindowHandle类,我们能够重写displayInfo函数,如下所示:
// 如果一个异常被抛出,这个函数能避免资源泄漏
void displayInfo(const Information& info)
{
WindowHandle w(createWindow());
在w对应的window中显式信息;
}
即使一个异常在displayInfo内被抛出,被createWindow 建立的window也能被释放。
资 源应该被封装在一个对象里,遵循这个规则,你通常就能避免在存在异常环境里发生资源泄漏。但是如果你正在分配资源时一个异常被抛出,会发生什么情况呢?例 如当你正处于resource-acquiring类的构造函数中。还有如果这样的资源正在被释放时,一个异常被抛出,又会发生什么情况呢?构造函数和析 构函数需要特殊的技术。你能在More effective C++条款10和More effective C++条款11中获取有关的知识。
抛出一个异常的行为个人认为接下来的这部分其实说的很经典,对我们理解异常行为/异常拷贝是很有帮助的。
条款12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
从语法上看,在函数里声明参数与在catch子句中声明参数几乎没有什么差别:
class Widget { ... }; file://一个类,具体是什么类 // 在这里并不重要 void f1(Widget w); // 一些函数,其参数分别为 void f2(Widget& w); // Widget, Widget&,或 void f3(const Widget& w); // Widget* 类型 void f4(Widget *pw); void f5(const Widget *pw); catch (Widget w) ... file://一些catch 子句,用来 catch (Widget& w) ... file://捕获异常,异常的类型为 catch (const Widget& w) ... // Widget, Widget&, 或 catch (Widget *pw) ... // Widget* catch (const Widget *pw) ...
你因此可能会认为用throw抛出一个异常到catch子句中与通过函数调用传递一个参数两者基本相同。这里面确有一些相同点,但是他们也存在着巨大的差异。
让 我们先从相同点谈起。你传递函数参数与异常的途径可以是传值、传递引用或传递指针,这是相同的。但是当你传递参数和异常时,系统所要完成的操作过程则是完 全不同的。产生这个差异的原因是:你调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。
有这样一个函数,参数类型是Widget,并抛出一个Widget类型的异常:
// 一个函数,从流中读值到Widget中 istream operator>>(istream& s, Widget& w); void passAndThrowWidget() { Widget localWidget; cin >> localWidget; file://传递localWidget到 operator>> throw localWidget; // 抛出localWidget异常 } 当 传递localWidget到函数operator>>里,不用进行拷贝操作,而是把operator>>内的引用类型变量w指 向localWidget,任何对w的操作实际上都施加到localWidget上。这与抛出localWidget异常有很大不同。不论通过传值捕获异 常还是通过引用捕获(不能通过指针捕获这个异常,因为类型不匹配)都将进行lcalWidget的拷贝操作,也就说传递到catch子句中的是 localWidget的拷贝。必须这么做,因为当localWidget离开了生存空间后,其析构函数将被调用。如果把localWidget本身(而 不是它的拷贝)传递给catch子句,这个子句接收到的只是一个被析构了的Widget,一个Widget的“尸体”。这是无法使用的。因此C++规范要 求被做为异常抛出的对象必须被复制。
即使被抛出的对象不会被释放,也会进行拷贝操作。例如如果passAndThrowWidget函数声明localWidget为静态变量(static),
void passAndThrowWidget() { static Widget localWidget; // 现在是静态变量(static); file://一直存在至程序结束 cin >> localWidget; // 象以前那样运行 throw localWidget; // 仍将对localWidget } file://进行拷贝操作当 抛出异常时仍将复制出localWidget的一个拷贝。这表示即使通过引用来捕获异常,也不能在catch块中修改localWidget;仅仅能修改 localWidget的拷贝。对异常对象进行强制复制拷贝,这个限制有助于我们理解参数传递与抛出异常的第二个差异:抛出异常运行速度比参数传递要慢。
usidc5
2010-09-19 00:08
当异常对象被拷贝时,拷贝操作是由对象的 拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。比如以下这经过少许修改的passAndThrowWidget: class Widget { ... }; class SpecialWidget: public Widget { ... }; void passAndThrowWidget() { SpecialWidget localSpecialWidget; ... Widget& rw = localSpecialWidget; // rw 引用SpecialWidget throw rw; file://它抛出一个类型为Widget // 的异常 } 这 里抛出的异常对象是Widget,即使rw引用的是一个SpecialWidget。因为rw的静态类型(static type)是Widget,而不是SpecialWidget。你的编译器根本没有主要到rw引用的是一个SpecialWidget。编译器所注意的是 rw的静态类型(static type)。这种行为可能与你所期待的不一样,但是这与在其他情况下C++中拷贝构造函数的行为是一致的。(不过有一种技术可以让你根据对象的动态类型 dynamic type进行拷贝,参见条款25) 异常是其它对象的拷贝,这个事实影响到你如何在catch块中再抛出一个异常。比如下面这两个catch块,乍一看好像一样: catch (Widget& w) // 捕获Widget异常 { ... // 处理异常 throw; // 重新抛出异常,让它 } // 继续传递 catch (Widget& w) // 捕获Widget异常 { ... // 处理异常 throw w; // 传递被捕获异常的 } // 拷贝 这两个catch块的差别在于第一个catch块中重新抛出的是当前捕获的异常,而第二个catch块中重新抛出的是当前捕获异常的一个新的拷贝。如果忽略生成额外拷贝的系统开销,这两种方法还有差异么? 当 然有。第一个块中重新抛出的是当前异常(current exception),无论它是什么类型。特别是如果这个异常开始就是做为SpecialWidget类型抛出的,那么第一个块中传递出去的还是 SpecialWidget异常,即使w的静态类型(static type)是Widget。这是因为重新抛出异常时没有进行拷贝操作。第二个catch块重新抛出的是新异常,类型总是Widget,因为w的静态类型 (static type)是Widget。一般来说,你应该用 throw 来重新抛出当前的异常,因为这样不会改变被传递出去的异常类型,而且更有效率,因为不用生成一个新拷贝。(顺便说一句,异常生成的拷贝是一个临时对象。正如条款19解释的,临时对象能让编译器优化它的生存期(optimize it out of existence),不过我想你的编译器很难这么做,因为程序中很少发生异常,所以编译器厂商不会在这方面花大量的精力。) 让我们测试一下下面这三种用来捕获Widget异常的catch子句,异常是做为passAndThrowWidgetp抛出的: catch (Widget w) ... // 通过传值捕获异常 catch (Widget& w) ... // 通过传递引用捕获 // 异常 catch (const Widget& w) ... file://通过传递指向const的引用 file://捕获异常我 们立刻注意到了传递参数与传递异常的另一个差异。一个被异常抛出的对象(刚才解释过,总是一个临时对象)可以通过普通的引用捕获;它不需要通过指向 const对象的引用(reference-to-const)捕获。在函数调用中不允许转递一个临时对象到一个非const引用类型的参数里(参见条款 19),但是在异常中却被允许。 让我们先不管这个差异,回到异常对象拷贝的测试上来。我们知道当用传值的方式传递函数的参数,我们制造了被传递对 象的一个拷贝(参见Effective C++ 条款22),并把这个拷贝存储到函数的参数里。同样我们通过传值的方式传递一个异常时,也是这么做的。当我们这样声明一个catch子句时: catch (Widget w) ... // 通过传值捕获 会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,第二个是把临时对象拷贝进w中。同样,当我们通过引用捕获异常时, catch (Widget& w) ... // 通过引用捕获 catch (const Widget& w) ... file://也通过引用捕获这仍旧会建立一个被抛出对象的拷贝:拷贝是一个临时对象。相反当我们通过引用传递函数参数时,没有进行对象拷贝。当抛出一个异常时,系统构造的(以后会析构掉)被抛出对象的拷贝数比以相同对象做为参数传递给函数时构造的拷贝数要多一个。我 们还没有讨论通过指针抛出异常的情况,不过通过指针抛出异常与通过指针传递参数是相同的。不论哪种方法都是一个指针的拷贝被传递。你不能认为抛出的指针是 一个指向局部对象的指针,因为当异常离开局部变量的生存空间时,该局部变量已经被释放。Catch子句将获得一个指向已经不存在的对象的指针。这种行为在 设计时应该予以避免。 对象从函数的调用处传递到函数参数里与从异常抛出点传递到catch子句里所采用的方法不同,这只是参数传递与异常传递的区 别的一个方面,第二个差异是在函数调用者或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不同。比如在标准数学库(the standard math library)中sqrt函数: double sqrt(double); // from <cmath> or <math.h> 我们能这样计算一个整数的平方根,如下所示: int i; double sqrtOfi = sqrt(i); 毫 无疑问,C++允许进行从int到double的隐式类型转换,所以在sqrt的调用中,i 被悄悄地转变为double类型,并且其返回值也是double。(有关隐式类型转换的详细讨论参见条款5)一般来说,catch子句匹配异常类型时不会 进行这样的转换。见下面的代码: void f(int value) { try { if (someFunction()) { // 如果 someFunction()返回 throw value; file://真,抛出一个整形值 ... } } catch (double d) { // 只处理double类型的异常 ... } ... } 在try块中抛出的int异常不会被处理double异常的catch子句捕获。该子句只能捕获真真正正为double类型的异常;不进行类型转换。因此如果要想捕获int异常,必须使用带有int或int&参数的catch子句。 不 过在catch子句中进行异常匹配时可以进行两种类型转换。第一种是继承类与基类间的转换。一个用来捕获基类的catch子句也可以处理派生类类型的异 常。例如在标准C++库(STL)定义的异常类层次中的诊断部分(diagnostics portion )(参见Effective C++ 条款49)。 捕获runtime_errors异常的Catch子句可以捕获range_error类型和overflow_error类型的异常,可以接收根类exception异常的catch子句能捕获其任意派生类异常。 这种派生类与基类(inheritance_based)间的异常类型转换可以作用于数值、引用以及指针上: catch (runtime_error) ... // can catch errors of type catch (runtime_error&) ... // runtime_error, catch (const runtime_error&) ... // range_error, or // overflow_error catch (runtime_error*) ... // can catch errors of type catch (const runtime_error*) ... // runtime_error*, // range_error*, or // overflow_error* 第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常: catch (const void*) ... file://捕获任何指针类型异常 传递参数和传递异常间最后一点差别是catch子句匹配顺序总是取决于它们在程序中出现的顺序。因此一个派生类异常可能被处理其基类异常的catch子句捕获,即使同时存在有能处理该派生类异常的catch子句,与相同的try块相对应。例如: try { ... } catch (logic_error& ex) { // 这个catch块 将捕获 ... // 所有的logic_error } // 异常, 包括它的派生类 catch (invalid_argument& ex) { // 这个块永远不会被执行 ... file://因为所有的 } // invalid_argument // 异常 都被上面的 // catch子句捕获。 与 上面这种行为相反,当你调用一个虚拟函数时,被调用的函数位于与发出函数调用的对象的动态类型(dynamic type)最相近的类里。你可以这样说虚拟函数采用最优适合法,而异常处理采用的是最先适合法。如果一个处理派生类异常的catch子句位于处理基类异常 的catch子句前面,编译器会发出警告。(因为这样的代码在C++里通常是不合法的。)不过你最好做好预先防范:不要把处理基类异常的catch子句放 在处理派生类异常的catch子句的前面。象上面那个例子,应该这样去写: try { ... } catch (invalid_argument& ex) { // 处理 invalid_argument < src="/js/google-top_336X280.js" type="text/javascript"> < src="http://a.alimama.cn/inf.js" type="text/javascript"> file://异常 } catch (logic_error& ex) { // 处理所有其它的 ... // logic_errors异常 } 综 上所述,把一个对象传递给函数或一个对象调用虚拟函数与把一个对象做为异常抛出,这之间有三个主要区别。第一、异常对象在传递时总被进行拷贝;当通过传值 方式捕获时,异常对象被拷贝了两次。对象做为参数传递给函数时不需要被拷贝。第二、对象做为异常被抛出与做为参数传递给函数相比,前者类型转换比后者要少 (前者只有两种转换形式)。最后一点,catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的catch将被用来执行。 当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里,即使该类不是在源代码的最前头。 灵巧指针第一次用到灵巧指针 是在写ADO代码的时候,用到com_ptr_t灵巧指针;但一直印象不是很深;其实灵巧指针的作用很大,对我们来说垃圾回收,ATL等都会使用到它,在 More effective 的条款后面特意增加这个节点,不仅是想介绍它在异常处理方面的作用,还希望对编写别的类型代码的时候可以有所帮助。 smart pointer(灵巧指针)其实并不是一个指针,其实是某种形式的类。 不过它的特长就是模仿C/C++中的指针,所以就叫pointer 了。所以希望大家一定要记住两点:smart pointer是一个类而非指针,但特长是模仿指针。 那怎么做到像指针的呢? C++的模板技术和运算符重载给了很大的发挥空间。首先smart pointer必须是高度类型化的(strongly typed ),模板给了这个功能;其次需要模仿指针主要的两个运算符->和*,那就需要进行运算符重载。 详细的实现: template<CLASS&NBSP;T> class SmartPtr { public: SmartPtr(T* p = 0); SmartPtr(const SmartPtr& p); ~SmartPtr(); SmartPtr& operator =(SmartPtr& p); T& operator*() const {return *the_p;} T* operator->() const {return the_p;} private: T *the_p; } 这 只是一个大概的印象,很多东西是可以更改的。比如可以去掉或加上一些const ,这都需要根据具体的应用环境而定。注意重载运算符*和->,正是它们使smart pointer看起来跟普通的指针很相像。而由于smart pointer是一个类,在构造函数、析构函数中都可以通过恰当的编程达到一些不错的效果。 举例: 比如C++标准库里的std::auto_ptr 就是应用很广的一个例子。它的实现在不同版本的STL 中虽有不同,但原理都是一样,大概是下面这个样子: template<CLASS&NBSP;X> class auto_ptr { public: typedef X element_type; explicit auto_ptr(X* p = 0) throw() : the_p(p) {} auto_ptr(auto_ptr& a) throw() : the_p(a.release()) {} auto_ptr& operator =(auto_ptr& rhs) throw() { reset(rhs.release()); return *this; } ~auto_ptr() throw() {delete the_p;} X& operator* () const throw() {return *the_p;} X* operator-> () const throw() {return the_p;} X* get () const throw() {return the_p;} X* release() throw() { X* tmp = the_p; the_p = 0; return tmp; } void reset(X* p = 0) throw() { if (the_p!=p) { delete the_p; the_p = p; } } private: X* the_p; }; 关 于auto_ptr 的使用可以找到很多的列子,这里不在举了。它的主要优点是不用 delete ,可以自动回收已经被分配的空间,由此可以避免资源泄露的问题。很多Java 的拥护者经常不分黑白的污蔑C++没有垃圾回收机制,其实不过是贻笑大方而已。抛开在网上许许多多的商业化和非商业化的C++垃圾回收库不提, auto_ptr 就足以有效地解决这一问题。并且即使在产生异常的情况下, auto_ptr 也能正确地回收资源。这对于写出异常安全(exception-safe )的代码具有重要的意义。 在使用smart pointer 的过程中,要注意的问题: 针 对不同的smart pointer ,有不同的注意事项。比如auto_ptr ,就不能把它用在标准容器里,因为它只在内存中保留一份实例。把握我前面说的两个原则:smart pointer 是类而不是指针,是模仿指针,那么一切问题都好办。比如,smart pointer 作为一个类,那么以下的做法就可能有问题。 SmartPtr p; if(p==0) if(!p) if(p) 很 显然, p 不是一个真正的指针,这么做可能出错。而SmartPtr 的设计也是很重要的因素。 您可以加上一个bool SmartPtr::null() const 来进行判断。如果坚持非要用上面的形式, 那也是可以的。我们就加上operator void* ()试试: template<CLASS&NBSP;T> class SmartPtr { public: ... operator void*() const {return the_p;} ... private: T* the_p; }; 这种方法在basic_ios 中就使用过了。这里也可以更灵活地处理,比如类本身需要operator void*()这样地操作,那么上面这种方法就不灵了。但我们还有重载operator !()等等方法来实现。 总结smart pointer的实质: smart pointer 的实质就是一个外壳,一层包装。正是多了这层包装,我们可以做出许多普通指针无法完成的事,比如前面资源自动回收,或者自动进行引用记数,比如ATL 中CComPtr 和 CComQIPtr 这两个COM 接口指针类。然而也会带来一些副作用,正由于多了这些功能,又会使 smart pointer 丧失一些功能。 WIN结构化异常 对使用WIN32平台的人来说,对WIN的结构化异常应该要有所了解的。 WINDOWS的结构化异常是操作系统的一部分,而C++异常只是C++的一部分,当我们用C++编写代码的时候,我们选择C++的标准异常(也可以用 MS VC的异常),编译器会自动的把我们的C++标准异常转化成SEH异常。 微软的Visual C++也支持C + +的异常处理,并且在内部实现上利用了已经引入到编译程序和Wi n d o w s操作系统的结构化异常处理的功能。 S E H实际包含两个主要功能:结束处理( termination handling)和异常处理( e x c e p t i o nh a n d l i n g)。 在MS VC的FAQ中有关于SHE的部分介绍,这里摘超其中的一句: “在VC5中,增加了新的/EH编译选项用于控制C++异常处理。C++同步异常处理(/EH)使得编译器能生成更少的代码,/EH也是VC的缺省模型。” 一定要记得在背后的事情:在使用SHE的时候,编译程序和操作系统直接参与了程序代码的执行。 Win32异常事件的理解 我写的另一篇文章:内存处理和DLL技术也涉及到了SHE中的异常处理。 Exception(异常处理) 分成软件和硬件exception 2种。如:一个无效的参数或者被0除都会引起软件exception,而访问一个尚未commit的页会引起硬件exception. 发生异常的时候,执行流程终止,同时控制权转交给操作系统,OS会用上下文(CONTEXT)结构把当前的进程状态保存下来,然后就开始search 一个能处理exception的组件,search order如下: 1、 首先检查是否有一个调试程序与发生exception的进程联系在一起,推算这个调试程序是否有能力处理 2、 如上面不能完成,操作系统就在发生exception event的线程中search exception event handler 3、 search与进程关联在一起的调试程序 4、 系统执行自己的exception event handler code and terminate process 结束处理程序利用S E H,你可以完全不用考虑代码里是不是有错误,这样就把主要的工作同错误处理分离开来。这样的分离,可以使你集中精力处理眼前的工作,而将可能发生的错误放在后面处理。 微软在Wi n d o w s中引入S E H的主要动机是为了便于操作系统本身的开发。操作系统的开发人员使用S E H,使得系统更加强壮。我们也可以使用S E H,使我们的自己的程序更加强壮。 使用S E H所造成的负担主要由编译程序来承担,而不是由操作系统承担。 当异常块(exception block)出现时,编译程序要生成特殊的代码。编译程序必须产生一些表( t a b l e)来支持处理S E H的数据结构。 编译程序还必须提供回调( c a l l b a c k)函数,操作系统可以调用这些函数,保证异常块被处理。 编译程序还要负责准备栈结构和其他内部信息,供操作系统使用和参考。在编译程序中增加S E H支持不是一件容易的事。不同的编译程序厂商会以不同的方式实现S E H,这一点并不让人感到奇怪。幸亏我们可以不必考虑编译程序的实现细节,而只使用编译程序的S E H功能。(其实大多数编译程序厂商都采用微软建议的语法) 结束处理程序代码初步一个结束处理程序能够确保去调用和执行一个代码块(结束处理程序,termination handler), 而不管另外一段代码(保护体, guarded body)是如何退出的。结束处理程序的语法结构如下: __try { file://保护块 } __finally { file://结束处理程序 } 在 上面的代码段中,操作系统和编译程序共同来确保结束处理程序中的__f i n a l l y代码块能够被执行,不管保护体(t r y块)是如何退出的。不论你在保护体中使用r e t u r n,还是g o t o,或者是longjump,结束处理程序(f i n a l l y块)都将被调用。 ===================== ************************ 我们来看一个实列:(返回值:10, 没有Leak,性能消耗:小) DWORD Func_SEHTerminateHandle() { DWORD dwReturnData = 0; HANDLE hSem = NULL; const char* lpSemName = "TermSem"; hSem = CreateSemaphore(NULL, 1, 1, lpSemName); __try { WaitForSingleObject(hSem,INFINITE); dwReturnData = 5; } __finally { ReleaseSemaphore(hSem,1,NULL); CloseHandle(hSem); } dwReturnData += 5; return dwReturnData; } 这段代码应该只是做为一个基础函数,我们将在后面修改它,来看看结束处理程序的作用: ==================== 在代码加一句:(返回值:5, 没有Leak,性能消耗:中下) DWORD Func_SEHTerminateHandle() { DWORD dwReturnData = 0; HANDLE hSem = NULL; const char* lpSemName = "TermSem"; hSem = CreateSemaphore(NULL, 1, 1, lpSemName); __try { WaitForSingleObject(hSem,INFINITE); dwReturnData = 5; return dwReturnData; } __finally { ReleaseSemaphore(hSem,1,NULL); CloseHandle(hSem); } dwReturnData += 5; return dwReturnData; } 在t r y块的末尾增加了一个r e t u r n语句。这个r e t u r n语句告诉编译程序在这里要退出这个函数并返回d w Te m p变量的内容,现在这个变量的值是5。但是,如果这个r e t u r n 语句被执行,该线程将不会释放信标,其他线程也就不能再获得对信标的控制。可以想象,这样的执行次序会产生很大的问题,那些等待信标的线程可能永远不会恢复执行。通 过使用结束处理程序,可以避免r e t u r n语句的过早执行。当r e t u r n语句试图退出t r y块时,编译程序要确保f i n a l l y块中的代码首先被执行。要保证f i n a l l y块中的代码在t r y块中的r e t u r n语句退出之前执行。在程序中,将R e l e a s e S e m a p h o r e的调用放在结束处理程序块中,保证信标总会被释放。这样就不会造成一个线程一直占有信标,否则将意味着所有其他等待信标的线程永远不会被分配C P U时间。 在f i n a l l y块中的代码执行之后,函数实际上就返回。任何出现在f i n a l l y块之下的代码将不再执行,因为函数已在t r y块中返回。所以这个函数的返回值是5,而不是10。 读 者可能要问编译程序是如何保证在t r y块可以退出之前执行f i n a l l y块的。当编译程序检查源代码时,它看到在t r y块中有r e t u r n语句。这样,编译程序就生成代码将返回值(本例中是5)保存在一个编译程序建立的临时变量中。编译程序然后再生成代码来执行f i n a l l y块中包含的指令,这称为局部展开。更特殊的情况是,由于t r y块中存在过早退出的代码,从而产生局部展开,导致系统执行f i n a l l y块中的内容。在f i n a l l y块中的指令执行之后,编译程序临时变量的值被取出并从函数中返回。 可以看到,要完成这些事情,编译程序必须生成附加的代码,系统要执行额外的工作。在不 同的C P U上,结束处理所需要的步骤也不同。例如,在A l p h a处理器上,必须执行几百个甚至几千个C P U指令来捕捉t r y块中的过早返回并调用f i n a l l y块。在编写代码时,就应该避免引起结束处理程序的t r y块中的过早退出,因为程序的性能会受到影响。 后面,将讨论_ _ l e a v e关键字,它有助于避免编写引起局部展开的代码。 设计异常处理的目的是用来捕捉异常的—不常发生的语法规则的异常情况(在我们的例子中,就是过早返回)。如果情况是正常的,明确地检查这些情况,比起依赖操作系统和编译程序的S E H功能来捕捉常见的事情要更有效。 注 意当控制流自然地离开t r y块并进入f i n a l l y块(就像在F u n c e n s t e i n 1中)时,进入f i n a l l y块的系统开销是最小的。在x86 CPU上使用微软的编译程序,当执行离开try 块进入f i n a l l y块时,只有一个机器指令被执行,读者可以在自己的程序中注意到这种系统开销。当编译程序要生成额外的代码,系统要执行额外的工作时系统开销就很值得注意 了 ======================== 修改代码:(返回值:5,没有Leak,性能消耗:中) DWORD Func_SEHTerminateHandle() { DWORD dwReturnData = 0; HANDLE hSem = NULL; const char* lpSemName = "TermSem"; hSem = CreateSemaphore(NULL, 1, 1, lpSemName); __try { WaitForSingleObject(hSem,INFINITE); dwReturnData = 5; if(dwReturnData == 5) goto ReturnValue; return dwReturnData; } __finally { ReleaseSemaphore(hSem,1,NULL); CloseHandle(hSem); } dwReturnData += 5; ReturnValue: return dwReturnData; } 代 码中,当编译程序看到t r y块中的g o t o语句,它首先生成一个局部展开来执行f i n a l l y块中的内容。这一次,在f i n a l l y块中的代码执行之后,在R e t u r n Va l u e标号之后的代码将执行,因为在t r y块和f i n a l l y块中都没有返回发生。这里的代码使函数返回5。而且,由于中断了从t r y块到f i n a l l y块的自然流程,可能要蒙受很大的性能损失(取决于运行程序的C P U) ************************* ===================== 写上面的代码是初步的,现在来看结束处理程序在我们代码里面的真正的价值:看代码:(信号灯被正常释放,reserve的一页内存没有被Free,安全性:安全) DWORD TermHappenSomeError() { DWORD dwReturnValue = 9; DWORD dwMemorySize = 1024; char* lpAddress; lpAddress = (char*)VirtualAlloc(NULL, dwMemorySize, MEM_RESERVE, PAGE_READWRITE); finally块的总结性说明 我们已经明确区分了强制执行f i n a l l y块的两种情况: • 从t r y块进入f i n a l l y块的正常控制流。 • 局部展开:从t r y块的过早退出(g o t o、l o n g j u m p、c o n t i n u e、b r e a k、r e t u r n等)强制控制转移到f i n a l l y块。 第 三种情况,全局展开( global unwind),在发生的时候没有明显的标识,我们在本章前面Func_SEHTerminate函数中已经见到。在Func_SEHTerminate 的t r y块中,有一个对TermHappenSomeError函数的调用。TermHappenSomeError函数会引起一个内存访问违规( memory access violation ),一个全局展开会使Func_SEHTerminate函数的f i n a l l y块执行。 由 于以上三种情况中某一种的结果而导致f i n a l l y块中的代码开始执行。为了确定是哪一种情况引起f i n a l l y块执行,可以调用内部函数AbnormalTermination:这个内部函数只在f i n a l l y块中调用,返回一个B o o l e a n值。指出与f i n a l l y块相结合的t r y块是否过早退出。换句话说,如果控制流离开t r y块并自然进入f i n a l l y块,AbnormalTermination将返回FA L S E。如果控制流非正常退出t r y块—通常由于g o t o、r e t u r n、b r e a k或c o n t i n u e语句引起的局部展开,或由于内存访问违规或其他异常引起的全局展开—对AbnormalTermination的调用将返回T R U E。没有办法区别f i n a l l y块的执行是由于全局展开还是由于局部展开。但这通常不会成为问题,因为可以避免编写执行局部展开的代码。(注意内部函数是编译程序识别的一种特殊函数。 编译程序为内部函数产生内联(i n l i n e)代码而不是生成调用函数的代码。例如, m e m c p y是一个内部函数(如果指定/ O i编译程序开关)。当编译程序看到一个对m e m c p y的调用,它直接将m e m c p y的代码插入调用m e m c p y的函数中,而不是生成一个对m e m c p y函数的调用。其作用是代码的长度增加了,但执行速度加快了。 在继续之前,回顾一下使用结束处理程序的理由: • 简化错误处理,因所有的清理工作都在一个位置并且保证被执行。 • 提高程序的可读性。 • 使代码更容易维护。 • 如果使用得当,具有最小的系统开销。 异常处理程序异 常是我们不希望有的事件。在编写程序的时候,程序员不会想去存取一个无效的内存地址或用0来除一个数值。不过,这样的错误还是常常会发生的。C P U负责捕捉无效内存访问和用0除一个数值这种错误,并相应引发一个异常作为对这些错误的反应。C P U引发的异常,就是所谓的硬件异常( hardware exception)。在本章的后面,我们还会看到操作系统和应用程序也可以引发相应的异常,称为软件异常( software exception)。 当出现一个硬件或软件异常时,操作系统向应用程序提供机会来考察是什么类型的异常被引发,并能够让应用程序自己来处理异常。下面就是异常处理程序的语法: __try { file://保护块 } __except(异常过虑器) { file://异常处理程序 } 注 意__ e x c e p t关键字。每当你建立一个t r y块,它必须跟随一个f i n a l l y块或一个e x c e p t块。一个try 块之后不能既有f i n a l l y块又有e x c e p t块。但可以在t r y - e x c e p t块中嵌套t r y - f i n a l l y块,反过来也可以。 异常处理程序代码初步与结束处理程序不同,异常过滤器( exception filter)和异常处理程序是通过操作系统直接执行的,编译程序在计算异常过滤器表达式和执行异常处理程序方面不做什么事。下面几节的内容举例说明t r y - e x c e p t块的正常执行,解释操作系统如何以及为什么计算异常过滤器,并给出操作系统执行异常处理程序中代码的环境。 本来想把代码全部写出来的,但是实在是写这边文挡化的时间太长了,所以接下来就只是做说明,而且try和except块比较简单。 尽 管在结束处理程序的t r y块中使用r e t u r n、g o t o、c o n t i n u e和b r e a k语句是被强烈地反对,但在异常处理程序的t r y块中使用这些语句不会产生速度和代码规模方面的不良影响。这样的语句出现在与e x c e p t块相结合的t r y块中不会引起局部展开的系统开销 当引发了异常时,系统将定位到e x c e p t块的开头,并计算异常过滤器表达式的值,过滤器表达式的结果值只能是下面三个标识符之一,这些标识符定义在windows的Except. h文件中。标识符定义为: EXCEPTION_CONTINUE_EXECUTION (–1) Exception is dismissed. Continue execution at the point where the exception occurred. EXCEPTION_CONTINUE_SEARCH (0) Exception is not recognized. Continue to search up the stack for a handler, first for containing try-except statements, then for handlers with the next highest precedence. EXCEPTION_EXECUTE_HANDLER (1) Exception is recognized. Transfer control to the exception handler by executing the __except compound statement, then continue execution at the assembly instruction that was executing when the exception was raised. 下面将讨论这些标识符如何改变线程的执行。下面的流程概括了系统如何处理一个异常的情况:(这里的流程假设是正向的) ***** 开始 -> 执行一个CPU指令 -> {是否有异常被引发} -> 是 -> 系统确定最里层的try 块 -> {这个try块是否有一个except块} -> 是 -> {过滤器表达式的值是什么} ->异常执行处理程序 -> 全局展开开始 -> 执行except块中的代码 -> 在except块之后执行继续***** EXCEPTION_EXECUTE_HANDLER 在 异常过滤器表达式的值如果是EXCEPTION_EXECUTE_HANDLER,这个值的意思是要告诉系统:“我认出了这个异常。即,我感觉这个异常可 能在某个时候发生,我已编写了代码来处理这个问题,现在我想执行这个代码。”在这个时候,系统执行一个全局展开,然后执行向except块中代码(异常处 理程序代码)的跳转。在except块中代码执行完之后,系统考虑这个要被处理的异常并允许应用程序继续执行。这种机制使windows应用程序可以抓住 错误并处理错误,再使程序继续运行,不需要用户知道错误的发生。但是,当except块执行后,代码将从何处恢复执行?稍加思索,我们就可以想到几种可能 性: 第一种可能性是从产生异常的CPU指令之后恢复执行。这看起来像是合理的做法,但实际上,很多程序的编写方式使得当前面的指令出错时,后续的 指令不能够继续成功地执行。代码应该尽可能地结构化,这样,在产生异常的指令之后的CPU指令有望获得有效的返回值。例如,可能有一个指令分配内存,后面 一系列指令要执行对该内存的操作。如果内存不能够被分配,则所有后续的指令都将失败,上面这个程序重复地产生异常。所幸的是,微软没有让系统从产生异常的 指令之后恢复指令的执行。这种决策使我们免于面对上面的问题。 第二种可能性是从产生异常的指令恢复执行。这是很有意思的可能性。如果在except块中 有 这样的语句会怎么样呢:在except块中有了这个赋值语句,可以从产生异常的指令恢复执行。这一次,执行将继续,不会产生其他的异常。可以做些修改,让 系统重新执行产生异常的指令。你会发现这种方法将导致某些微妙的行为。我们将在EXCEPTION_CONTINUE_EXECUTION一节中讨论这种 技术。 第三种可能性是从except块之后的第一条指令开始恢复执行。这实际是当异常过滤器表达式的值为EXCEPTION_EXECUTE_HANDLER时所发生的事。在except块中的代码结束执行后,控制从except块之后的第一条指令恢复。
usidc5
2010-09-19 00:09
从语法上看,在函数里声明参数与在catch子句中声明参数是一样的,catch里的参数可以是值类型,引用类型,指针类型。例如: try { ..... } catch(A a) { } catch(B& b) { } catch(C* c) { } 尽管表面是它们是一样的,但是编译器对二者的处理却又很大的不同。调用函数时,程序的控制权最终还会返回到函数的调用处,但是抛出一个异常时,控制权永远不会回到抛出异常的地方。 class A; void func_throw() { A a; throw a; //抛出的是a的拷贝,拷贝到一个临时对象里 } try { func_throw(); } catch(A a) //临时对象的拷贝 { } 当 我们抛出一个异常对象时,抛出的是这个异常对象的拷贝。当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型 (static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。此时对象会丢失RTTI信息。 异常是其它对象的拷贝,这个事实影响到你如何在catch块中再抛出一个异常。比如下面这两个catch块,乍一看好像一样: catch (A& w) // 捕获异常 { // 处理异常 throw; // 重新抛出异常,让它继续传递 } catch (A& w) // 捕获Widget异常 { // 处理异常 throw w; // 传递被捕获异常的拷贝 } 第一个块中重新抛出的是当前异常(current exception),无论它是什么类型。(有可能是A的派生类) 第二个catch块重新抛出的是新异常,失去了原来的类型信息。 一般来说,你应该用throw来重新抛出当前的异常,因为这样不会改变被传递出去的异常类型,而且更有效率,因为不用生成一个新拷贝。 看看以下这三种声明: catch (A w) ... // 通过传值 catch (A& w) ... // 通过传递引用 catch (const A& w) ... //const引用 一个被异常抛出的对象(总是一个临时对象)可以通过普通的引用捕获;它不需要通过指向const对象的引用(reference-to-const)捕获。在函数调用中不允许转递一个临时对象到一个非const引用类型的参数里,但是在异常中却被允许。 回到异常对象拷贝上来。我们知道,当用传值的方式传递函数的参数,我们制造了被传递对象的一个拷贝,并把这个拷贝存储到函数的参数里。同样我们通过传值的方式传递一个异常时,也是这么做的当我们这样声明一个catch子句时: catch (A w) ... // 通过传值捕获 会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,第二个是把临时对象拷贝进w中。实际上,编译器会优化掉一个拷贝。同样,当我们通过引用捕获异常时, catch (A& w) ... // 通过引用捕获 catch (const A& w) ... //const引用捕获 这仍旧会建立一个被抛出对象的拷贝:拷贝是一个临时对象。相反当我们通过引用传递函数参数时,没有进行对象拷贝。话虽如此,但是不是所有编译器都如此。VS200就表现很诡异。 通 过指针抛出异常与通过指针传递参数是相同的。不论哪种方法都是一个指针的拷贝被传递。你不能认为抛出的指针是一个指向局部对象的指针,因为当异常离开局部 变量的生存空间时,该局部变量已经被释放。Catch子句将获得一个指向已经不存在的对象的指针。这种行为在设计时应该予以避免。 另外一个重要的差异是在函数调用者或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不同。在函数传递参数时,如果参数不匹配,那么编译器会尝试一个类型转换,如果存在的话。而对于异常处理的话,则完全不是这样。见一下的例子: void func_throw() { CString a; throw a; //抛出的是a的拷贝,拷贝到一个临时对象里 } try { func_throw(); } catch(const char* s) { } 抛出的是CString,如果用const char*来捕获的话,是捕获不到这个异常的。 尽管如此,在catch子句中进行异常匹配时可以进行两种类型转换。第一种是基类与派生类的转换,一个用来捕获基类的catch子句也 可以处理派生类类型的异常。反过来,用来捕获派生类的无法捕获基类的异常。第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常: catch (const void*) ... //可以捕获所有指针异常 另外,你还可以用catch(...)来捕获所有异常,注意是三个点。 传递参数和传递异常间最后一点差别是catch子句匹配顺序总是取决于它们在程序中出现的顺序。因此一个派生类异常可能被处 理其基类异常的catch子句捕获,这叫异常截获,一般的编译器会有警告。 class A { public: A() { cout << "class A creates" << endl; } void print() { cout << "A" << endl; } ~A() { cout << "class A destruct" << endl; } }; class B: public A { public: B() { cout << "class B create" << endl; } void print() { cout << "B" << endl; } ~B() { cout << "class B destruct" << endl; } }; void func() { B b; throw b; } try { func(); } catch( B& b) //必须将B放前面,如果把A放前面,B放后面,那么B类型的异常会先被截获。 { b.print(); } catch (A& a) { a.print() ; } 相反的是,当你调用一个虚拟函数时,被调用的函数位于与发出函数调用的对象的动态类型(dynamic type)最相近的 类里。你可以这样说虚拟函数匹配采用最优匹配法,而异常处理匹配采用的是最先匹配法。
usidc5
2011-11-26 13:12
SEH的优点在于当你编写代码的时候,可以集中注意力完成任务,如果在运行的时候出现了问题,系统会捕获到错误并且通知你出了问题. 使 得SEH工作的任务主要落在编译器而不是操作系统上.当进入,退出异常块时候,你的编译器必须产生特殊的代码.编译器必须产生处理SEH的数据结构,还必 须提供操作系统能调用的回调函数来使得异常块能被遍历.编译器还负责准备栈框架何别的供操作系统使用和引用的内部信息. 不同厂家的编译器对实现SEH的方法也许不一样,但功能上大致是一样的,所以我们这里可以忽略SEH具体的实现细节.大多数厂家都遵循了Mircosoft建议的语法. 这 里一定要记住:千万不用把SEH同C++的异常处理混淆.C++的异常处理是另外一种异常处理的形式,他使用C++中的try,catch和throw关 键字.microsoft 在Visual C++ 2.0的时候加入了C++异常处理.关于C++的异常处理我想在下一篇文章来阐述. (SEH)结构化异常处理中的关键字: __try,__finally,__leave,__execept (SEH)结构化异常处理的功能分类: 1终止处理程序,2异常过滤程序,3异常处理程序.,4软件异常. 下面就对这四种形式加以讨论.首先来看看终止处理程序.所谓的终止处理程序,是__try {} __finally{}块.他的microsoft 语法是:(以下如果没有特殊说明,指的都是mircosoft的语法) __try { //Guaded body ... } __fianlly { // Termination handler ... } 关键字 __try 和__finally描述终止处理程序的两个部分.在上面的代码中,操作系统和编译器一起确保终止处理程序的中__finally代码块被执行,而不管被保护体__try是怎样退出的. 也就是就算你在__try中加如了return,goto,或者longjump,终止程序都会被执行.下面是代码执行的流程: //1 先于__try块的代码被执行 __try{ //2 __try中的代码被执行 } __finally{ //3 __fianlly中代码被执行 } // 4 __fianlly后面的代码被执行. 也许这样的描述还不够清楚,是的,要了解SEH是如何工作的,最好的办法就是写段代码来测试一下: DWORD Fun(void) { DWORD dwTemp; // DO any thing u want here __try { WaitForSingleObject(G_hSign,INFINITE); dwTemp = 5; return dwTemp; } __finally { ReleaseSemaphore(G_hSign,1,NULL); } dwTemp = 9; return dwTemp; } 也许你已经发现在__try中有一个return语句,如果__finally不被执行的话,那么信号量就不会释放,程序的流程就出问题了.请记住SEH终止处理程序保证在__try中过早退出时,__finally中代码一定会被执行,然后才返回. 这被称为局部展开(local unwind) 如果真的想早点跳出__try 块,请使用__leave这个关键字。他的用法如下: __try { // condition test __leave; //another conditon test __leave; //so on } __finally { //do some clean up job here } __leave关键字的含义是: 在__try中使用__leave关键字将导致一个到__try快末尾的跳转,而后进入到__finally 块。 __finally块中的代码被执行有三种可能性: 1 程序自然的从__try中进入__try. 2 从try块中过早的退出如使用了goto, longjump,continue,break,return等强迫控制转移到__finally块。 3 是全局展开,不用显式的表明,比如我们在__try 块中调用了一个函数,而这个函数引起了内存访问错误 那么__finally块中的代码也会被执行。如下面的代码 void Dofnc() { char* buffer = 0; *buffer = 0x30; } __try { Dofnc(); //这个函数引起一个内存访问错误。 //其他的代码 } __finally { //do clean up job here } BOOL AbnormalTermintion(void) 这个函数可以使我得知程序是正常进入还是异常(全局,局部)进入__finally块。 AbnormalTermintion返回TRUE 表示异常进入,FALSE表示正常进入__finally
usidc5
2011-11-26 13:13
导读: 从本篇文章开始,将全面阐述 __try,__except,__finally,__leave异常模型机制,它也即是Windows系列操作系统平台上提供的SEH模型。主人公阿 愚将在这里与大家分享SEH( 结构化异常处理)的学习过程和经验总结。 深入理解请参阅<<windows 核心编程>>第23, 24章. SEH实际包含两个主要功能:结束处理(termination handling)和异常处理(exception handling) 每当你建立一个try块,它必须跟随一个finally块或一个except块。 一个try 块之后不能既有finally块又有except块。但可以在try - except块中嵌套try - finally块,反过来 也可以。 __try __finally关键字用来标出结束处理程序两段代码的轮 不管保护体(try块) 是如何退出的。不论你在保护体中使用return,还是goto,或者是longjump,结束处理程序 (finally块)都将被调用。 在try使用__leave关键字会引起跳转到try块的结尾 SEH有两项非常强大的功能。当然,首先是异常处理模型了,因此,这篇文章首先深入阐述SEH提供的异常处理模型。另外,SEH还有一个特别强大的功能,这将在下一篇文章中进行详细介绍。 try-except入门 SEH的异常处理模型主要由try-except语句来完成,它与标准C++所定义的异常处理模型非常类似,也都是可以定义出受监控的代码模块,以及定义异常处理模块等。还是老办法,看一个例子先,代码如下: //seh-test.c
void main() { // 定义受监控的代码模块 __try { puts("in try"); } //定义异常处理模块 __except(1) { puts("in except"); } }
呵呵!是不是很简单,而且与C++异常处理模型很相似。当然,为了与C++异常处理模型相区别,VC编译器对关键字做了少许变动。首先是在每个关键字加上两个下划线作为前缀,这样既保持了语义上的一致性,另外也尽最大可能来避免了关键字的有可能造成名字冲突而引起的麻烦等;其次,C++异常处理模型是使用catch关键字来定义异常处理模块,而SEH是采用__except关键字来定义。并且,catch关键字后面往往好像接受一个函数参数一样,可以是各种类型的异常数据对象;但是__except关键字则不同,它后面跟的却是一个表达式(可以是各种类型的表达式,后面会进一步分析)。 try-except进阶 与C++异常处理模型很相似,在一个函数中,可以有多个try-except语句。它们可以是一个平面的线性结构,也可以是分层的嵌套结构。例程代码如下: // 例程1 // 平面的线性结构
void main() { __try { puts("in try"); } __except(1) { puts("in except"); } // 又一个try-except语句 __try { puts("in try1"); } __except(1) { puts("in except1"); } }
// 例程2 // 分层的嵌套结构
void main() { __try { puts("in try"); // 又一个try-except语句 __try { puts("in try1"); } __except(1) { puts("in except1"); } } __except(1) { puts("in except"); } }
// 例程3 // 分层的嵌套在__except模块中
void main() { __try { puts("in try"); } __except(1) { // 又一个try-except语句 __try { puts("in try1"); } __except(1) { puts("in except1"); } puts("in except"); } }
1. 受监控的代码模块被执行(也即__try定义的模块代码); 2. 如果上面的代码执行过程中,没有出现异常的话,那么控制流将转入到__except子句之后的代码模块中; 3. 否则,如果出现异常的话,那么控制流将进入到__except后面的表达式中,也即首先计算这个表达式的值,之后再根据这个值,来决定做出相应的处理。这个值有三种情况,如下: EXCEPTION_CONTINUE_EXECUTION (–1) 异常被忽略,控制流将在异常出现的点之后,继续恢复运行。 EXCEPTION_CONTINUE_SEARCH (0) 异常不被识别,也即当前的这个__except模块不是这个异常错误所对应的正确的异常处理模块。系统将继续到上一层的try-except域中继续查找一个恰当的__except模块。 EXCEPTION_EXECUTE_HANDLER (1) 异常已经被识别,也即当前的这个异常错误,系统已经找到了并能够确认,这个__except模块就是正确的异常处理模块。控制流将进入到__except模块中。 try-except深入 上面的内容中已经对try-except进行了全面的了解,但是有一点还没有阐述到。那就是如何在__except模块中获得异常错误的相关信息,这非 常关键,它实际上是进行异常错误处理的前提,也是对异常进行分层分级别处理的前提。可想而知,如果没有这些起码的信息,异常处理如何进行?因此获取异常信 息非常的关键。Windows提供了两个API函数,如下: LPEXCEPTION_POINTERS GetExceptionInformation(VOID); DWORD GetExceptionCode(VOID); 其中GetExceptionCode()返回错误代码,而GetExceptionInformation()返回更全面的信息,看它函数的声明,返 回了一个LPEXCEPTION_POINTERS类型的指针变量。那么EXCEPTION_POINTERS结构如何呢?如下, typedef struct _EXCEPTION_POINTERS { // exp PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord; } EXCEPTION_POINTERS; 呵呵!仔细瞅瞅,这是不是和上一篇文章中,用户程序所注册的异常处理的回调函数的两个参数类型一样。是的,的确没错!其中 EXCEPTION_RECORD类型,它记录了一些与异常相关的信息;而CONTEXT数据结构体中记录了异常发生时,线程当时的上下文环境,主要包括 寄存器的值。因此有了这些信息,__except模块便可以对异常错误进行很好的分类和恢复处理。不过特别需要注意的是,这两个函数只能是在__except后面的括号中的表达式作用域内有效,否则结果可能没有保证(至于为什么,在后面深入分析异常模型的实现时候,再做详细阐述)。看一个例程吧!代码如下:
int exception_access_violation_filter(LPEXCEPTION_POINTERS p_exinfo) { if(p_exinfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) { printf("存储保护异常\n"); return 1; } else return 0; } int exception_int_divide_by_zero_filter(LPEXCEPTION_POINTERS p_exinfo) { if(p_exinfo->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO) { printf("被0除异常\n"); return 1; } else return 0; } void main() { __try { __try { int* p; // 下面将导致一个异常 p = 0; *p = 45; } // 注意,__except模块捕获一个存储保护异常 __except(exception_access_violation_filter(GetExceptionInformation())) { puts("内层的except块中"); } //可以在此写除0异常的语句 int b = 0; int a = 1 / b; } // 注意,__except模块捕获一个被0除异常 __except(exception_int_divide_by_zero_filter(GetExceptionInformation())) { puts("外层的except块中"); } }
上面的程序运行结果如下: 存储保护异常内层的except块中 Press any key to continue 呵呵!感觉不错,大家可以在上面的程序基础之上改动一下,让它抛出一个被0除异常,看程序的运行结果是不是如预期那样。 最后还有一点需要阐述,在C++的异常处理模型中,有一个throw关键字,也即在受监控的代码中抛出一个异常,那么在SEH异常处理模型中,是不是也 应该有这样一个类似的关键字或函数呢?是的,没错!SEH异常处理模型中,对异常划分为两大类,第一种就是上面一些例程中所见到的,这类异常是系统异常, 也被称为硬件异常;还有一类,就是程序中自己抛出异常,被称为软件异常。怎么抛出呢?还是Windows提供了的API函数,它的声明如下: VOID RaiseException( DWORD dwExceptionCode, // exception code DWORD dwExceptionFlags, // continuable exception flag DWORD nNumberOfArguments, // number of arguments in array CONST DWORD *lpArguments // address of array of arguments ); 很简单吧!实际上,在C++的异常处理模型中的throw关键字,最终也是对RaiseException()函数的调用,也即是说,throw是 RaiseException的上层封装的更高级一类的函数,这以后再详细分析它的代码实现。这里还是看一个简单例子吧!代码如下:
int seh_filer(int code) { switch(code) { case EXCEPTION_ACCESS_VIOLATION : printf("存储保护异常,错误代码:%x\n", code); break; case EXCEPTION_DATATYPE_MISALIGNMENT : printf("数据类型未对齐异常,错误代码:%x\n", code); break; case EXCEPTION_BREAKPOINT : printf("中断异常,错误代码:%x\n", code); break; case EXCEPTION_SINGLE_STEP : printf("单步中断异常,错误代码:%x\n", code); break; case EXCEPTION_ARRAY_BOUNDS_EXCEEDED : printf("数组越界异常,错误代码:%x\n", code); break; case EXCEPTION_FLT_DENORMAL_OPERAND : case EXCEPTION_FLT_DIVIDE_BY_ZERO : case EXCEPTION_FLT_INEXACT_RESULT : case EXCEPTION_FLT_INVALID_OPERATION : case EXCEPTION_FLT_OVERFLOW : case EXCEPTION_FLT_STACK_CHECK : case EXCEPTION_FLT_UNDERFLOW : printf("浮点数计算异常,错误代码:%x\n", code); break; case EXCEPTION_INT_DIVIDE_BY_ZERO : printf("被0除异常,错误代码:%x\n", code); break; case EXCEPTION_INT_OVERFLOW : printf("数据溢出异常,错误代码:%x\n", code); break; case EXCEPTION_IN_PAGE_ERROR : printf("页错误异常,错误代码:%x\n", code); break; case EXCEPTION_ILLEGAL_INSTRUCTION : printf("非法指令异常,错误代码:%x\n", code); break; case EXCEPTION_STACK_OVERFLOW : printf("堆栈溢出异常,错误代码:%x\n", code); break; case EXCEPTION_INVALID_HANDLE : printf("无效句病异常,错误代码:%x\n", code); break; default : if(code & (1<<29)) printf("用户自定义的软件异常,错误代码:%x\n", code); else printf("其它异常,错误代码:%x\n", code); break; } return 1; } void main() { __try { puts("try块中"); // 注意,主动抛出一个软异常 RaiseException(0xE0000001, 0, 0, 0); } __except(seh_filer(GetExceptionCode())) { puts("except块中"); } }
上面的程序运行结果如下: hello try块中用户自定义的软件异常,错误代码:e0000001 except块中 world Press any key to continue 上 面的程序很简单,这里不做进一步的分析。我们需要重点讨论的是,在__except模块中如何识别不同的异常,以便对异常进行很好的分类处理。毫无疑问, 它当然是通过GetExceptionCode()或GetExceptionInformation ()函数来获取当前的异常错误代码,实际也即是DwExceptionCode字段。异常错误代码在winError.h文件中定义,它遵循 Windows系统下统一的错误代码的规则。每个DWORD被划分几个字段,如下表所示:例如我们可以在winbase.h文件中找到EXCEPTION_ACCESS_VIOLATION的值为0 xC0000005,将这个异常代码值拆开,来分析看看它的各个bit位字段的涵义。 C 0 0 0 0 0 0 5 (十六进制) 1100 0000 0000 0000 0000 0000 0000 0101 (二进制)第 3 0位和第3 1位都是1,表示该异常是一个严重的错误,线程可能不能够继续往下运行,必须要及时处理恢复这个异常。第2 9位是0,表示系统中已经定义了异常代码。第2 8位是0,留待后用。第1 6 位至2 7位是0,表示是FACILITY_NULL设备类型,它代表存取异常可发生在系统中任何地方,不是使用特定设备才发生的异常。第0位到第1 5位的值为5,表示异常错误的代码。 如果程序员在程序代码中,计划抛出一些自定义类型的异常,必须要规划设计好自己的异常类型的划分,按照上面的规则来填充异常代码的各个字段值,如上面示例程序中抛出一个异常代码为0xE0000001软件异常。 总结 (1) C++异常模型用try-catch语法定义,而SEH异常模型则用try-except语法; (2) 与C++异常模型相似,try-except也支持多层的try-except嵌套。 (3) 与C++异常模型不同的是,try-except模型中,一个try块只能是有一个except块;而C++异常模型中,一个try块可以有多个catch块。 (4) 与C++异常模型相似,try-except模型中,查找搜索异常模块的规则也是逐级向上进行的。但是稍有区别的是,C++异常模型是按照异常对象的类型 来进行匹配查找的;而try-except模型则不同,它通过一个表达式的值来进行判断。如果表达式的值为 1(EXCEPTION_EXECUTE_HANDLER),表示找到了异常处理模块;如果值为 0(EXCEPTION_CONTINUE_SEARCH),表示继续向上一层的try-except域中继续查找其它可能匹配的异常处理模块;如果值为 -1(EXCEPTION_CONTINUE_EXECUTION),表示忽略这个异常,注意这个值一般很少用,因为它很容易导致程序难以预测的结果,例 如,死循环,甚至导致程序的崩溃等。 (5) __except关键字后面跟的表达式,它可以是各种类型的表达式,例如,它可以是一个函数调用,或是一个条件表达式,或是一个逗号表达式,或干脆就是一 个整型常量等等。最常用的是一个函数表达式,并且通过利用GetExceptionCode()或GetExceptionInformation ()函数来获取当前的异常错误信息,便于程序员有效控制异常错误的分类处理。 (6) SEH异常处理模型中,异常被划分为两大类:系统异常和软件异常。其中软件异常通过RaiseException()函数抛出。RaiseException()函数的作用类似于C++异常模型中的throw语句。
usidc5
2012-01-09 10:29
异常基本概念 异常即程序在运行期间产生的非预期性错误,如内存配额不足、文件不存在、网络不可用、SQL语句非法等。 异常分为可恢复性异常和不可恢复性异常。 异常是否可恢复,不同的应用场景将有不同的理解。例如,对于内存配合不足,在短暂的峰涌情况下内存能够快速恢复则属于可恢复性异常,因为一但被释放和回收,程序将回到常态;若内存长时间得不到释放,则属于不可恢复性异常,此时应当终止程序的运行。 异常又可分为已知异常和未知异常。已知异常是指程序编写者事先考虑到的异常,对异常有清晰的定义和处理逻辑,并在程序中进行捕捉;未知异常是指程序编写者事先未察觉到的异常,可以理解为程序bug。 异 常捕获是一把双刃剑,对异常进行显示捕获,尤其对于bug型的未知异常,需要找出产生异常的原因和代码进行修复,即需要考虑异常的分析定位机制。因此,对 于未知异常或者无法处理的异常,不应当使用catch(…)笼统的进行捕获,当不需要关心异常或者无法处理异常的时候,就应该避免捕获异常或者将未处理的 异常再次抛出,以便交由bugreport统一对未处理异常进行捕获、分析。 函数的异常声明 在函数的声明后面使用throw关键字可对函数的异常产生进行限制。 不允许函数抛出任何异常 void f() throw(); 表示f函数不允许抛出任何异常;以VS2005编译器为例,若在f函数中抛出异常,编译时会有如下提示: warning C4297: 'f' : function assumed not to throw an exception but does 1> __declspec(nothrow) or throw() was specified on the function 但异常还是能正常抛出和被捕获。 允许函数抛出任意异常 void f() throw(…);表示允许f函数抛出任意异常。 允许函数抛出指定类型的异常 void f() throw(MyException1,MyException2);表示允许函数f抛出MyException1和MyException2两种类型的异常。以vs2005为例,在f中抛出其他类型的异常,编译未见提示,且异常能正常抛出和被捕获。 异常的抛出 使用关键字throw抛出异常,有如下两种语法。 throw express;抛出express所代表的表达式的异常,异常类型为表达式的值的类型,异常的值为表达式的值。例如, 1 try{ 2 3 throw 10; 4 }catch(int e){ 5 6 //progress.... 7 } throw;将异常继续“向上传递”。例如, 1 try{ 2 3 throw 10; 4 }catch(int e){ 5 6 throw... 7 } 异常类型 任何类型都可以作为异常类型,包括基本数据类型,如int、double、char等;还包括类类型,如std:string;还包括自定义异常类型,即从std::exception派生的子类。 异常对象的传递 自定义的异常类:CCustomException 01 #pragma once 02 03 #include <string> 04 using namespace std; 05 06 07 class CCustomException : public std::exception 08 { 09 public: 10 CCustomException(void); 11 CCustomException(const char*); 12 ~CCustomException(void); 13 14 15 std::string getMsg(); 16 void setMsg(std::string msg); 17 virtual const char* what() const; 18 private: 19 std::string m_msg; 20 }; 01 #include "StdAfx.h" 02 #include "CustomException.h" 03 04 CCustomException::CCustomException(void) 05 { 06 } 07 08 CCustomException::~CCustomException(void) 09 { 10 } 11 12 CCustomException::CCustomException(const char* msg) 13 { 14 m_msg = msg; 15 } 16 17 std::string CCustomException::getMsg() 18 { 19 return m_msg; 20 } 21 22 void CCustomException::setMsg(std::string msg) 23 { 24 m_msg = msg; 25 } 26 27 const char* CCustomException::what() const 28 { 29 return m_msg.c_str(); 30 } main函数如下: 01 // ExceptionExample.cpp : 定义控制台应用程序的入口点。 02 // 03 04 #include "stdafx.h" 05 #include "CustomException.h" 06 #include <iostream> 07 08 void exceptionTest1() throw(CCustomException) 09 { 10 throw CCustomException("this is CCustomException Test1!"); 11 return; 12 } 13 14 void exceptionTest2() throw(CCustomException*) 15 { 16 throw new CCustomException("this is CCustomException Test2!"); 17 return; 18 } 19 20 void exceptionTest3() throw(CCustomException&) 21 { 22 throw(CCustomException("this is CCustomException Test3!")); 23 return; 24 } 25 26 void Test1() 27 { 28 try 29 { 30 exceptionTest1(); 31 } 32 catch (std::exception e) 33 { 34 cout<<e.what()<<endl; 35 } 36 } 37 38 void Test2() 39 { 40 try 41 { 42 exceptionTest2(); 43 } 44 catch (std::exception *e) 45 { 46 cout<<e->what()<<endl; 47 delete e; 48 } 49 } 50 51 void Test3() 52 { 53 try 54 { 55 exceptionTest3(); 56 } 57 catch (std::exception &e) 58 { 59 cout<<e.what()<<endl; 60 } 61 } 62 63 int _tmain(int argc, _TCHAR* argv[]) 64 { 65 Test1(); 66 Test2(); 67 Test3(); 68 69 getchar(); 70 return 0; 71 } 输出结果如下: 异常对象的传递和函数参数的传递一样,有值传递、指针传递和引用传递3种传递方式。 值传递会产生临时对象拷贝,且不支持多态。,虽然CCustomException是std::exception的子类,但是仍然不能调用CCustomException的what函数,输出了Unknown Message。 指针传递 指针传递可以实现多态,但会将临时对象的地址作为指针传出去,出现悬挂指针错误。如果在堆上分配内存空间,又不知道何时恰当的删除对象,二来代码可读性、可维护性遭到破坏,违反了内存空间从哪里申请得到就在哪里释放的内存分配释放原则。 值引用传递既能避免临时对象的拷贝,又能支持多态,且没有对象释放问题。推荐采用值引用的方式传递异常对象。输出结果为:This is CCustomException Test3. catch的匹配规则 异 常匹配采用自底向上的匹配顺序进行匹配,即先在异常产生的函数中进行匹配,若该造成该异常的代码没有被try…catch…捕获或者catch类型不匹 配,则向上传递直到匹配到第一条catch结束匹配,或则未发现任何匹配的catch则产生未处理异常交由运行时环境处理。 对于基本数据类型的异常,采用严格类型匹配机制,不支持类型转换 对于自定义类型,基类类型能够匹配子类异常对象,子类类型不能匹配基本异常对象。 C++提供了强大的异常处理机制将异常产生和异常处理的代码分离,即将程序的正常业务逻辑和错误处理逻辑进行分离,并在不改变函数原型的情况下,实现异常对象的“向上”传递。 C++ 的异常处理机制有3部分组成:try(检查),throw(抛出),catch(捕获)。把需要检查的语句放在try模块中,检查语句发生错 误,throw抛出异常,发出错误信息,由catch来捕获异常信息,并加以处理。一般throw抛出的异常要和catch所捕获的异常类型所匹配。异常 处理的一般格式为: 01 try 02 { 03 被检查语句 04 throw 异常 05 } 06 catch(异常类型1) 07 { 08 进行异常处理的语句1 09 } 10 catch(异常类型2) 11 { 12 进行异常处理的语句2 13 }
usidc5
2012-02-25 14:38
异常:程序执行期间,可检测到的不正常情况。例如:0作除数;数组下标越界;打开不存在的文件;远程机器连接超时;malloc失败等等。程序的两种状态:正常状态和异常状态,发生不正常情况后,进入异常状态,从当前函数开始,按调用链的相反次序,查找处理该异常的程序片断。 1.throw 表达式语义:用表达式的值生成一个对象(异常对象),程序进入异常状态。 Terminate函数,终止程序的执行。 2.try-catch语句 try{ 包含可能抛出异常的语句; }catch(类型名 [形参名]){ }catch(类型名 [形参名]){ } 例子程序: #include <iostream> #include <math.h> using namespace std; double sqrt_delta(double d){ if(d < 0) throw 1; return sqrt(d); } double delta(double a, double b, double c){ double d = b * b - 4 * a * c; return sqrt_delta(d); } void main() { double a, b, c; cout << "please input a, b, c" << endl; cin >> a >> b >> c; while(true){ try{ double d = delta(a, b, c); cout << "x1: " << (d - b) / (2 * a); cout << endl; cout << "x2: " << -(b + d) / (2 * a); cout << endl; break; }catch(int){ cout << "delta < 0, please reenter a, b, c."; cin >> a >> b >> c; } } } 3.重新抛出异常语法: throw; 语义:重新抛出原有的异常对象。如果在throw后面有表达式,则抛出新的异常对象。例子程序: #include <iostream> using namespace std; void fun(int x){ try{ if(x == 1) throw 1; if(x == 2) throw 1.0; if(x == 3) throw ''1''; }catch(int){ cout << "catch an int in fun()" << endl; }catch(double){ cout << "catch an double in fun()" << endl; } cout << "testing exception in fun()..."<< endl; } void gun() { try{ //fun(1); //fun(2); //fun(3); fun(4); }catch(char){ cout << "catch a char in gun()" << endl; } cout << "testing exception in gun()..."<< endl; } int main() { gun(); } 4.扑获所有异常 catch(...){ } 下面的程序是不对的: error C2311: ''int'' : is caught by ''...'' on line 7 #include <iostream> using namespace std; void fun() { try{ }catch(...){ cout << "catch all exception ..." << endl; }catch(int){ cout << "catch int exception ..." << endl; } } 5.异常规范指出函数可以抛出的所有异常类型名。语法:值类型 函数名(形参表) throw(类型名表) 函数体空异常规范表示不抛出异常;例如: warning C4297: ''function'' : function assumed not to throw an exception but does __declspec(nothrow) or throw() was specified on the function #include <io stream> using namespace std; void function(int x) throw() { if(x == 1) throw 1; } 无异常规范表示可抛出任何异常。异常规范违例,在函数的声明中并没有声明抛出该类异常,但在程序中却抛出了该类的异常?例如: warning C4290: C++ exception specification ignored except to indicate a function is not __declspec(nothrow) void function(int x) throw (int) { if(x == 1) throw 1.5; } 注:在g++中并未警告。对于函数指针,例如: #include <iostream> using namespace std; void function(int x)throw(int) { if(x == 1) throw 1; } int main() { void (*fp)(int)throw(char); fp = function; fp(1); } 同样的,在g++中没有警告,但在vc8中提出警告: warning C4290: C++ exception specification ignored except to indicate a function is not __declspec(nothrow) pan> <补充> 异常规范违例,例子程序如下: #include <iostream> using namespace std; class A { }; void function(int x)throw(int) //void function(int x)throw(A*) { if(x == 1) throw new A; } void test() throw (A* ) { //void (*fp)(int)throw(A); void (*fp)(int)throw(int); fp = function; try{ fp(1); }catch(int) { cout << "test" << endl; throw; } } int main() { try{ test(); }catch(A*){ cout << "test in main" <<endl; } return 0; } 这个代码在vc8和g++环境中的运行结果不同? <补充> 1.异常处理仅仅通过类型而不是通过值来匹配的,否则又回到了传统的错误处理技术上去了,所以catch块的参数可以没有参数名称,只需要参数类型,除非要使用那个参数。 2.虽然异常对象看上去像局部对象,但并非创建在函数栈上,而是创建在专用的异常栈上,因此它才可以跨接多个函数而传递到上层,否则在栈清空的过程中就会被销毁。 3.函 数原型中的异常说明要与实现中的异常说明一致,否则容易引起异常冲突。由于异常处理机制是在运行时有异常时才发挥作用的,因此如果函数的实现中抛出了没有 在其异常说明列表中列出的异常,则编译器并不能检查出来。但是当运行时如果真的抛出了这样的异常,就会导致异常冲突。因为你没有提示函数的调用者:该函数 会抛出一种没有被说明的即不期望的异常,于是异常处理机制就会检测到这个冲突并调用标准库函数unexcepted(),unexcepted()的默认行为就是调用terminate()来结束程序。 实际工作中使用set_unexcepter()来预设一个回调函数。 4.当异常抛出时局部对象如何释放? Bjarne Stroustrup引入了“resource acquistion is initialization”思想,异常处理机制保证:所有从try到throw语句之间构造起来的局部对象的析构函数将被自动调用,然后清退堆栈(就像函数正常退出一样)。如果一直上溯到main函数后还没有找到匹配的catch块,那么系统调用terminate()终止整个程序,这种情况下不能保证所有局部对象会被正确地销毁。 5.catch块的参数应采用引用传递而不是值传递,不仅可以提高效率,还可以利用对象的多态性。另外,派生类的异常扑获要放到父类异常扑获的前面,否则,派生类的异常无法被扑获。catch(void *)要放到catch(...)前面。 6.编写异常说明时,要确保派生类成员函数的异常说明和基类成员函数的异常说明一致,即派生类改写的虚函数的异常说明至少要和对应的基类虚函数的异常说明相同,甚至更加严格,更特殊。
usidc5
2012-02-25 14:57
try{} catch(…){} 以前都是用try{} catch(…){}来捕获C++中一些意想不到的异常, 今天看了Winhack的帖子才知道,这种方法在VC中其实是靠不住的。例如下面的代码: try { BYTE* pch ; pch = ( BYTE* )00001234 ; //给予一个非法地址 *pch = 6 ; //对非法地址赋值,会造成Access Violation 异常 } catch(...) { AfxMessageBox( "catched" ) ; } 这 段代码在debug下没有问题,异常会被捕获,会弹出”catched”的消息框。 但在Release方式下如果选择了编译器代码优化选项,则VC编译器会去搜索try块中的代码, 如果没有找到throw代码, 他就会认为try catch结构是多余的, 给优化掉。 这样造成在Release模式下,上述代码中的异常不能被捕获,从而迫使程序弹出错误提示框退出。那么能否在release代码优化状态下捕获这个异常呢, 答案是有的。 就是__try, __except结构, 上述代码如果改成如下代码异常即可捕获。 __try { BYTE* pch ; pch = ( BYTE* )00001234 ; //给予一个非法地址 *pch = 6 ; //对非法地址赋值,会造成Access Violation 异常 } __except( EXCEPTION_EXECUTE_HANDLER ) { AfxMessageBox( "catched" ) ; } 但是用__try, __except块还有问题, 就是这个不是C++标准, 而是Windows平台特有的扩展。 而且如果在使用过程中涉及局部对象析构函数的调用,则会出现C2712 的编译错误。 那么还有没有别的办法呢?当然有, 就是仍然使用C++标准的try{}catch(..){}, 但在编译命令行中加入 /EHa 的参数。这样VC编译器不会把try catch模块给优化掉了。
usidc5
2012-02-25 14:58
VC++ Runtime Error, 对不少朋友来说, 这是一个十分讨厌的错误提示, 您可能不知道如何着手调试: 产生这个错误的原因是什么? 确实只有知道了产生这个错误的直接原因, 才能去调试这个错误. 刚碰到这个错误的时候, 是发生在视频解码的时候, 由于解码一直在工作状态, 所以我也不知道如何去调试, 当出现这个错误之后, 我们大多数时候就忽略了, 想从其他地方解决, 提高稳定性, 甚至怀疑解码器的稳定性; 后来, 我接触解码库之后, 开始调试这样的错误, 刚开始这样的错误并不容易重现, 往往要几个小时, 当这个错误重现之后, 程序还是在运行的, 只是其中的某一个线程中断了执行, 其中的这个线程弹出了 "VC++ Runtime Error" 这样的对话框, 如果你点击它, 则整个应用程序会直接退出. 为了调试, 我就不能点击这个对话框, 而是使用VC2005附件到进程, 然后再直接中断进程, 这个时候, 会有一个线程中断点就在对话框的消息循环中, 仔细查看堆栈, 发现了一个函数: msvcrt.dll!_abort() , 到这里是时候查看MSDN了: 函数名: abort 功 能: 异常终止一个进程 用 法: void abort(void); In a single or multithreaded Windows-based application, abort calls the Windows MessageBox function to create a message box to display the message with an OK button. When the user clicks OK, the program aborts immediately. 我们的程序就是基于WINDOS窗口的多线程应用程序, 调用了abort就会弹出对话框, 在release版本中, 就是一个确认对话框, 点击后程序就提示出错并退出. 在 正常的程序里, 我们是不会调用abort的, 除非是遇到了严重的, 不能恢复的错误. 那么到底这个abort是怎么被调用的呢, 我们自己写的代码显然是没有这个函数, 再仔细查看堆栈, 发现是在一个C语言版本的开源库中. 我们的程序是需要7*24小时运行的, 出现了解码异常应该要被我们忽略, 而不是应用程序崩溃. 开源的跨平台解码库是C语言写的, 在出现了严重错误时, 就直接abort这也是可以理解的, 不过, 这样的程序在我们的代码中显然要避免. 大哥, 现在都是什么年代了, 很多程序都是需要一直跑的, 我只好改的库的源代码来重新编译程序才能解决这个问题了, 该怎么改了, 如果去分析解码的逻辑, 我们没有专业的人才. 我想就干脆从abort函数这里入手, 直接返回成功值, 但是这样对解码逻辑影响更大, 可能导致更大的错误, 我想到了操作系统的异常机制, 由于我们是在WINDOWS平台上工作, 所以可以利用WINDOWS结构化异常, 我们可取消abort调用, 在这里我们使用代码产生一个结构化异常(SEH), 结构化异常分为硬件异常和软件异常, CPU可以检查到内存非法访问和除零错误等异常, 那么我们就将abort替换成除零语句, 比如 int i = 10/0; 当程序执行到这里的时候, CPU会捕捉这个异常, 并提示用户, 我们可以在调用解码函数的地方, 增加SEH捕捉代码, 来捕捉这个错误, 那么程序就能忽略这个错误并继续执行了. 后来的事实也证明了这个错误的忽略对程序并没有什么明显的影响. 怎么写这个捕捉代码呢, 操作系统支持的SEH捕捉代码块为 __try - __finally 块和 __try - __except 块, 而__try - __finnaly块就可以实现我们的功能. 写到这里, 可能有朋友要说了, 我们平时见的最多的是try-catch语句, 那么我要解释一下了, try-catch 是C++异常的处理方式, 而__try-__finnaly是操作系统SEH异常处理方式. 在C++语言的try-catch并不能捕捉操作系统 结构化异常(比如CPU异常, 内存访问冲突, 除零错误等). C++异常只能捕获软件异常, 通常是调用throw而产生的异常, 比如MFC异常中常见的CException. SEH异常和C++异常有本质的区别, SEH是操作系统提供的异常处理技术, 在任何支持该操作系统的编程语言中, 都可以使用, 而C++异常处理只能在编写C++代码时使用。然而, 应当知道WINDOWS的VC++编译器是使用操作系统结构化异常来实现C++异常的. 也就是说, C++的try块在VC++下编译时, 会变成__try块, C++的catch块会变成SEH的 __except块: catch测试则变成SEH异常过滤器, catch中的代码则变为__except中的代码. 事实上, C++的throw块, 在编译的时候也会变成SEH的RaiseException函数调用, 由c++异常变为SEH异常. __finnally 的好处在于, 有时更详细的异常信息对我们没有更大帮助, 我们只需要捕获到异常并忽略它。上面提到C++异常在VC++里被转换成SEH异常, 那么在VC下使用try-catch是否能捕获硬件异常呢? 比如我们常见的 0x0000000C 不可读或写. VC++编译器已经提供了支持: try {;} catch(...){;} 这样的语句就能够捕获所有异常:包括CPU异常, 以及C++异常; 不过需要注意的是, 在VC6.0中, 是默认支持的. 但是在VC2005中, 是默认不捕获CPU异常的. 区别在于一个C++编译选项/Eha , 只有这个选项打开才能用上面的try-catch()捕捉SEH异常.
usidc5
2012-02-25 14:58
在初学VC的时候,总以为try()catch(...)可以抓到所有的异常. 在开发之前开发的一个服务器程序中,才发现服务器经常莫名其妙的宕机了.一直觉得很诡异.
直到后来看了很多资料才明白结构化异常跟C++异常是两套东西,不统一。有些异常try.catch不一定能不抓到. 要将两种异常共同使用.下面的代码可以达到目的. 使用下面异常类,可以使程序更稳定.(注意:编译选项里面要记得打开 结构化异常开关. compile with: /EHa)
[cpp] ); background-attachment: initial; background-origin: initial; background-clip: initial; background-color: inherit; border-top-style: none; border-right-style: none; border-bottom-style: none; border-left-style: none; border-width: initial; border-color: initial; border-image: initial; padding-top: 1px; padding-right: 1px; padding-bottom: 1px; padding-left: 1px; margin-top: 0px; margin-right: 10px; margin-bottom: 0px; margin-left: 0px; display: inline-block; width: 16px; height: 16px; text-indent: -2000px; background-position: 0% 0%; background-repeat: no-repeat no-repeat; ">view plain); background-attachment: initial; background-origin: initial; background-clip: initial; background-color: inherit; border-top-style: none; border-right-style: none; border-bottom-style: none; border-left-style: none; border-width: initial; border-color: initial; border-image: initial; padding-top: 1px; padding-right: 1px; padding-bottom: 1px; padding-left: 1px; margin-top: 0px; margin-right: 10px; margin-bottom: 0px; margin-left: 0px; display: inline-block; width: 16px; height: 16px; text-indent: -2000px; background-position: 0% 0%; background-repeat: no-repeat no-repeat; ">copy
1. // 把结构化异常转化为C++异常
2.
3. struct SException
4.
5. {
6.
7. EXCEPTION_RECORD er;
8.
9. CONTEXT context;
10.
11. SException(PEXCEPTION_POINTERS pep)
12.
13. {
14.
15. er = *(pep->ExceptionRecord);
16.
17. context = *(pep->ContextRecord);
18.
19. }
20.
21. operator DWORD() { return er.ExceptionCode; }
22.
23. static void MapSEtoCE() { _set_se_translator( TranslateSEToCE ); }
24.
25.
26. static void __cdecl TranslateSEToCE( UINT dwEC, PEXCEPTION_POINTERS pep )
27.
28. {
29.
30. throw SException(pep);
31.
32. }
33.
34. };
35.
36. void main()
37.
38. {
39.
40. SException::MapSEtoCE();
41.
42. try
43.
44. {
45.
46. int* p = 0;
47.
48. int a = *p;
49.
50. }
51.
52. catch( SException& e )
53.
54. {
55.
56. if ( (DWORD)e == EXCEPTION_ACCESS_VIOLATION )
57.
58. {
59.
60. printf( "Access violation" );
61.
62. }
63.
64. }
65.
66. }
另外可以调用MS提供的函数SetUnhandledExceptionFilter, 这是程序异常未处理的最后一到防线.可以在回调函数中写出DUMP文件,然后通过PDB文件来调试看到宕机的源代码段.
[cpp] ); background-attachment: initial; background-origin: initial; background-clip: initial; background-color: inherit; border-top-style: none; border-right-style: none; border-bottom-style: none; border-left-style: none; border-width: initial; border-color: initial; border-image: initial; padding-top: 1px; padding-right: 1px; padding-bottom: 1px; padding-left: 1px; margin-top: 0px; margin-right: 10px; margin-bottom: 0px; margin-left: 0px; display: inline-block; width: 16px; height: 16px; text-indent: -2000px; background-position: 0% 0%; background-repeat: no-repeat no-repeat; ">view plain); background-attachment: initial; background-origin: initial; background-clip: initial; background-color: inherit; border-top-style: none; border-right-style: none; border-bottom-style: none; border-left-style: none; border-width: initial; border-color: initial; border-image: initial; padding-top: 1px; padding-right: 1px; padding-bottom: 1px; padding-left: 1px; margin-top: 0px; margin-right: 10px; margin-bottom: 0px; margin-left: 0px; display: inline-block; width: 16px; height: 16px; text-indent: -2000px; background-position: 0% 0%; background-repeat: no-repeat no-repeat; ">copy
1. LONG __stdcall MyCrashHandlerExceptionFilter(EXCEPTION_POINTERS* pEx)
2. {
3. #ifdef _M_IX86
4. if (pEx->ExceptionRecord->ExceptionCode == EXCEPTION_STACK_OVERFLOW)
5. {
6. // be sure that we have enought space...
7. static char MyStack[1024*128];
8. // it assumes that DS and SS are the same!!! (this is the case for Win32)
9. // change the stack only if the selectors are the same (this is the case for Win32)
10. //__asm push offset MyStack[1024*128];
11. //__asm pop esp;
12. __asm mov eax,offset MyStack[1024*128];
13. __asm mov esp,eax;
14. }
15. #endif
16.
17. bool bFailed = true;
18. HANDLE hFile;
19. hFile = CreateFile(szMiniDumpFileName.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
20. if (hFile != INVALID_HANDLE_VALUE)
21. {
22. MINIDUMP_EXCEPTION_INFORMATION stMDEI;
23. stMDEI.ThreadId = GetCurrentThreadId();
24. stMDEI.ExceptionPointers = pEx;
25. stMDEI.ClientPointers = TRUE;
26. // try to create an miniDump:
27. if (s_pMDWD(
28. GetCurrentProcess(),
29. GetCurrentProcessId(),
30. hFile,
31. MiniDumpNormal,
32. &stMDEI,
33. NULL,
34. NULL
35. ))
36. {
37. bFailed = false; // suceeded
38. }
39. CloseHandle(hFile);
40. }
41.
42. if (bFailed)
43. {
44. return EXCEPTION_CONTINUE_SEARCH;
45. }
46. if (bMsgbox)
47. {
48. string strMsg = "Run failed, Please Copy the ";
49. strMsg += szMiniDumpFileName;
50. strMsg += " file to us!";
51.
52. ::MessageBox(NULL, strMsg.c_str(), TEXT("Error"), 0);
53. }
54.
55.
56. // or return one of the following:
57. // - EXCEPTION_CONTINUE_SEARCH
58. // - EXCEPTION_CONTINUE_EXECUTION
59. // - EXCEPTION_EXECUTE_HANDLER
60. return EXCEPTION_EXECUTE_HANDLER; // this will trigger the "normal" OS error-dialog
61. }
62. #ifndef _M_IX86
63. #error "The following code only works for x86!"
64. #endif
65. LPTOP_LEVEL_EXCEPTION_FILTER WINAPI MyDummySetUnhandledExceptionFilter(
66. LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter)
67. {
68. return NULL;
69. }
70.
71. BOOL PreventSetUnhandledExceptionFilter()
72. {
73. HMODULE hKernel32 = LoadLibrary(_T("kernel32.dll"));
74. if (hKernel32 == NULL) return FALSE;
75. void *pOrgEntry = GetProcAddress(hKernel32, "SetUnhandledExceptionFilter");
76. if(pOrgEntry == NULL) return FALSE;
77. unsigned char newJump[ 100 ];
78. DWORD dwOrgEntryAddr = (DWORD) pOrgEntry;
79. dwOrgEntryAddr += 5; // add 5 for 5 op-codes for jmp far
80. void *pNewFunc = &MyDummySetUnhandledExceptionFilter;
81. DWORD dwNewEntryAddr = (DWORD) pNewFunc;
82. DWORD dwRelativeAddr = dwNewEntryAddr-dwOrgEntryAddr;
83.
84. newJump[ 0 ] = 0xE9; // JMP absolute
85. memcpy(&newJump[ 1 ], &dwRelativeAddr, sizeof(pNewFunc));
86. SIZE_T bytesWritten;
87. BOOL bRet = WriteProcessMemory(GetCurrentProcess(),
88. pOrgEntry, newJump, sizeof(pNewFunc) + 1, &bytesWritten);
89. return bRet;
90. }
91.
92. void InitMiniDumpWriter()
93. {
94. if (s_hDbgHelpMod != NULL)
95. return;
96.
97. // Initialize the member, so we do not load the dll after the exception has occured
98. // which might be not possible anymore...
99. s_hDbgHelpMod = LoadLibrary(_T("dbghelp.dll"));
100. if (s_hDbgHelpMod != NULL)
101. s_pMDWD = (tMDWD) GetProcAddress(s_hDbgHelpMod, "MiniDumpWriteDump");
102.
103. // Register Unhandled Exception-Filter:
104. LPTOP_LEVEL_EXCEPTION_FILTER p = SetUnhandledExceptionFilter(MyCrashHandlerExceptionFilter);
105.
106. BOOL bRet = PreventSetUnhandledExceptionFilter();
107.
108. // Additional call "PreventSetUnhandledExceptionFilter"...
109. // See also: "SetUnhandledExceptionFilter" and VC8 (and later)
110. // http://blog.kalmbachnet.de/?postid=75
111. }
usidc5
2012-02-25 14:59
#include <stdio.h> void main(){ char *a; try { a=0; (*a)=0; } catch(...) { printf( "oops,exception!!\n "); }}
这段代码的运行结果是什么?
你一定会说
"屏幕上打出oops,exception!!\n ".
没错,理论上的确是这样.
我们来验证一下,用vc6产生一个空的win32 console工程,
加入上面的cpp文件,
debug方式编译后,得到如期的结果,
但是!
release方式编译后,仍然出现了 "访问冲突 "!
这如何解释?
本怪兽经调查发现,
VC里缺身编译选项里关于异常的选项是/GX,
文档里说,这等价与/EHs--同步异常捕捉.
何解?
答:只有编译器认为有可能出异常的情况下,
即有throw出现的情况下,
编译器才会生成异常捕捉代码.
据说是VC6的一项新优化功能,
真是自作聪明!谁会希望这样的 "异常捕捉 "?
解决方法,去掉/GX,加上/EHa--异步异常捕捉.
这样可以保证异常捕捉代码不被 "高明 "的编译器优化掉.
那为何开头的例子里,debug版本运行正常呢?
答:debug版本不做优化.故正常也.
这是VC6 IDE里非常莫名其妙的地方,
用try catch的人请一定小心.
usidc5
2012-02-25 15:57
本讲基本要求 * 掌握;函数声名中异常情况的指定;异常处理中处理析构函数; 异常继承以及C++标准库的异常类层次结构。 * 理解:异常的任务及解决的方法。 * 了解:异常和继承以及C++标准库的异常类层次结构。重点、难点 ◆函数声名中异常情况的指定;异常处理中处理析构函数; 异常继承以及C++标准库的异常类层次结构。 在C++发展的后期,有的C++编译系统根据实际工作的需要,增加了一些功能,作为工具来使用,其中主要有模板(包括函数模板和类模板)、异常处理、命名 空间和运行时类型,以帮助程序设计人员更方便地进行程序的设计和调试工作。1997年ANSI C++委员会它们纳入了ANSII C++标准,建议所有的C++编译系统都能实现这些功能。这些工具是非有用的,C++的使用者应当尽量使用这些工具,因此本书对此作简要的介绍,以便为日 后的进一步学习和使用打下初步基础。 在第3章的11节已介绍了类模板。在本章中主要介绍异常处理和命名空间,应当注意,期的C++是不具备这些功能的,只有近期的C++系统根据ANSIC++的要求,实现了这些功。请读者注意使用的C++版本。 一、异常处理的任务 程序编制者总是希望自己所编写的程序都是正确无误的,而且运行结果也是完全正确的。但是这几乎是不可能的,智者千虑,必有一失,不怕一万,就怕万一。因 此,程序编制者不仅要考虑程序没有错误的理想情况,更要考虑程序存在错误时的情况,应该能够尽快地发现错误,消除错误。 语法错误: 在编译时,编译系统能发现程序中的语法错误(如关键字拼写错误,变量名未定义,语句末尾缺分号,括号不配对等),编译系统会告知用户在第几行出错,是什么 样的错误。由于是在编译阶段发现的错误,因此这类错误又称编译错误。有的初学者写的并不长的程序,在编译时会出现十几个甚至几十个语法错误,有人往往感到 手足无措。但是,总的来说,这种错误是比较容易发现和纠正的,因为它们一般都是有规律的,在有了一定的编译经验以后,可以很快地发现出错的位置和原因并加 以改正。 运行错误: 有的程序虽然能通过编译,也能投入运行。但是在运行过程中会出现异常,得不到正确的运行结果,甚至导致程序不正常终止,或出现死机现象。 例如: .在一系列计算过程中,出现除数为0的情况。 .内存空间不够,无法实现指定的操作。 .无法打开输入文件,因而无法读取数据。 .输入数据时数据类型有错。 由于程序中没有对此的防范措施,因此系统只好终止程序的运行。这类错误比较隐蔽,易被发现,往往耗费许多时间和精力,这成为程序调试中的一个难点。 在设计程序时,应当事先分析程序运行时可能出现的各种意外的情况,并且分别制订出相应的处理方法,这就是程序的异常处理的任务。 需要说明,在一般情况下,异常指的是出错(差错),但是异常处理并不完全等同于对出错的处理。只要出现与人们期望的情况不同,都可以认为是异常,并对它进 行异常处理。例如,在输入学生学号时输入了负数,此时程序并不出错,也不终止运行,但是人们认为这是不应有的学号,应予以处理。因此,所谓异常处理指的是 对运行时出现的差错以及其他例外情况的处理。 二、异常处理的方法 C++处理异常的机制引入 在一个小的程序中,可以用比较简单的方法处理异常,例如用if语句判别除数是否为0,如果是。则输出一个出错信息。但是在一个大的系统中,包含许多模块, 每个模块义包含许多函数,函数之间又五相调用,比较复杂。如果在每一个函数中都设置处理异常的程序段,会使程序过于复杂和庞大。因此,C++采取的办法 是:如果在执行一个函数过程中出现异常,可以不在本函数中立即处理,而是发出一个信息,传给它的上一级(即调用它的函数),它的上级捕捉到这个信启后进行 处理。如果上一级的函数也不能处理,就再传给其上一级,由其上一级处理。如此逐级上送,如果到最高一级还无法处理,最后只好异常终止程序的执行。 这样做使异常的发现与处理不由同一函数来完成。好处是使底层的函数专门用于解决实际任务,而不必再承担处理异常的任务,以减轻底层函数的负担,而把处理异 常的任务上移到某一层去处理。例如在主函数中调用十几个函数,只需在主函数中设置处理异常即可,而不必在每个函数中都设置处理异常,这样可以提高效率。 C++处理异常的机制组成: C++处理异常的机制是由3个部分组成的,即检查(try)、抛出(throw)和捕捉(catch)。把需要检查的语句放在try块中,throw用来 当出现异常时发出(形象地称为抛出,throw的意思是抛出)一个异常信息,而catch则用来捕捉异常信息,如果捕捉到厂异常信息,就处理它: 例1 给出三角形的三边a,b,c,求三角形的面积。只有a+b>c,b+c>a,c+a>b时才能构成三角形。设置异常处理,对不符合三角形条件的输出警告信息,不予计算。 #include <iostream> #include <cmath> using namespace std; int main() { double triangle(double,double,double); double a,b,c; cin>>a>>b>>c; while(a>0 && b>0 && c>0) { cout<<triangle(a,b,c)<<endl; cin>>a>>b>>c; } return 0; } double triangle(double a,double b,double c) { double area; double s=(a+b+c)/2; area=sqrt(s*(s-a)*(s-b)*(s-c)); return area; } 运行情况如下: 6 5 4/ (输入a,b,c的值) 9.92157 (输出三角形的面积) 1 1.5 2/ (输入a,b,c的值) 0.726184 (输{¨三角形的面积} 1 2 1 / (输人a,b,c的值) o (输出三角形的面积,此结果显然不对,因为不是三角形) 1 0 6/ (输入a,b,c的值) (结束) 程序没有对三角形条件(任意两边之和应大于第三边)进行检查,因此,当输入a=l,b=2,c=1时,则计算出三角形的面积为0,显然是不合适的。 现在修改程序,在函数triangle中对三角形条件进行检查,如果不符合三角形条件,就抛出一个异常信息,在主函数中的try-catch块中调用triangle函数,检测有无异常信息,并相应处理。修改后的程序如下: #include <iostream> #include <cmath> using namespace std; int main() { double triangle(double,double,double); double a,b,c; cin>>a>>b>>c; try //在try块中包含要检查的函数 { while(a>0 && b>0 && c>0) { cout<<triangle(a,b,c)<<endl; cin>>a>>b>>c;} } catch(double) //用catch捕捉异常信息并作相应处理 { cout<<"a="<<a<<",b="<<b<<",c="<<c<<",that is not a traingle!"<<endl;} cout<<"end"<<endl; return 0; } double triangle(double a,double b,double c) //计算三角形的面积的函数 { double sqrt; double s=(a+b+c)/2; if (a+b<=c||b+c<=a||c+a<=b) throw a; //当不符合三角形条什抛出异常信息 return sqrt(s*(s-a)*(s-b)*(s-c)); } 程序运行结果如下: 6 5 4 (输入a,b,c的值) 9.92157 (计算出三角形的面积) 1 1.5 2 (输入a,b,c的值) 0.726184 (计算出三角形的面积) 1 2 1 (输入a,b,c的值) a=1,b=2,c=1,that is not a triangle! (异常处理) end 现在结合程序分析怎样进行异常处理。 (1)首先把可能出现异常的、需要检查的语句或程序段放在try后面的花括号中。由于riangle函数是可能出现异常的部分,所以把while循环连同 triangle函数放在try块中。这些语句是正常流程的一部分,虽然被放在by块中,并不影响它们按照原来的顺序执行。 (2)程序开始运行后,按正常的顺序执行到try块,开始执行by块中花括号内的语句。如果在执行try块内的语句过程中没有发生异常,则catch子句不起作用,流程转到catch子句后面的语句继续执行。 (3)如果在执行try块内的语句(包括其所调用的函数)过程中发生异常,则throw运算符抛出一个异常信息。请看程序中的triangle函数部分, 当不满足三角形条件时,throw抛出double类型的异常信息a。throw抛出异常信息后,流程立即离开本函数,转到其上一级的函数(main函 数)。因此不会执行triangle函数中if语句之后的return语句。 throw抛出什么样的数据由程序设计者自定,可以是任何类型的数据(包括自定义类型的据,如类对象)。 (4)这个异常信息提供给try-catch结构,系统会寻找与之匹配的catch子句。现在a是double型,而catch子句的括号内指定的类型也 是double型,二者匹配,即catch捕获了该异常信息,这时就执行catch子句中的语句,本程序输出a=1.b=2,c=1,that is not a triangle! (5)在进行异常处理后,程序并不会自动终止,继续执行catch子句后面的语句。本程序输出“end”。注意并不是从出现异常点继续执行while循 环。如果在try块的花括号内有10个语句,在执行第3个语句时出现异常,则在处理完该异常后,其余7个语句不再执行,而转到catch子句后面的语句去 继续执行。由于catch子句是用来处理异常信息的,往往被称为catch异常处理块或catch异常处理器。 异常处理的语法: throw语句一般是由throw运算符和一个数据组成的,其形式为: throw 表达式; try-catch的结构为: try { 被检查的语句} catch(异常信息类型[变量名]) { 进行异常处理的语句} 说明: (1)被检测的函数必须放在try块中,否则不起作用。 (2)try块和catch块作为—个整体出现,catch块是try-catch结构中的一部分,必须紧跟在try块之后,不能单独使用,在二者之间也不能插入其他语句, (3)try和catch块中必须有用花括号括起来的复合语句,即使花括号内只有一个语句,也不能省略花括号。 (4)一个try-catch结构中只能有一个try块,但却可以有多个catch块,以便与不同的异常信息匹配。 (5)catch后面的圆括号中,一般只写异常信息的类型名,如 catch(double) catch只检查所捕获异常信息的类型: 异常信息可以是C++系统预定义的标准类型,也可以是用户自定义的类型(如结构体或类)。如果由throw抛出的信息属于该类型或其子类型,则catch与throw二者匹配,catch捕获该异常信息。 catch还可以有另外一种写法,即除了指定类型名外,还指定变量名,如: catch(double d) 此时如果throw抛出的异常信息是double型的变量a,则catch在捕获异常信息a的同时,还使d获得a的值,或者说d得到a的一个拷贝。什么时候需要这样做呢?有时希望在捕获异常信息时,还能利用throw抛出的值,如 (6)如果在catch子句中没有指定异常信息的类型,而用了删节号“…”,则表示它可以捕捉任何类型的异常信息,如: catch(…){cout<<"OK"<<endl;} 它能捕捉所有类型的异常信息,并输出”OK”。 (7)try- catch结构可以与throw出现在同一个函数中,也可以不在同一函数中。当throw抛出异常信息后,首先在本函数中寻找与之匹配的catch,如果 在本函数中无try-catch结构或找不到与之匹配的catch,就转到其上一层去处理,如果其上一层也无try-catch结构或找不到与之匹配的 catch,则再转到更上一层的try-catch结构去处理,也就是说转到离开出现异常最近的try-catch结构去处理。 (8)在某些情况下,在throw语句中可以不包括表达式, (9)如果throw抛出的异常信息找不到与之匹配的catch块,那么系统就会调用一个系统函数terminate,使程序终止运行。 例2 在函数嵌套的情况下检测异常处理。这是一个简单的例子,用来说明在try块中有函数嵌套调用的情况下抛出异常和捕捉异常的情况。 #include <iostream> using namespace std; int main() { void f1(); try { f1( ); } //调用fl() catch(double) { cout<<"OK0!"<<endl;} cout<<"end0"<<endl; return 0; } void f1() { void f2(); try { f2(); } //调用f2() catch( char ) { cout<<"OK1!";} cout<<"end1"<<endl; } void f2() { void f3(); try { f3();} //调用f3() catch(int) { cout<<"Ok2!"<<endl;} cout<<"end2"<<endl; } void f3() { double a=0; try { throw a;} //抛出double类型异常信息 catch(float) { cout<<"OK3!"<<endl;} cout<<"end3"<<endl; } 分3种情况分析运行情况: (1)执行上面的程序。图1为有函数嵌套时异常处理示意图。 (2)如果将f3函数中的catch子句改为catch(double),而程序中其他部分不变,则f3函数中的throw抛出的异常信息立即被门函数中 的catch子句捕获(因为抛出的是double型异常,catch要捕捉的也是double型异常,二者匹配)。于是执行catch子句中的复合语句, 输出"OK3",再执行catch子句后面的语句,输出"end3"。f3函数执行结束后,流程返回f2函数中调用f3函数处继续往下执行。程序运行结果如下: OK31 (在f3函数中捕获异常) end3 (执行f3函数中最后一个语句时的输出) end2 (执行f2函数中最后一个语句时的输出) endl (执行u函数中最后一个浯句时的输出) endO (执行主函数中最后—个语句时的输出) (3)如果在此基础上再将f3函数中的catch块改为 catch(double) { cout<<"OK3!"<<endl;throw;} f3函数中的catch子句捕获throw抛出的异常信息a,输出"OK3!"表示收到此异常信息,但它立即用"throw;”将a再抛出。由于a是 double型,与f2和门函数中的catch都不匹配,因此最后被main函数中的catch子句捕获。程序运行结果如下: OK3! (在f3函数中捕获异常) OKO! (在主函数中捕获异常) endO (执行主函数中最后一个语句时的输出) 三、 在函数声明中进行异常情况指定 为了便于阅渎程序,使用户在看程序时能够知道所用的函数是否会抛出异常信息以及异常信息可能的类型,C++允许在声明函数时列出可能抛出的异常类型,如可以将例1中第二个程序的第3行改写为: double triangle(double,double,double)throw(double); 表示triangle函数只能抛出double类型的异常信息。如果写成 double triangle(double,double,double)throw(int,double,float,char); 则表示triangle函数可以抛出int,double,float或char类型的异常信息。异常指定是函数声明的一部分,必须同时出现在函数声明和函数定义的首行中,否则在进行函数的另一次声明时,编泽系统会报告“类型不匹配”。 如果在声明函数时未列出可能抛出的异常类型,则该函数可以抛出任何类型的异常信息。如例1中第2个程序中所表示的那样。 如果想声明一个不能抛出异常的函数,可以写成以下形式: double triangle(double,double,double) throw(); //throw无参数 这时即使在函数执行过程中出现了throw语句,实际上也并不执行throw语句,并不抛出任何异常信息,程序将非正常终止。 四、 在异常处理中处理析构函数 如果在try块(或try块中调用的函数)中定义了类对象,在建立该对象时要调用构造函数。在执行try块(包括在try块中调用其他函数)的过程中如果 发生了异常,此时流程立即离开try块(如果是在try块调用的函数中发生异常,则流程首先离开该函数,回到调用它的try块处,然后流程再从try块中 跳出转到catch处理块)。这样流程就有可能离开该对象的作用域而转到其他函数,因而应当事先做好结束对象前的请理工作,C++的异常处理机制会在 throw抛出异常信息被catch捕获时,对有关的局部对象进行析构(调用类对象的析构函数),析构对象的顺序与构造的顺序相反,然后执行与异常信息匹 配的catch块中的语句。 例 在异常处理中处理析构函数。 这是一个为说明在异常处理中调用析构函数的示例,为了请晰地表示流程,程序中加入了一些cout语句,输出有关的信息,以便读者对照结果分析程序。 #include <iostream> #include <string> using namespace std; class Student { public: Student(int n,string nam) //定义构造函数 { cout<<"construtor-"<<n<<endl; num=n;name=nam;} ~Student(){cout<<"destructor-"<<num<<endl;} //定义析构函数 void get_data(); //成员函数声明 private: int num; string name; }; void Student::get_data() //定义成员函数 { if(num==0) throw num; //如num=O,抛出int型变量num else cout<<num<<" "<<name<<endl; //若num不等O,输出num,name cout<<"in get_data()"<<endl; //输出信息,表示目前在fet_data函数中 } void fun() //过程函数(注意可见性) { Student stud1(1101,"tan");//建立对象studl stud1.get_data(); //调用studl的getdata函数 Student stud2(0,"Li"); //建立对象stud2 stud2.get_data(); } //调用smd2的get data函数 int main() { cout<<"main begin"<<endl; //表示主函数开始了 cout<<"call fun()"<<endl; //表示调用fun函数 try {fun();} //调用fun函数 catch(int n) { cout<<"num="<<n<<",error!"<<endl;} //表示num=0出错 cout<<"main end"<<endl; //表示主函数结束 return 0; } 分析程序执行过程,程序运行结果如下: main begiin call fun() constructor-110l //对立Student类对象stud1,num=1101 110l tan //对象stud1调用Student类的成员函数get_data,num不等O in get_data() //执行get_data constructor-0 //建立Student类对象stud2,num=0,抛出num. destructor-O //调用Student类对象stud1的析构函数 destructor-1101 //调用Student类对象stud2的析构函数 num=O,error! //表示num=0出错。 main end //表示主函数结束 本节简单地介绍了异常处理的机制和使用方法,读者将会在今后的实际应用中进一步掌握它们。
usidc5
2012-02-25 16:05
作者:PJ Naughter
下载源代码和例子 简介: CExceptionLogger,是一个可以免费使用的C++类,用它可以截获未处理异常,如:非法存取、栈溢出、被零除等,并可以将异常详细信息记录到日志文件。这个类源自于MSDN Magazine 2002年3月的一篇专栏文章“Under the Hood: Improved Error Reporting with DBGHELP 5.1”,该文章的作者是 Matt Pietrek。 特性: 在默认情况下,CExceptionLogger产生一个日志文件,名字为:nameofexe.exception,这里的nameofexe是exe文件的名字。如果以ASCII模式生成CExceptionLogger,则日志文件为一ASCII文件;如果以UNICODE模式生成CExceptionLogger,那么日志文件为一UNICODE文件。日志文件记录的信息包括:
1. 记录异常发生的日期和时间。
2. 异常代码。
3. 如果发生非法存取,则记录该异常的详细信息。
4. 记录的异常详细信息包括:线性地址、段、偏移量和模块路径。
5. 进程的全路径名。
6. 当前Win32工作目录。
7. 进程的命令行。
8. 进程ID。
9. 发生异常的线程ID。
10. 列举进程中所有的线程(假设ToolHelp32是可获得的),内容包括:
o 线程ID
o 优先级和Delta优先级
o 参考
o 创建时间
o Kernel 和 User Time
· 列举进程中的模块(同样假设ToolHelp32是可获得的),内容包括:
· 名字和全路径
· 全程及每个进程的引用计数
· 模块句柄
· 大小
· 模块完全展开后的所有符号
· 所有x86寄存器。
· 异常发生的调用堆栈,包括:段、偏移量、模块、函数和行信息。
· 日 志文件记录的内容还包括每一个堆栈帧(stack frame)以及所有模块、所有变量、所有参数;所有基本数据类型,如:voids, chars, shorts, words, ints, DWORDS, floats, doubles 和 longs。此外日志还记录用户定义的数据类型(UDT)包括结构、联合以及类的信息,再现其成员数据。每种类型都包括名字、地址、类型和值。如果变量是 一数组,该数组中的值被完全记录。 版权声明:
· 你可以在任何以二进制形式发布的产品(包括商业的、共享的、自由的或其它的)中包含此源代码
· 在不修改每个模块(*.h、*.cpp)最上方版权细则的前提下,你可以用任何方式修改源代码
· 如果你想要与自己的应用程序一起分发源代码,只允许分发作者最新发布的版本,以便保证源代码的出处是唯一的
使用方法:
· 编译这个类需要安装2002年11月以后发布的平台SDK。使用时既可以将 ExceptHandler.cpp/h 文件直接添加到C++工程中,也可以用DLL输出异常处理函数类,并用LoadLibrary函数在需要时动态加载DLL。
· ExceptHandler的二进制版本在XCPTHLR.DLL中提供。此DLL可以从本文最上方链接处下载。
· 为 了在客户机器上运行该代码,必须分发DBGHELP 5.1 动态链接库,这个库可以从2002年11月以后的平台SDK中获得。选择“Install Debugging Tools for Windows”选项安装该DLL。此外,还要注意DBGHELP 动态链接库在最新的 Windows 版本中是受到保护 Windows 系统文件,所以请将 DBGHELP 动态链接库放在与应用程序相同的目录中。
· 为了在release模式中给代码提供符号,必须按照下列步骤修改工程设置: 1. Project Settings -> Link -> Debug (Category) , 启用“Debug Info” 并选中“Microsoft Format”。 2.相同的地方,选中“Separate Types”。 3. 在相同页的“Project Options”编辑框中,添加“/OPT:REF”,这样可以保证从最终的二进制中排除掉未引用的函数。 4. Project Settings -> C++ -> General (Category) Debug Info 组合框 -> 选择“Program Database” 5. 如果要对无意义的堆栈进行调用,那么需将“Optimizations”设置为“Disable (Debug)”。
· 还要记住与代码一起分发最终生成的pdb文件(或用其它方法使之可以得到)。以便CExceptionLogger能在最终的日志中给出源和行信息。
· 请注意由于所有的符号,即便是很小的应用的符号,各自的异常日志可能超过100K。我的观点是磁盘空间不值钱,而开发人员为查找BUG所花的时间则很宝贵。
· 参考资料: [pre]Bugslayer, MSJ, August 1998 by John Robbins, http://www.microsoft.com/msj/defaultframe.asp?page=/msj/0898/bugslayer0898.htm Under the Hood, MSDN, March 2002 by Matt Pietrek, http://msdn.microsoft.com/msdnmag/issues/02/03/Hood/Hood0203.asp[/pre]
改进计划:
· 可配置日志文件名。
· 提供对Win64平台的支持。
· 提供对非x86 调用堆栈的支持。
· 如果有任何改进建议,不妨来信告知,以便我将它们加到下下一个版本中。
作者的联系方式: [email=%20pjna@naughter.com]PJ Naughter[/email] 个人网站:http://www.naughter.com
usidc5
2012-02-25 16:06
我们的异常措置类的features 若何写一个异常措置类是一个不太轻易的工作,比来刚好接触了一些不错的代码,看到了一些技巧,这里和巨匠分享一下。 一个相对完美的异常措置类(以及附加的一些工具)应该能够措置下面的一些功能 1) 能够便利的界说异常类的担任树 2) 能够便利的throw、catch,也就是在代码中捕捉、措置代码的部门应该更短 3) 能够获取异常呈现的源文件的名字、体例的名字、行号 4) 能够获取异常呈现的挪用栈而且打印出来 因为今朝我用的平台是linux,所以琅缦沔挪用的一些函数也只是在linux下面有用。Windows也必定是具有响应的函数的,具体可能需要去查查 首先科普一些内容 1) 对于没有捕捉的异常(no handler),则会终止轨范,挪用terminate() 2) 在界说函数的时辰,我们可以在界说的后面加上throw (exception1, exception2…): a) 如不美观没有写这一段、则可能抛出肆意的异常 b) 如不美观写throw(),则暗示函数不能抛出肆意的异常 c) 如不美观写throw(A, B), 暗示函数抛出A、B的异常 如不美观抛出的异常不在列表规模内,则异常不能被catch,也就会挪用terminate() 我们构想一下我们界说、挪用我们的异常类的时辰是若何的一个气象 1) 界说 1: class DerivedException : public BaseException 2: { 3: public: 4: MY_DEFINE_EXCEPTION(DerivedException, BaseException); 5: }; 2) 若何抛出异常 1: MY_THROW(DerivedException) 3) 若何catch异常 1: catch (DerivedException& e) 2: { 3: cout<< e.what() << endl; 4: } 这个输出的内容搜罗错误的行号、文件名、体例名、和挪用栈的列表 给出我们异常类的头文件 1: #ifndef EXCEPTION_TEST 2: #define EXCEPTION_TEST 3: 4: #include <exception> 5: #include <string> 6: 7: #define MY_THROW(ExClass, args...) “ 8: do “ 9: { “ 10: ExClass e(args); “ 11: e.Init(__FILE__, __PRETTY_FUNCTION__, __LINE__); “ 12: throw e; “ 13: } “ 14: while (false) 15: 16: #define MY_DEFINE_EXCEPTION(ExClass, Base) “ 17: ExClass(const std::string& msg = "") throw() “ 18: : Base(msg) “ 19: {} “ 20: “ 21: ~ExClass() throw() {} “ 22: “ 23: /* override */ std::string GetClassName() const “ 24: { “ 25: return #ExClass; “ 26: } 27: 28: class ExceptionBase : public std::exception 29: { 30: public: 31: ExceptionBase(const std::string& msg = "") throw(); 32: 33: virtual ~ExceptionBase() throw(); 34: 35: void Init(const char* file, const char* func, int line); 36: 37: virtual std::string GetClassName() const; 38: 39: virtual std::string GetMessage() const; 40: 41: const char* what() const throw(); 42: 43: const std::string& ToString() const; 44: 45: std::string GetStackTrace() const; 46: 47: protected: 48: std::string mMsg; 49: const char* mFile; 50: const char* mFunc; 51: int mLine; 52: 53: private: 54: enum { MAX_STACK_TRACE_SIZE = 50 }; 55: void* mStackTrace[MAX_STACK_TRACE_SIZE]; 56: size_t mStackTraceSize; 57: mutable std::string mWhat; 58: }; 59: 60: class ExceptionDerived : public ExceptionBase 61: { 62: public: 63: MY_DEFINE_EXCEPTION(ExceptionDerived, ExceptionBase); 64: }; 65: 66: #endif
usidc5
2012-02-25 16:11
我 觉得C++中使用异常在使用中最大的不方便就是在抛出异常的时候没有位置信息,当程序到一定规模的时候,也很难确定异常从那里抛出,又在那里捕获的,不利 于程序的调试。而在C#中,我发现它的异常的功能太强大了,可以确定异常的位置和捕获的位置,但它是靠CLR的功能实现的。那么我们怎么在C++中实现这 个功能呢? 下面我就为大家介绍我写的可以获得抛出异常位置和捕获异常位置的异常类。该类使用标准C++实现,继承标准异常exception。你也可以按照这个思路 实现一个MFC的版本。我在使用的时候就是使用两个不同的版本。MFC的版本的功能要强大一点,它内置了Win32错误代码转换错误信息的转换函数,为程 序提供更强大的异常处理功能。 功能都在两个主要的宏里面实现: 1.抛出异常 // throw a comm_exception that contains throw position information #define THROW_EX(what) \ throw comm_exception(what, __FILE__,__FUNCTION__,__LINE__); 这样在抛出异常的时候,就把文件,函数,行号信息等信息加入到异常类中。 2.捕获异常 // set the position information of catching the comm_exception #define SET_CATCH_POS(comm_ex) \ comm_ex.set_catch_file(__FILE__);\ comm_ex.set_catch_function(__FUNCTION__);\ comm_ex.set_catch_line(__LINE__); 当使用catch捕获到异常的时候,就通过这个宏设置它的捕获位置 。 try { THROW_EX( " there is a exception\n " ); } catch (more_exception & e) { SET_CATCH_POS(e); std::cout << e; } 比如一个ADO链接数据库的异常输出为: what: [DBNETLIB][ConnectionOpen (Connect()).]SQL Server 不存在或拒绝访问。 code: -1 throw exception at:----------------- file: f:\tt messenger\v0.7\adodb\adoconn.cpp function:C2217::Data::CAdoConn::Open line: 71 catch excetion at:------------------ file: e:\项目\tt messenger v0.7\code\server\adodb\adodb.cpp function:main line: 98 头文件如下: /* **************************************************************************** * Copyright(c) 2003-2005, C2217 Studio * * Name of the File: comm_exception.h * Summary: standerd exception that can get throwing position and catching position like exception in .NET * Current Version: 1.2 * Author: DengYangjun * Email: dyj057@gmail.com * Created Date: April 30, 2005 * Finished Date: July 13, 2005 * * Substitutional Version: * Note: 1.You can use it freely, but reserve the copyright and the author information * History: * Modify Date: 2005-5-9 * version 1.1 * Support MFC and Get Error message * Modify Date: 2005-7-13 * version 1.2 * Rename as comm_exception, it is can used in STD C++ * 2005-8-16 * Add Error Code and macro THROW_MY_CODE * ***************************************************************************** */ #pragma once #ifndef IBMS_STDLIB_COMM_EXCEPTION #define IBMS_STDLIB_COMM_EXCEPTION #include < exception > #include < string > #include < ostream > #include < iomanip > using namespace std; #ifndef __FUNCTION__ #define __FUNCTION__ "Unkown" #endif namespace C2217 { namespace StdLib { class comm_exception : public exception { public : comm_exception( string what, string throw_file = "" , string throw_function = "" , int throw_line =- 1 ) :m_what(what),m_throw_file(throw_file), m_throw_function(throw_function),m_throw_line(throw_line),m_catch_line( - 1 ), m_error_code( - 1 ){} comm_exception( int code, string what = "" , string throw_file = "" , string throw_function = "" , int throw_line =- 1 ) :m_what(what),m_throw_file(throw_file), m_throw_function(throw_function),m_throw_line(throw_line),m_catch_line( - 1 ), m_error_code(code){} virtual ~ comm_exception( void ){} virtual const char * what() const throw () { return m_what.c_str(); } inline const char * get_throw_file() const { return m_throw_file.c_str(); } inline const char * get_throw_function() const { return m_throw_function.c_str(); } inline int get_throw_line() const { return m_throw_line; } inline void set_catch_file( const char * catch_file) { m_catch_file = catch_file; } inline void set_catch_function( const char * catch_function) { m_catch_function = catch_function; } inline void set_catch_line( int catch_line) { m_catch_line = catch_line; } inline const char * get_catch_file() const { return m_catch_file.c_str(); } inline const char * get_catch_function() const { return m_catch_function.c_str(); } inline int get_catch_line() const { return m_catch_line; } inline int get_error_code() const { return m_error_code; } string str() const ; protected : string m_what; string m_throw_file; string m_throw_function; int m_throw_line; string m_catch_file; string m_catch_function; int m_catch_line; int m_error_code; }; // end of class comm_exception // override operator << for class type comm_exception ostream & operator << (ostream & out , const comm_exception & e); // throw a comm_exception that contains throw position information #define THROW_EX(what) \ throw comm_exception(what, __FILE__,__FUNCTION__,__LINE__); // throw a comm_exception that contians a error code #define THROW_MY_CODE(errorCode) \ throw comm_exception((errorCode), "" , __FILE__, __FUNCTION__, __LINE__ ); // throw a comm_exception that conts a error code and what #define THROW_MY_CODE0(errorCode,what) \ throw comm_exception((errorCode), what,__FILE__, __FUNCTION__, __LINE__ ); // throw a exception that derived from comm_exception #define THROW_EX_TYPE(type, what) \ throw type(what, __FILE__,__FUNCTION__,__LINE__); // throw a comm_exception that contians a error code #define THROW_MY_CODE_TYPE(type,errorCode) \ throw type((errorCode), "" , __FILE__, __FUNCTION__, __LINE__ ); // throw a exception that derived from comm_exception #define THROW_MY_CODE0_TYPE(type,errorCode,what) \ throw type((errorCode), what,__FILE__, __FUNCTION__, __LINE__ ); // set the position information of catching the comm_exception #define SET_CATCH_POS(comm_ex) \ comm_ex.set_catch_file(__FILE__);\ comm_ex.set_catch_function(__FUNCTION__);\ comm_ex.set_catch_line(__LINE__); #ifdef _DEBUG #define DEBUG_ONLY(f) (f) #else #define DEBUG_ONLY(f) ((void)0) #endif } // End of namespace C2217 } // End of namespace StdLib #endif 实现文件如下: #include " stdafx.h " #include " comm_exception.h " #include < sstream > using namespace std; namespace C2217 { namespace StdLib { ostream & operator << (ostream & out , const comm_exception & e) { out << " what: " << e.what() << endl << " code: " << e.get_error_code() << endl << " throw exception at:----------------- " << endl << " file: " << e.get_throw_file() << endl << " function: " << e.get_throw_function() << endl << " line: " << e.get_throw_line() << endl << " catch excetion at:------------------ " << endl << " file: " << e.get_catch_file() << endl << " function: " << e.get_catch_function() << endl << " line: " << e.get_catch_line() << endl; return out ; } string comm_exception::str() const { stringstream ss; ss << * this ; return ss.str(); } } } 与C#中的异常类比较,它少了异常扩散的路径。 注意:源程序在Windows XP professional + SP2, Visual .NET 2003 环境下编译通过。
posted on 2007-01-24 09:07 天下无双 阅读(5556) 评论(5) 编辑 收藏 引用 所属分类: C/C++
FeedBack:
# re: 可获得抛出位置和捕获位置的C++异常类[未登录]
2007-01-29 12:56 | hdqqq
这个其实使用了编译器内建的__FILE__,和 __LINE__宏,在编译器就确定了的,上面的代码来说,如果在某个函数中处理了异常并显示信息,以后不管这个函数在那里被调用,显示的异常信息都是一样的. 象下面 void excep_handle(...) { try { ... THROW_EX( " there is a exception\n " ); } catch (more_exception & e) { SET_CATCH_POS(e); std::cout << e; } } 而调用的函数是这样的 void test() { int i,j,k; for (i = 0; i < 100; i++) { excep_handle(...) for (j = 0; j < 100; j++) { excep_handle(...) for(k = 0; k < 100; k++) { excep_handle(...) } } } } 上面的代码,不管在那层循环的调用中,异常弹出的都是同一个文件和同一个行数无法知道具体是在那一层出现的异常.
- try catch
- try{} catch{}
- try catch
- try...catch
- try{} catch(...){}
- try catch
- try-catch
- try catch
- try-catch
- try catch
- try catch
- try catch
- try catch
- try catch
- try {...} catch (){....}
- try{} catch{}
- try catch
- Try catch
- 接下来的任务
- 在Silverlight中读取指定URL图片包数据
- 使用Keychain存储用户敏感信息
- source code
- The currently displayed page contains invalid values
- try catch
- streams流复制ORA-01403错误解决一则
- RegExp类型
- 大型机汇编(HLASM)之多进程指令CS 和 CDS
- [AS3] 文字显示
- 数字录音机 汇编实验
- 模板缓存1
- 如何解决Install ncurses(ncurses-devel) and try again
- 取偏移地址指令总结(不完整版)0