C++ Exception Handler
2001-12-11
异常处理的基本思想是简化程序的错误代码,为程序键壮性提供一个标准检测机制。
也许我们已经使用过异常,但是你会是一种习惯吗,不要老是想着当我打开一个文件的时候才用异常判断一下,我知道对你来说你喜欢用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,但这是有解决的方法的,那就是使用“灵巧指针”。
如果对照
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
class ExceptionClass{
char* name;
public:
ExceptionClass(const char* name="default name")
{
cout<<"Construct "<
}
~ExceptionClass()
{
cout<<"Destruct "<
void mythrow()
{
throw ExceptionClass("my throw");
}
}
void main(){
ExceptionClass e("Test");
try{
e.mythrow();
}
catch(...)
{
cout<<”*********”<
}
这是输出信息:
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"<
~ExceptionClass(){
cout<<"Exception Class Destruct String"<
void ReportError() {
cout<<"Exception Class:: This is Report Error Message"<
};
class ArguClass{
char* name;
public:
ArguClass(char* name="default name"){
cout<<"Construct String::"<
}
~ArguClass(){
cout<<"Destruct String::"<
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!!"<
catch(ExceptionClass pTest)
{
pTest.ReportError();
}
catch(...){
cout<<"***************"<
}
输出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
#include
class ExceptionClass1
{
char* s;
public:
ExceptionClass1(){
cout<<"ExceptionClass1()"<
cout<<"throw a exception"<
}
~ExceptionClass1(){
cout<<"~ExceptionClass1()"<
}
};
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 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 auto_ptr {
public:
typedef T element_type;
explicit auto_ptr(T *p = 0) throw();
auto_ptr(const auto_ptr
auto_ptr
~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
pa->processAdoption();
}
}
这个版本的processAdoptions在两个方面区别于原来的processAdoptions函数。
第一, pa被声明为一个auto_ptr
第二, 在循环的结尾没有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的拷贝。对异常对象进行强制复制拷贝,这个限制有助于我们理解参数传递与抛出异常的第二个差异:抛出异常运行速度比参数传递要慢。
当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(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
我们能这样计算一个整数的平方根,如下所示:
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
... 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
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
{
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
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);
*lpAddress = 'A'; // <- Here, Will Happen a Access Exception!
VirtualFree((LPVOID)lpAddress, 1024, MEM_RELEASE);
return dwReturnValue;
}
DWORD Func_SEHTerminate()
{
DWORD dwReturnData = 0;
HANDLE hSem = NULL;
const char* lpSemName = "TermSem";
hSem = CreateSemaphore(NULL, 1, 1, lpSemName);
__try
{
WaitForSingleObject(hSem,INFINITE);
dwReturnData = TermHappenSomeError();
}
__finally
{
ReleaseSemaphore(hSem,1,NULL);
CloseHandle(hSem);
}
return dwReturnData;
}
在上面的代码中,我们看到TermHappenSomeError()函数包含一个错误(对没有Commit的页进行提交),会引起一个无效内存访问。如果没有S E H,在这种情况下,将会给用户显示一个很常见的Application Error对话框。当用户忽略这个错误对话框,该进程就结束了。当这个进程结束(由于一个无效内存访问),信标仍将被占用并且永远不会被释放,这时候,任何等待信标的其他进程中的线程将不会被分配C P U时间。但若将对R e l e a s e S e m a p h o r e的调用放在f i n a l l y块中,就可以保证信标获得释放,即使某些其他函数会引起内存访问错误。
如果结束处理程序足够强,能够捕捉由于无效内存访问而结束的进程,我们就可以相信它也能够捕捉s e t j u m p和l o n g j u m p的结合,还有那些简单语句如b r e a k和c o n t i n u e。
=======================
下面是一个非常有挑战性的代码,但是这段代码在现实编程中,我相信没有程序员会写这么无聊的代码段。
代码:(返回值:14)
DWORD Func_SEHTerminateHard()
{
DWORD dwTemp = 0;
while(dwTemp < 10)
{
__try
{
if(dwTemp == 2)
continue;
if(dwTemp == 3)
break;
}
__finally
{
dwTemp++;
}
dwTemp++;
}
dwTemp += 10;
return dwTemp;
}
我们来一步一步地分析函数做了什么。首先d w Te m p被设置成0。t r y块中的代码执行,但两个i f语句的值都不为T R U E。执行自然移到f i n a l l y块中的代码,在这里d w Te m p增加到1。然后f i n a l l y块之后的指令又增加d w Te m p,使它的值成为2,当循环继续,d w Te m p为2,t r y块中的c o n t i n u e语句将被执行。如果没有结束处理程序在从t r y块中退出之前强制执行f i n a l l y块,执行就立即跳回w h i l e测试,d w Te m p不会被改变,将出现无限(死)循环。利用一个结束处理程序,系统知道c o n t i n u e语句要引起控制流过早退出t r y块,而将执行移到f i n a l l y块。在f i n a l l y块中,d w Te m p被增加到3。但f i n a l l y块之后的代码不执行,因为控制流又返回到c o n t i n u e,再到循环的开头。现在我们处理循环的第三次重复。这一次,第一个i f语句的值是FA L S E,但第二个语句的值是T R U E。系统又能够捕捉要跳出t r y块的企图,并先执行f i n a l l y块中的代码。现在d w Te m p增加到4。由于b r e a k语句被执行,循环之后程序部分的控制恢复。这样, f i n a l l y块之后的循环中的代码没有执行。循环下面的代码对d w Te m p增加1 0,这时d w Te m p的值是1 4,这就是调用这个函数的结果。
尽管结束处理程序可以捕捉t r y块过早退出的大多数情况,但当线程或进程被结束时,它不能引起f i n a l l y块中的代码执行。当调用E x i t T h r e a d或E x i t P r o c e s s时,将立即结束线程或进程,而不会执行f i n a l l y 块中的任何代码。另外,如果由于某个程序调用Te r m i n a t e T h r e a d或Te r m i n a t e P r o c e s s,线程或进程将死掉, f i n a l l y块中的代码也不执行。某些C运行期函数(例如a b o r t)要调用E x i t P r o c e s s,也使f i n a l l y块中的代码不能执行。虽然没有办法阻止其他程序结束你的一个线程或进程,但你可以阻止你自己过早调用E x i t T h r e a d和E x i t P r o c e s s。
========================
回到我们的基本函数,我们对它进行修改,接下来的代码是:(返回值:100)
DWORD Func_SEHTerminateMReturn()
{
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);
return (DWORD)100;
}
dwReturnData = 10;
return dwReturnData;
}
t r y块将要执行,并试图将dwReturnData的值(5)返回给这个函数的调用者。前面已经说过,试图从t r y块中过早的返回将导致产生代码,把返回值置于由编译程序建立的临时变量中。然后, f i n a l l y块中的代码被执行。在这里, f i n a l l y块中增加了一个r e t u r n语句。那么返回5还是1 0 0?这里的答案是1 0 0,因f i n a l l y块中的r e t u r n语句引起值1 0 0存储在值5所存储的临时变量中,覆盖了值5。当f i n a l l y块完成执行,现在临时变量中的值( 1 0 0)从函数返回给调用程序。
我们已经看到结束处理程序在补救t r y块中的过早退出的执行方面很有效,但也看到结束处理程序由于要阻止t r y块的过早退出而产生了我们不希望有的结果。更好的办法是在结束处理程序的t r y块中避免任何会引起过早退出的语句。实际上,最好将r e t u r n、c o n t i n u e、b r e a k、g o t o等语句从结束处理程序的t r y块和f i n a l l y块中移出去,放在结束处理程序的外面。这样做会使编译程序产生较小的代码,因为不需要再捕捉t r y块中的过早退出,也使编译程序产生更快的代码(因为执行局部展开的指令也少)。另外,代码也更容易阅读和维护
应用结束处理程序编程
我们已经谈过了结束处理程序的基本语法和语意。现在看一看如何用结束处理程序来简化
一个更复杂的编程问题。先看一个完全没有利用结束处理程序的函数:
(唉~~~~~~~~~~~~各位大哥,我实在吃不消打了,所以代码我就不paste上来了,在这里我只说说注意点和编程规范。)
在编程中,有很多人往往喜欢用错误判断,这样造成的后果是代码的可读性很差,代码难以维护,所以我们使用结束处理程序的真正好处是函数的所有清理( c l e a n u p)代码都局部化在一个地方且只在一个地方: f i n a l l y块。如果需要在这个函数中再增加条件代码,只需在f i n a l l y块中简单地增加一个清理行,不需要回到每个可能失败的地方添加清理代码
但是我们已经发现和证实了,如果我们在__try块中使用到了return等跳转语句的时候,系统开销这个问题就产生了。为了帮助避免在t r y块中使用r e t u r n语句,微软在其C / C + +编译程序中增加了另一个关键字__l e a v e。(自己看MSDN把)
在t r y块中使用 l e a v e关键字会引起跳转到t r y块的结尾。可以认为是跳转到t r y块的右大括号。由于控制流自然地从t r y块中退出并进入f i n a l l y块,所以不产生系统开销。当按照这种方式利用结束处理程序来设计函数时,要记住在进入t r y块之前,要将所有资源句柄初始化为无效的值。然后,在f i n a l l y块中,查看哪些资源被成功的分配,就可以知道哪些要释放。另外一种确定需要释放资源的办法是对成功分配的资源设置一个标志。然后, f i n a l l y块中的代码可以检查标志的状态,来确定资源是否要释放。
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块之后的第一条指令恢复。
在现在,软件变的越来越大,如果不用SEH,要实现完全强壮的应用程序简直是不可能的。我们先来看一个样板程序,即C的运行时函数strcpy:这是一个相当简单的函数,它怎么会引起一个进程结束呢?如果调用者对这些参数中的某一个传递NULL(或任何无效的地址),strcpy就引起一个存取异常,并且导致整个进程结束。使用SEH,就可以建立一个完全强壮的strcpy函数:
char* RobustStrcpy(char* strDes, char* strSource)
{
__try
{
strcpy(strDes, strSource);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
file://don't do no thing
}
return strDes;
}
这个函数所做的一切就是将对strcpy的调用置于一个结构化的异常处理框架中。如果strcpy执行成功,函数就返回。如果strcpy引起一个存取异常,导致该线程执行异常处理程序代码。在这个函数中,处理程序代码什么也不做,RobustStrcpy只是返回到它的调用者,根本不会造成进程结束。
下面这段代码中的函数,返回一个字符串里的以空格分界的符号个数:
DWORD Func_RobustTokens(const char* str)
{
DWORD iTokens = -1;
char* strTemp = NULL;
__try
{
strTemp = (char*)malloc(strlen(str)+1);
strcpy(strTemp, str);
char* pszToken = strtok(strTemp, " ");
for(; pszToken!=NULL; pszToken = strtok(NULL," "))
iTokens++;
iTokens++;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
file://Do nothing for test
}
free(strTemp);
return iTokens;
}
看看在几个不同的情况下这个函数能任何做:
首先,如果调用者向函数传递了NULL(或任何无效的内存地址),iTokens被初始化成-1。在try块中对strlen的调用会引起存取异常。异常过滤器获得控制并将控制转移给except块,except块什么也不做。在except块之后,调用free来释放临时内存块。但是,这个内存从未分配,所以结束调用free,向它传递NULL作为参数。ANSI C明确说明用NULL作为参数调用free是合法的。这时free什么也不做,这并不是错误。最后,函数返回-1,指出失败。注意进程并没有结束。
其次,调用者可能向函数传递了一个有效的地址,但对malloc的调用(在try块中)可能失败并返回NULL。这将导致对strcpy的调用引起一个存取异常。同样,异常过滤器被调用,except块执行(什么也不做),free被调用,传递给它NULL(什么也不做),返回-1,告诉调用程序该函数失败。注意进程也没有结束。
最后,假定调用者向函数传递了一个有效的地址,并且对malloc的调用也成功了。这种情
况下,其余的代码也会成功地在iTokens变量中计算符号的数量。在try块的结尾,异常过滤器不会被求值, except块中代码不会被执行,临时内存缓冲区将被释放,并向调用者返
回iTokens。
使用SEH会感觉很好。
前面的几个代码在__except中都没有代码,现在我们来看这段代码:
PBYTE Func_RobustMemDup(PBYTE pbSrc, size_t cb)
{
PBYTE pbDup = NULL;
__try
{
pbDup = (PBYTE)malloc(cb);
memcpy(pbDup, pbSrc, cb);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
free(pbDup);
pbDup = NULL;
}
return pbDup;
}
这个函数分配一个内存缓冲区,并从源块向目的块复制字节。然后函数将复制的内存缓冲区
的地址返回给调用程序(如果函数失败则返回NULL)。希望调用程序在不需要缓冲区时释放它。
这是在except块中实际有代码的第一个例子。我们看一看这个函数在不同条件下是如何执行的。
• 如果调用程序对pbSrc参数传递了一个无效地址,或者如果对malloc的调用失败(返回NULL),memcpy将引起一个存取异常。该存取异常执行过滤器,将控制转移到except块。在except块内,内存缓冲区被释放, pbDup被设置成NULL以便调用程序能够知道函数失败。这里,注意ANSI C允许对free传递NULL。
• 如果调用程序给函数传递一个有效地址,并且如果对malloc的调用成功,则新分配内存
块的地址返回给调用程序
全局展开
当一个异常过滤器的值为EXCEPTION_EXECUTE_HANDLER时,系统必须执行一个全局展开(global unwind)。这个全局展开使所有那些在处理异常的try _ except块之后开始执行但未完成的try_finally块恢复执行。
关于全局展开在这里不在继续介绍了(写这篇文挡已经是第4天了,我倒~),全局展开也就是在try _ except的try中调用了含有try_finally的代码或函数。
另,通过在finally块中放入一个return语句,就可以阻止系统去完成一个全局展开。虽然MS通过这种设计,来让程序员把展开停止,让代码继续下去,但我曾经说过,尽量的避免在finally块中放入return 语句。
EXCEPTION_CONTINUE_EXECUTION
我们再仔细考察一下异常过滤器,看它是如何计算出定义在Excpt.h中的三个异常标识符之一的。在前面的代码中,我们直接对异常的过滤器设置值为EXCEPTION_EXECUTE_HANDLER,但实际上我们应该让根据具体的情况来得到过滤器的值,比如调用一个函数确定应该返回哪一个标识符。
发生一个异常,如果是EXCEPTION_CONTINUE_EXECUTION标志,代码会试图对发生异常的代码再执行一遍(这个时候__except块中的代码并没有被执行!),所以我们使用EXCEPTION_CONTINUE_EXECUTION的时候,一般都会在返回EXCEPTION_CONTINUE_EXECUTION过滤标志之前,把我们的错误代码修改回来,否则会进入到死循环中。下面这段代码就会导致死循环:
DWORD Func_SEHExceptionContinue()
{
DWORD dwTemp = 0;
__try {
dwTemp = 5/dwTemp;
dwTemp += 10;
}
__except(EXCEPTION_CONTINUE_EXECUTION) {
MessageBeep(0);
}
return dwTemp;
}
在使用EXCEPTION_CONTINUE_EXECUTION的时候,一定要特别的小心,有一种情况可以保证每次使用EXCEPTION_CONTINUE_EXECUTION都能成功:
当离散地向一个保留区域提交存储区时。
将虚拟内存技术同结构化异常处理结合起来,可以编写一些执行非常快,非常高效的程序。
EXCEPTION_CONTINUE_SEARCH
EXCEPTION_CONTINUE_SEARCH:是告诉系统继续上溯调用树,去寻找另外的异常过滤器。(如果每个过滤器都返回EXCEPTION_CONTINUE_SEARCH时会出现什么情况呢?在这种情况下,就出现了所谓的“未处理异常”(Unhandled exception))
大家把下面这段代码跟踪一遍就明白了:
void Func_SEHPost(char* sz)
{
__try{
*sz = 0;
}
__except(EXCEPTION_CONTINUE_SEARCH) {
file://Here never Exceute
cout<<"If have message, Here is Error"<
}
DWORD GetExceptionFliterCode(char** ppszBuffer)
{
if(*ppszBuffer == NULL)
{
*ppszBuffer = g_szBuffer;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_EXECUTE_HANDLER;
}
DWORD Func_SEHExceptionSearch()
{
char* pszBuffer = NULL;
__try{
Func_SEHPost(pszBuffer);
}
__except(GetExceptionFliterCode(&pszBuffer))
{
cout<<"Exception Block Run"<
return TRUE;
}
GetExceptionCode
一个异常过滤器在确定要返回什么值之前,必须分析具体情况。例如,异常处理程序可能知道发生了除以0引起的异常时该怎么做,但是不知道该如何处理一个内存存取异常。异常过滤器负责检查实际情况并返回适当的值。
下面的代码举例说明了一种方法,指出所发生异常的类别:
BOOL Func_SEHExceptionGetCodeBase()
{
int x,y;
__try{
x = 0;
y = 4/x;
}
__except((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ? /
EXCEPTION_EXECUTE_HANDLER :/
EXCEPTION_CONTINUE_SEARCH)
{
file://handle divide by zero exception
}
}
GetExceptionCode返回一个值,该值指出所发生异常的种类:
下面列出所有预定义的异常和相应的含意,这些内容取自Platform SDK文档。这些异常标识符可以在WinBase.h文件中找到。我们对这些异常做了分类。
1. 与内存有关的异常
• E X C E P T I O N _ A C C E S S _ V I O L AT I O N。线程试图对一个虚地址进行读或写,但没有做适当的存取。这是最常见的异常。
• E X C E P T I O N _ D ATAT Y P E _ M I S A L I G N M E N T。线程试图读或写不支持对齐( a l i g n m e n t)的硬件上的未对齐的数据。例如, 1 6位数值必须对齐在2字节边界上,3 2位数值要对齐在4字节边界上。
• E X C E P T I O N _ A R R AY _ B O U N D S _ E X C E E D E D。线程试图存取一个越界的数组元素,相应的硬件支持边界检查。
• E X C E P T I O N _ I N _ PA G E _ E R R O R。由于文件系统或一个设备启动程序返回一个读错误,造成不能满足要求的页故障。
• E X C E P T I O N _ G U A R D _ PA G E。一个线程试图存取一个带有PA G E _ G U A R D保护属性的内存页。该页是可存取的,并引起一个E X C E P T I O N _ G U A R D _ PA G E异常。
• EXCEPTION_STA C K _ O V E R F L O W。线程用完了分配给它的所有栈空间。
• E X C E P T I O N _ I L L E G A L _ I N S T R U C T I O N。线程执行了一个无效的指令。这个异常由特定的C P U结构来定义;在不同的C P U上,执行一个无效指令可引起一个陷井错误。
• E X C E P T I O N _ P R I V _ I N S T R U C T I O N。线程执行一个指令,其操作在当前机器模式中不允许。
2. 与异常相关的异常
• E X C E P T I O N _ I N VA L I D _ D I S P O S I T I O N。一个异常过滤器返回一值,这个值不是E X C E P T I O N _ E X E C U T E _ H A N D L E R 、E X C E P T I O N _ C O N T I N U E _ S E A R C H、E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N三者之一。
• E X C E P T I O N _ N O N C O N T I N U A B L E _ E X C E P T I O N。一个异常过滤器对一个不能继续的异常返回E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N。
3. 与调试有关的异常
• EXCEPTION_BREAKPOINT。遇到一个断点。
• E X C E P T I O N _ S I N G L E _ S T E P。一个跟踪陷井或其他单步指令机制告知一个指令已执行完毕。
• E X C E P T I O N _ I N VA L I D _ H A N D L E。向一个函数传递了一个无效句柄。
4. 与整数有关的异常
• EXCEPTION_INT_DIVIDE_BY_ZERO。线程试图用整数0来除一个整数
• EXCEPTION_INT_OVERFLOW。一个整数操作的结果超过了整数值规定的范围。
5. 与浮点数有关的异常
• E X C E P T I O N _ F LT _ D E N O R M A L _ O P E R A N D。浮点操作中的一个操作数不正常。不正常的值是一个太小的值,不能表示标准的浮点值。
• EXCEPTION_FLT _ D I V I D E _ B Y _ Z E R O。线程试图用浮点数0来除一个浮点。
• EXCEPTION_FLT _ I N E X A C T _ R E S U LT。浮点操作的结果不能精确表示成十进制小数。
• EXCEPTION_FLT _ I N VA L I D _ O P E R AT I O N。表示任何没有在此列出的其他浮点数异常。
• EXCEPTION_FLT _ O V E R F L O W。浮点操作的结果超过了允许的值。
• EXCEPTION_FLT _ S TA C K _ C H E C K。由于浮点操作造成栈溢出或下溢。
• EXCEPTION_FLT _ U N D E R F L O W。浮点操作的结果小于允许的值。
函数GetExceptionCode只能在一个过滤器中调用(__except之后的括号里),或在一个异常处理程序中被调用。记住,不能在一个异常过滤器函数里面调用GetExceptionCode。编译程序会捕捉这样的错误。
还有,上面的异常代码遵循在WinError.h文件中定义的有关错误代码的规则。(捎后我会写一段小的文章表明WinError.h的作用)
GetExceptionInformation
当一个异常发生时,操作系统要向引起异常的线程的栈里压入三个结构,这三个结构是:EXCEPTION_RECORD结构、CONTEXT结构和EXCEPTION_POINTERS结构。
EXCEPTION_RECORD结构包含有关已发生异常的独立于C P U的信息,
CONTEXT结构包含已发生异常的依赖于C P U的信息。
EXCEPTION_POINTERS结构只有两个数据成员,二者都是指针,分别指向被压入栈的EXCEPTION_POINTERS和CONTEXT结构。
为了取得这些信息并在你自己的程序中使用这些信息,需要调用GetExceptionInformation
函数:这个内部函数返回一个指向EXCEPTION_POINTERS结构的指针。
关于GetExceptionInformation函数,要记住的最重要事情是它只能在异常过滤器中调用,因为仅仅在处理异常过滤器时, EXCEPTION_RECORD和EXCEPTION_POINTERS才是有效的。一旦控制被转移到异常处理程序,栈中的数据就被删除。
如果需要在你的异常处理程序块里面存取这些异常信息(虽然很少有必要这样做),必须将EXCEPTION_POINTERS结构所指向的CONTEXT数据结构和/或EXCEPTION_RECORD数据结构保存在你所建立的一个或多个变量里。下面的代码说明了如何保存EXCEPTION_RECORD和CONTEXT数据结构:
void Func_SEHExceptionGetInfo()
{
EXCEPTION_RECORD SaveExceptRec;
CONTEXT SaveExceptContext;
int x = 0;
__try{
x = 5/x;
}
__except(SaveExceptRec =
*(GetExceptionInformation())->ExceptionRecord,
SaveExceptContext =
*(GetExceptionInformation())->ContextRecord,
EXCEPTION_EXECUTE_HANDLER
)
{
switch(SaveExceptRec.ExceptionCode)
{
file://Here, Do your thing for code value
}
}
}
上面的代码中:注意在异常过滤器中C语言逗号(,)操作符的使用。许多程序员不习惯使用这个操作符。它告诉编译程序从左到右执行以逗号分隔的各表达式。当所有的表达式被求值之后,返回最后的(或最右的)表达式的结果。
下面是EXCEPTION_RECORD的结构:
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
EXCEPTION_RECORD结构包含有关最近发生的异常的详细信息,这些信息独立于C P U:
• ExceptionCode包含异常的代码。这同内部函数GetExceptionCode返回的信息是一样的。
• ExceptionFlags包含有关异常的标志。当前只有两个值,分别是0(指出一个可以继续的异常)和E X C E P T I O N _ N O N C O N T I N U A B L E(指出一个不可继续的异常)。在一个不可继续的异常之后,若要继续执行,会引发一个E X C E P T I O N _ N O N C O N T I N U A B L E _E X C E P T I O N异常。
• E x c e p t i o n R e c o r d指向另一个未处理异常的E X C E P T I O N _ R E C O R D结构。在处理一个异常的时候,有可能引发另外一个异常。例如,异常过滤器中的代码就可能用零来除一个数。当嵌套异常发生时,可将异常记录链接起来,以提供另外的信息。如果在处理一个异常过滤器的过程当中又产生一个异常,就发生了嵌套异常。如果没有未处理异常,这个成员就包含一个N U L L。
• ExceptionAddress指出产生异常的C P U指令的地址。
• N u m b e r P a r a m e t e r s 规定了与异常相联系的参数数量( 0 到1 5 )。这是在E x c e p t i o n I n f o r m a t i o n数组中定义的元素数量。对几乎所有的异常来说,这个值都是零。
• E x c e p t i o n I n f o r m a t i o n规定一个附加参数的数组,用来描述异常。对大多数异常来说,数组元素是未定义的。
E X C E P T I O N _ R E C O R D结构的最后两个成员,N u m b e r P a r a m e t e r s和E x c e p t i o n I n f o r m a t i o n向异常过滤器提供一些有关异常的附加信息。目前只有一种类型的异常提供附加信息,就是E X C E P T I O N _ A C C E S S _ V I O L AT I O N。所有其他可能的异常都将N u m b e r P a r a m e t e r s设置成零。我们可以检验E x c e p t i o n I n f o r m a t i o n的数组成员来查看关于所产生异常的附加信息。对于一个E X C E P T I O N _ A C C E S S _ V I O L AT I O N异常来说,E x c e p t i o n I n f o r m a t i o n [ 0 ]包含一个标志,指出引发这个存取异常的操作的类型。如果这个值是0,表示线程试图要读不可访问的数据。如果这个值是1,表示线程要写不可访问的数据。ExceptionInformation[1] 指出不可访问数据的地址。通过使用这些成员,我们可以构造异常过滤器,提供大量有关程序的信息。
本质上,对C P U上每一个可用的寄存器,这个结构相应地包含一个成员。当一个异常被引发时,可以通过检验这个结构的成员找到更多的信息。遗憾的是,为了得到这种可能的好处,要求程序员编写依赖于平台的代码,以确认程序所运行的机器,使用适当的CONTEXT结构。最好的办法是在代码中安排一个# ifdefs指令。Windows支持的不同CPU的CONTEXT结构定义在WinNT.h文件中。
软件异常
到目前为止,我们一直在讨论硬件异常,也就是C P U捕获一个事件并引发一个异常。在代码中也可以强制引发一个异常。这也是一个函数向它的调用者报告失败的一种方法。传统上,失败的函数要返回一些特殊的值来指出失败。函数的调用者应该检查这些特殊值并采取一种替代的动作。通常,这个调用者要清除所做的事情并将它自己的失败代码返回给它的调用者。这种错误代码的逐层传递会使源程序的代码变得非常难于编写和维护。另外一种方法是让函数在失败时引发异常。用这种方法,代码更容易编写和维护,而且也执行得更好,因为通常不需要执行那些错误测试代码。实际上,仅当发生失败时也就是发生异常时才执行错误测试代码。但令人遗憾的是,许多开发人员不习惯于在错误处理中使用异常。这有两方面的原因。第一个原因是多数开发人员不熟悉S E H。即使有一个程序员熟悉它,但其他程序员可能不熟悉它。如果一个程序员编写了一个引发异常的函数,但其他程序员并不编写S E H框架来捕获这个异常,那么进程就会被操作系统结束。开发人员不使用S E H的第二个原因是它不能移植到其他操作系统。许多公司的产品要面向多种操作系统,因此希望有单一的源代码作为产品的基础,这是可以理解的。S E H是专门针对Wi n d o w s的技术。
本段讨论通过异常返回错误有关的内容。首先,让我们看一看Windows Heap函数,例如HeapCreate、HeapAlloc等。通常当某个堆( h e a p)函数失败,它会返回N U L L来指出失败。然而可以对这些堆函数传递HEAP_GENERATE_EXCEPTIONS标志。如果使用这个标志并且函数失败,函数不会返回N U L L,而是由函数引发一个STATUS_NO_MEMOR软件异常,程序代码的其他部分可以用S E H框架来捕获这个异常。
如果想利用这个异常,可以编写你的t r y块,好像内存分配总能成功。如果内存分配失败,可以利用e x c e p t块来处理这个异常,或通过匹配t r y块与f i n a l l y块,清除函数所做的事。这非常方便。
程序捕获软件异常采取的方法与捕获硬件异常完全相同。也就是说,前面介绍的内容可以同样适用于软件异常。这里讨论如何让你自己的函数引发软件异常,作为指出失败的方法。实际上,可以用类似于微软实现堆函数的方法来实现你的函数:让函数的调用者传递一个标志,告诉函数如何指出失败。
引发一个软件异常很容易,只需要调用RaiseException函数:
RaiseException
This function raises an exception in the calling thread.
void RaiseException(
DWORD dwExceptionCode,
DWORD dwExceptionFlags,
DWORD nNumberOfArguments,
const DWORD *lpArguments);
第一个参数d w E x c e p t i o n C o d e是标识所引发异常的值。H e a p A l l o c函数对这个参数设定S TAT U S _ N O _ M E M O RY。如果程序员要定义自己的异常标识符,应该遵循标准Wi n d o w s错误代码的格式,像Wi n E r r o r. h文件中定义的那样。如果要建立你自己的异常代码,要填充D W O R D的4个部分:
• 第3 1位和第3 0位包含严重性系数( s e v e r i t y )。
• 第2 9位是1(0表示微软建立的异常,如H e a p A l l o c的S TAT U S _ N O _ M E M O RY)。
• 第2 8位是0。
• 第2 7位到1 6位是某个微软定义的设备代码。
• 第1 5到0位是一个任意值,用来标识引起异常的程序段。
R a i s e E x c e p t i o n 的第二个参数d w E x c e p t i o n F l a g s ,必须是0 或E X C E P T I O N _N O N C O N T I N U A B L E。
本质上,这个标志是用来规定异常过滤器返回E X C E P T I O N _CONTINUE _EXECUTION来响应所引发的异常是否合法。如果没有向R a i s e E x c e p t i o n传递EXCEPTION_ NONCONTINUABLE参数值,则过滤器可以返回E X C E P T I O N _ C O N T I N U E _E X E C U T I O N。正常情况下,这将导致线程重新执行引发软件异常的同一C P U指令。但微软已做了一些动作,所以在调用R a i s e E x c e p t i o n函数之后,执行会继续进行。如果你向R a i s e E x c e p t i o n传递了E X C E P T I O N _ N O N C O N T I N U A B L E标志,你就是在告诉系统,你引发异常的类型是不能被继续执行的。这个标志在操作系统内部被用来传达致命(不可恢复)的错误信息。另外,当H e a p A l l o c引发S TAT U S _ N O _ M E M O RY软件异常时,它使用E X C E P T I O N _ N O N C O N T I N U A B L E标志来告诉系统,这个异常不能被继续。意思就是没有办法强制分配内存并继续运行。如果一个过滤器忽略E X C E P T I O N _ N O N C O N T I N U A B L E并返回E X C E P T I O N _ C O N T I N U E _
E X E C U T I O N,系统会引发新的异常:E X C E P T I O N _ N O N C O N T I N U A B L E _ E X C E P T I O N。
当程序在处理一个异常的时候,有可能又引发另一个异常。比如说,一个无效的内存存取
有可能发生在一个f i n a l l y块、一个异常过滤器、或一个异常处理程序里。当发生这种情况时,系统压栈异常。回忆一下G e t E x c e p t i o n I n f o r m a t i o n函数。这个函数返回EXCEPTION_ POINTERS结构的地址。E X C E P T I O N _ P O I N T E R S的E x c e p t i o n R e c o r d成员指向一个EXCEPTION_ R E C O R D结构,这个结构包含另一个E x c e p t i o n R e c o r d成员。这个成员是一个指向另外的E X C E P T I O N _R E C O R D的指针,而这个结构包含有关以前引发异常的信息。
通常系统一次只处理一个异常,并且E x c e p t i o n R e c o r d成员为N U L L。然而如果处理一个异常的过程中又引发另一个异常,第一个E X C E P T I O N _ R E C O R D结构包含有关最近引发异常的信息,并且这个E X C E P T I O N _ R E C O R D结构的E x c e p t i o n R e c o r d成员指向以前发生的异常的E X C E P T I O N _R E C O R D结构。如果增加的异常没有完全处理,可以继续搜索这个E X C E P T I O N _ R E C O R D结构的链表,来确定如何处理异常。
R a i s e E x c e p t i o n的第三个参数n N u m b e r O f A rg u m e n t s和第四个参数p A rg u m e n t s,用来传递有关所引发异常的附加信息。通常,不需要附加的参数,只需对p A rg u m e n t s参数传递N U L L,这种情况下, R a i s e E x c e p t i o n函数忽略n N u m b e r O f A rg u m e n t s参数。如果需要传递附加参数,n N u m b e r O f A rg u m e n t s参数必须规定由p A rg u m e n t s参数所指向的U L O N G _ P T R数组中的元素数目。这个数目不能超过E X C E P T I O N _ M A X I M U M _ PA R A M E T E R S,EXCEPTION_ MAXIMUM_PARAMETERS 在Wi n N T. h中定义成1 5。在处理这个异常期间,可使异常过滤器参照E X C E P T I O N _ R E C O R D结构中的N u m b e r P a r a m e t e r s和E x c e p t i o n I n f o r m a t i o n成员来检查n N u m b e r O f A rg u m e n t s和p A rg u m e n t s参数中的信息。
你可能由于某种原因想在自己的程序中产生自己的软件异常。例如,你可能想向系统的事
件日志发送通知消息。每当程序中的一个函数发现某种问题,你可以调用R a i s e E x c e p t i o n并让某些异常处理程序上溯调用树查看特定的异常,或者将异常写到日志里或弹出一个消息框。你还可能想建立软件异常来传达程序内部致使错误的信息。