----------------------------------------------------------------------------------------------------------------------------
SEH的强大功能之二
本文转自 http://se.csai.cn/ExpertEyes/No160.htm上一篇文章讲述了SEH的异常处理机制,也即try-except模型的使用规则。本篇文章继续探讨SEH另外一项很重要的机制,那就是“有效保证资源的清除”,其实这才是SEH设计上最为精华的一个东东,对于C程序而言,它贡献简直是太大了。
SEH的这项机制被称为结束处理(Termination Handling),它是通过try-finally语句来实现的,下面开始讨论吧!
try-finally的作用
对于try-finally的作用,还是先看看MSDN中怎么说的吧!摘略如下:
The try-finally statement is a Microsoft extension to the C and C++ languages that enables 32-bit target applications to guarantee execution of cleanup code when execution of a block of code is interrupted. Cleanup consists of such tasks as deallocating memory, closing files, and releasing file handles. The try-finally statement is especially useful for routines that have several places where a check is made for an error that could cause premature return from the routine.
上面的这段话的内容翻译如下:
try-finally语句是Microsoft对C和C++语言的扩展,它能使32位的目标程序在异常出现时,有效保证一些资源能够被及时清除,这些资源的清除任务可以包括例如内存的释放,文件的关闭,文件句柄的释放等等。try-finally语句特别适合这样的情况下使用,例如一个例程(函数)中,有几个地方需要检测一个错误,并且在错误出现时,函数可能提前返回。
try-finally的语法规则
上面描述try-finally机制的有关作用时,也许一时我们还难以全面理解,不过没关系,这里还是先看一下try-finally的语法规则吧!其实它很简单,示例代码如下:
//seh-test.c
#include <windows.h>
#include <stdio.h>
void main()
{
puts("hello");
__try
{
puts("__try块中");
}
// 注意,这里不是__except块,而是__finally取代
__finally
{
puts("__finally块中");
}
puts("world");
}
上面的程序运行结果如下:
hello
__try块中
__finally块中
world
Press any key to continue
try-finally语句的语法与try-except很类似,稍有不同的是,__finally后面没有一个表达式,这是因为try-finally语句的作用不是用于异常处理,所以它不需要一个表达式来判断当前异常错误的种类。另外,与try-except语句类似,try-finally也可以是多层嵌套的,并且一个函数内可以有多个try-finally语句,不管它是嵌套的,或是平行的。当然,try-finally多层嵌套也可以是跨函数的。这里不一一列出示例,大家可以自己测试一番。
另外,对于上面示例程序的运行结果,是不是觉得有点意料之外呢?因为__finally块中的put(“__finally块中”)语句也被执行了。是的,没错!这就是try-finally语句最具有魔幻能力的地方,即“不管在何种情况下,在离开当前的作用域时,finally块区域内的代码都将会被执行到”。呵呵!这的确是很厉害吧!为了验证这条规则,下面来看一个更典型示例,代码如下:
#include <stdio.h>
void main()
{
puts("hello");
__try
{
puts("__try块中");
// 注意,下面return语句直接让函数返回了
return;
}
__finally
{
puts("__finally块中");
}
puts("world");
}
上面的程序运行结果如下:
hello
__try块中
__finally块中
Press any key to continue
上面的程序运行结果是不是有点意思。在__try块区域中,有一条return语句让函数直接返回了,所以后面的put(“world”)语句没有被执行到,这是很容易被理解的。但是请注意,__finally块区域中的代码也将会被予以执行过了,这是不是进一步验证了上面了那条规则,呵呵!阿愚深有感触的想:“__finally的特性真的很像对象的析构函数”,朋友们觉得如何呢?
另外,大家也许还特别关心的是,goto语句是不是有可能破坏上面这条规则呢?因为在C语言中,goto语句一般直接对应一条jmp跳转指令,所以如果真的如此的话,那么goto语句很容易破坏上面这条规则。还是看一个具体的例子吧!
#include <stdio.h>
void main()
{
puts("hello");
__try
{
puts("__try块中");
// 跳转指令
goto RETURN;
}
__finally
{
puts("__finally块中");
}
RETURN:
puts("world");
}
上面的程序运行结果如下:
hello
__try块中
__finally块中
world
Press any key to continue
呵呵!即便上面的示例程序中,goto语句跳过了__finally块,但是__finally块区域中的代码还是被予以执行了。当然,大家也许很关心这到底是为什么?为什么try-finally语句具有如此神奇的功能?这里不打算深入阐述,在后面阐述SEH实现的时候会详细分析到。这里朋友们只牢记一点,“不管是顺序的线性执行,还是return语句或goto语句无条件跳转等情况下,一旦执行流在离开当前的作用域时,finally块区域内的代码必将会被执行”
try-finally块中的异常
上面只列举了return语句和goto语句的情况下,但是如果程序中出现异常的话,那么finally块区域内的代码还会被执行吗?上面所讲到的那条规则仍然正确吗?还是看看示例,代码如下:
#include <stdio.h>
void test()
{
puts("hello");
__try
{
int* p;
puts("__try块中");
// 下面抛出一个异常
p = 0;
*p = 25;
}
__finally
{
// 这里会被执行吗
puts("__finally块中");
}
puts("world");
}
void main()
{
__try
{
test();
}
__except(1)
{
puts("__except块中");
}
}
上面的程序运行结果如下:
hello
__try块中
__finally块中
__except块中
Press any key to continue
从上面示例程序的运行结果来看,它是和“不管在何种情况下,在离开当前的作用域时,finally块区域内的代码都将会被执行到”这条规则相一致的。
__leave关键字的作用
其实,总结上面的__finally块被执行的流程时,无外乎三种情况。第一种就是顺序执行到__finally块区域内的代码,这种情况很简单,容易理解;第二种就是goto语句或return语句引发的程序控制流离开当前__try块作用域时,系统自动完成对__finally块代码的调用;第三种就是由于在__try块中出现异常时,导致程序控制流离开当前__try块作用域,这种情况下也是由系统自动完成对__finally块的调用。无论是第2种,还是第3种情况,毫无疑问,它们都会引起很大的系统开销,编译器在编译此类程序代码时,它会为这两种情况准备很多的额外代码。一般第2种情况,被称为“局部展开(LocalUnwinding)”;第3种情况,被称为“全局展开(GlobalUnwinding)”。在后面阐述SEH实现的时候会详细分析到这一点。
第3种情况,也即由于出现异常而导致的“全局展开”,对于程序员而言,这也许是无法避免的,因为你在利用异常处理机制提高程序可靠健壮性的同时,不可避免的会引起性能上其它的一些开销。呵呵!这世界其实也算瞒公平的,有得必有失。
但是,对于第2种情况,程序员完全可以有效地避免它,避免“局部展开”引起的不必要的额外开销。实际这也是与结构化程序设计思想相一致的,也即一个程序模块应该只有一个入口和一个出口,程序模块内尽量避免使用goto语句等。但是,话虽如此,有时为了提高程序的可读性,程序员在编写代码时,有时可能不得不采用一些与结构化程序设计思想相悖的做法,例如,在一个函数中,可能有多处的return语句。针对这种情况,SEH提供了一种非常有效的折衷方案,那就是__leave关键字所起的作用,它既具有像goto语句和return语句那样类似的作用(由于检测到某个程序运行中的错误,需要马上离开当前的__try块作用域),但是又避免了“局部展开” 的额外开销。还是看个例子吧!代码如下:
#include <stdio.h>
void test()
{
puts("hello");
__try
{
int* p;
puts("__try块中");
// 直接跳出当前的__try作用域
__leave;
p = 0;
*p = 25;
}
__finally
{
// 这里会被执行吗?当然
puts("__finally块中");
}
puts("world");
}
void main()
{
__try
{
test();
}
__except(1)
{
puts("__except块中");
}
}
上面的程序运行结果如下:
hello
__try块中
__finally块中
world
Press any key to continue
这就是__leave关键字的作用,也许大家在编程时很少使用它。但是请注意,如果你的程序中,尤其在那些业务特别复杂的函数模块中,既采用了SEH机制来保证程序的可靠性,同时代码中又拥有大量的goto语句和return语句的话,那么你的源代码编译出来的二进制程序将是十分糟糕的,不仅十分庞大,而且效率也受很大影响。此时,建议不妨多用__leave关键字来提高程序的性能。
try-finally深入
现在,相信我们已经对try-finally机制有了非常全面的了解,为了更进一步认识try-finally机制的好处(当然,主人公阿愚认为,那些写过Windows平台下设备驱动程序的朋友一定深刻认识到try-finally机制的重要性),这里给出一个具体的例子。还记得,在《第21集 Windows系列操作系统平台中所提供的异常处理机制》中,所讲述到的采用setjmp和longjmp异常处理机制实现的那个简单例程吗?现在如果有了try-finally机制,将能够很容易地来避免内存资源的泄漏,而且还极大地提高了程序模块的可读性,减少程序员由于不小心造成的程序bug等隐患。采用SEH重新实现的代码如下:
#include <stdio.h>
#include <stdlib.h>
void test1()
{
char* p1, *p2, *p3, *p4;
__try
{
p1 = malloc(10);
p2 = malloc(10);
p3 = malloc(10);
p4 = malloc(10);
// do other job
// 期间可能抛出异常
}
__finally
{
// 这里保证所有资源被及时释放
if(p1) free(p1);
if(p2) free(p2);
if(p3) free(p3);
if(p4) free(p4);
}
}
void test()
{
char* p;
__try
{
p = malloc(10);
// do other job
// 期间可能抛出异常
test1();
// do other job
}
__finally
{
// 这里保证资源被释放
if(p) free(p);
}
}
void main( void )
{
__try
{
char* p;
__try
{
p = malloc(10);
// do other job
// 期间可能抛出异常
test();
// do other job
}
__finally
{
// 这里保证资源被释放
if(p) free(p);
}
}
__except(1)
{
printf("捕获到一个异常/n");
}
}
呵呵!上面的代码与采用setjmp和longjmp机制实现的代码相比,是不是更简洁,更美观。这就是try-finally语句的贡献所在。
总结
(1) “不管在何种情况下,在离开当前的作用域时,finally块区域内的代码都将会被执行到”,这是核心法则。
(2) try-finally语句的作用相当于面向对象中的析构函数。
(3) goto语句和return语句,在其它少数情况下,break语句以及continue语句等,它们都可能会导致程序的控制流非正常顺序地离开__try作用域,此时会发生SEH的“局部展开”。记住,“局部展开”会带来较大的开销,因此,程序员应该尽可能采用__leave关键字来减少一些不必要的额外开销。
通过这几篇文章中对SEH异常处理机制的深入阐述,相信大家已经能够非常熟悉使用SEH来进行编程了。下一篇文章把try-except和try-finally机制结合起来,进行一个全面而综合的评述,继续吧!
------------------------------------------------------------------------------------------------------------------------------
SEH的综合
转自 http://se.csai.cn/ExpertEyes/No161.htmSEH模型主要包括try-except异常处理机制和try-finally结束处理机制,而且这两者能够很好地有机统一起来,它们结合使用时,能够提供给程序员非常强大、非常灵活的控制手段。其实这在上一篇文章中的几个例子中已经使用到,这里将继续进行系统的介绍,特别是try-except和try-finally结合使用时的一些细节问题。
try-except和try-finally组合使用
try-except和try-finally可以组合起来使用,它们可以是平行线性的关系,也可以是嵌套的关系。而且不仅是try-except语句中可以嵌套try-finally语句;同时try-finally语句中也可以嵌套try-except语句。所以它们的使用起来非常灵活,请看下面的代码:
// 例程1,try-except语句与try-finally语句平行关系
#include <stdio.h>
void main()
{
puts("hello");
__try
{
int* p;
puts("__try块中");
// 下面抛出一个异常
p = 0;
*p = 25;
}
__except(1)
{
puts("__except块中");
}
__try
{
}
__finally
{
puts("__finally块中");
}
puts("world");
}
result:
hello
__try块中
__except块中
__finally块中
world
// 例程2,try-except语句中嵌套try-finally
#include <stdio.h>
void main()
{
puts("hello");
__try
{
__try
{
int* p;
puts("__try块中");
// 下面抛出一个异常
p = 0;
*p = 25;
}
__finally
{
// 这里会被执行吗
puts("__finally块中");
}
}
__except(1)
{
puts("__except块中");
}
puts("world");
}
rerult:
hello
__try块中
__finally块中
__except块中
world
// 例程3,try-finally语句中嵌套try-except
#include <stdio.h>
void main()
{
puts("hello");
__try
{
__try
{
int* p;
puts("__try块中");
// 下面抛出一个异常
p = 0;
*p = 25;
}
__except(1)
{
puts("__except块中");
}
}
__finally
{
puts("__finally块中");
}
puts("world");
}
result:
hello
__try块中
__except块中
__finally块中
world
try-except和try-finally组合使用时,需注意的事情
在C++异常模型中,一个try block块可以拥有多个catch block块相对应,但是在SEH模型中,一个__try块只能是拥有一个__except块或一个__finally块相对应,例如下面的程序代码片断是存在语法错误的。
// 例程1,一个__try块,两个__except块
#include <stdio.h>
void main()
{
puts("hello");
__try
{
int* p;
puts("__try块中");
// 下面抛出一个异常
p = 0;
*p = 25;
}
__except(1)
{
puts("__except块中");
}
// 这里有语法错误
__except(1)
{
puts("__except块中");
}
puts("world");
}
// 例程2,一个__try块,两个__finally块
#include <stdio.h>
void main()
{
puts("hello");
__try
{
puts("__try块中");
}
__finally
{
puts("__finally块中");
}
// 这里有语法错误
__finally
{
puts("__finally块中");
}
puts("world");
}
// 例程3,一个__try块,对应一个__finally块和一个__except块
#include <stdio.h>
void main()
{
puts("hello");
__try
{
int* p;
puts("__try块中");
// 下面抛出一个异常
p = 0;
*p = 25;
}
__finally
{
puts("__finally块中");
}
// 这里有语法错误
__except(1)
{
puts("__except块中");
}
puts("world");
}
温过而知新
这里给出最后一个简单的try-except和try-finally相结合的例子,让我们温过而知新。代码如下(这是MSDN中的例程):
#include "stdio.h"
void test()
{
int* p = 0x00000000; // pointer to NULL
__try
{
puts("in try");
__try
{
puts("in try");
// causes an access violation exception;
// 导致一个存储异常
*p = 13;
// 呵呵,注意这条语句
puts("这里不会被执行到");
}
__finally
{
puts("in finally");
}
// 呵呵,注意这条语句
puts("这里也不会被执行到");
}
__except(puts("in filter 1"), 0)
{
puts("in except 1");
}
}
void main()
{
puts("hello");
__try
{
test();
}
__except(puts("in filter 2"), 1)
{
puts("in except 2");
}
puts("world");
}
上面的程序运行结果如下:
hello
in try
in try
in filter 1
in filter 2
in finally
in except 2
world
Press any key to continue
下面用图表描述一下上面例程运行时的执行流程,如下图所示。
总结
(1) try-except和try-finally可以组合起来使用,它们可以是平行线性的关系,也可以是嵌套的关系。而且不仅是try-except语句中可以嵌套try-finally语句;同时try-finally语句中也可以嵌套try-except语句。
(2) 一个__try块只能是拥有一个__except块或一个__finally块相对应。
至此,关于SEH的__try、__except、__finally、__leave模型已经基本阐述完毕,但是主人公阿愚认为,有关SEH模型机制,还有一个非常关键的内容没有阐述到,那就是SEH与C++异常处理模型可以结合使用吗?如果可以的话?它们组合使用时,有什么限制吗?或带来什么不良后果吗?
大家知道,Windows平台提供的SEH机制虽然主要是应用于C语言的程序,以便第三厂商开发出高效的设备驱动程序来。但是__try、__except、__finally、__leave模型同样也可以在C++程序中使用,这在MSDN中已经提到,虽然微软还是建议,在C++程序中尽量采用C++的异常处理模型。
但是对于广大程序员而言,大家有必要知道,__try、__except、__finally、__leave模型在C++程序中使用时的一些限制。下一篇文章中,阿愚将把自己总结的一些经验和体会与大家一块分享。去看看吧!GO!
------------------------------------------------------------------------------------------------------------------------
SEH 可以在 C++ 程序中使用
本文转自 http://se.csai.cn/ExpertEyes/No162.htm首先声明的是, C++ 中的异常处理机制是建立在 Windows 平台上的 SEH 机制之上,所以 SEH 当然可以在 C++ 程序中使用。不过“阿愚”多次强调过,我们平常一般狭义上的 SEH 都是指 try-except 和 try-finally 异常机制,而它们是给 C 语言( VC 环境)编写 windows driver 而设计的,所以 SEH 主要应该在 C 程序中被使用,而 C++ 程序则应该使用 try-catch 机制的 C++ 异常处理模型( micorsoft 的 MSDN 一直强烈建议程序员遵循此规则)。但是, SEH 到底能在 C++ 程序中使用吗?“当然可以,肯定可以”,其实在一开始阐述 Windows 平台多种异常机制之间的关系时,就已经清楚地表明了这一点。
这篇文章系统地来看看 SEH 在 C++ 程序中的各种使用情况。
先来一个简单的例子
其实简单的例子,就是把以前的使用 SEH 机制的 C 程序,改称 C++ 程序,看它能正常编译和运行否?什么意思,很简单,就是把原来的 .c 程序的扩展名改为 .cpp 文件,也即此时 VC 编译器会采用 C++ 语言来编译此程序(这也即为 C++ 程序了)。朋友们试试吧!代码如下:
// 注意,这是 C++ 程序,文件名为: SEH-test.cpp
#include "stdio.h"
void test()
{
int* p = 0x00000000; // pointer to NULL
__try
{
puts("in try");
__try
{
puts("in try");
// causes an access violation exception;
// 导致一个存储异常
*p = 13;
puts(" 这里不会被执行到 ");
}
__finally
{
puts("in finally");
}
puts(" 这里也不会被执行到 ");
}
__except(puts("in filter 1"), 0)
{
puts("in except 1");
}
}
void main()
{
puts("hello");
__try
{
test();
}
__except(puts("in filter 2"), 1)
{
puts("in except 2");
}
puts("world");
}
是不是能编译通过,而且运行结果也和以前 c 程序的运行结果完全一致,如下:
hello
in try
in try
in filter 1
in filter 2
in finally
in except 2
world
Press any key to continue
来一个真正意义上的 C++ 程序,且使用 SEH 机制。
也许很多程序员朋友对上面的例子不以为然,觉得它说明不了什么问题。好的,现在我们来看一个 真正意义上的 C++ 程序,且使用 SEH 机制。什么是真正意义上的 C++ 程序,当然是采用了面向对象技术。看例子吧!代码如下(其实就是在上面程序的基础上加了一个 class ):
// 注意,这是 C++ 程序,文件名为: SEH-test.cpp
#include "stdio.h"
class A
{
public:
void f1() {}
void f2() {}
};
void test1()
{
A a1;
A a2,a3;
a2.f1();
a3.f2();
}
void test()
{
int* p = 0x00000000; // pointer to NULL
__try
{
// 这里调用 test1 函数,它函数内部会创造 object
// 应该属于 100% 的 C++ 程序了吧!
test1();
puts("in try");
__try
{
puts("in try");
// causes an access violation exception;
// 导致一个存储异常
*p = 13;
puts(" 这里不会被执行到 ");
}
__finally
{
puts("in finally");
}
puts(" 这里也不会被执行到 ");
}
__except(puts("in filter 1"), 0)
{
puts("in except 1");
}
}
void main()
{
puts("hello");
__try
{
test();
}
__except(puts("in filter 2"), 1)
{
puts("in except 2");
}
puts("world");
}
总结
通过以上实践得知, SEH 的确可以在 C++ 程序中使用,而且 SEH 不仅可以在 C++ 程序中使用;更进一步, SEH 异常机制( try-except 和 try-finally )还可以与 C++ 异常处理模型( try-catch ),两者在同一个 C++ 程序中混合使用,这在下一篇文章中接着讨论。
但问题是,微软 MSDN 的忠告( C 程序中使用 try-except 和 try-finally ;而 C++ 程序则应该使用 try-catch ),这岂不是完全属于吓唬人吗?非也!非也!微软的建议一点也没有错, SEH 与 C++ 异常处理机制混合使用时,的确有一定的约束(虽然,平时我们很少关心这一点),这同样也在下一篇文章中详细接着讨论, 程序员朋友们,继续吧!
------------------------------------------------------------------------------------------------------------------------------------------
SEH 与 C++ 异常模型的混合使用
本文转自 http://se.csai.cn/ExpertEyes/No163.htm在上一篇文章中我们看到了,在 C++ 程序中可以能够很好地使用 SEH 的 try-except 和 try-finally 机制(虽然 MSDN 中不建议这样做),这一篇文章中我们继续讨论,在 C++ 程序中同时使用 try-except 异常机制( SEH )和 try-catch 异常机制( C++ 异常模型 )的情况。
朋友们,准备好了心情吗?这可是有点复杂呦!
如何混合使用呢?
同样,还是看例子先。仍然是在原来例程的代码基础上做修改,修改后的代码如下:
// 注意,这是 C++ 程序,文件名为: SEH-test.cpp
#include "stdio.h"
class A
{
public:
void f1() {}
// 抛出 C++ 异常
void f2() { throw 888;}
};
// 这个函数中使用了 try-catch 处理异常,也即 C++ 异常处理
void test1()
{
A a1;
A a2,a3;
try
{
a2.f1();
a3.f2();
}
catch(int errorcode)
{
printf("catch exception,error code:%d/n", errorcode);
}
}
// 这个函数没什么改变,仍然采用 try-except 异常机制,也即 SEH 机制
void test()
{
int* p = 0x00000000; // pointer to NULL
__try
{
// 这里调用 test1 函数
test1();
puts("in try");
__try
{
puts("in try");
// causes an access violation exception;
// 导致一个存储异常
*p = 13;
puts(" 这里不会被执行到 ");
}
__finally
{
puts("in finally");
}
puts(" 这里也不会被执行到 ");
}
__except(puts("in filter 1"), 0)
{
puts("in except 1");
}
}
void main()
{
puts("hello");
__try
{
test();
}
__except(puts("in filter 2"), 1)
{
puts("in except 2");
}
puts("world");
}
上面程序不仅能够被编译通过,而且运行结果也是正确的(和预期的一样,同样符合 C++ 异常处理模型的规则,和 SEH 异常模型的处理规则)。其结果如下:
hello
catch exception,error code:888
in try
in try
in filter 1
in filter 2
in finally
in except 2
world
Press any key to continue
继续深入刚才的例子
上面的例程中,虽然在同一个程序中既有 try-catch 机制,又有 try-except 机制,当然这也完全算得上 SEH 与 C++ 异常模型的混合使用 。但是,请注意,这两种机制其实是完全被分割开的,它们完全被分割在了两个函数的内部。也即这两种机制其实并没有完全交互起来,换句话说,它们还算不上两种异常处理机制真正的混合使用。这里继续给出一个更绝的例子,还是先来看看代码吧,如下:
// 注意,这是 C++ 程序,文件名为: SEH-test.cpp
#include "stdio.h"
class MyException
{
public:
MyException() {printf(" 构造一个 MyException 对象 /n");}
MyException(const MyException& e) {printf(" 复制一个 MyException 对象 /n");}
operator=(const MyException& e) {printf(" 复制一个 MyException 对象 /n");}
~MyException() {printf(" 析构一个 MyException 对象 /n");}
};
class A
{
public:
A() {printf(" 构造一个 A 对象 /n");}
~A() {printf(" 析构一个 A 对象 /n");}
void f1() {}
// 注意,这里抛出了一个 MyException 类型的异常对象
void f2() {MyException e; throw e;}
};
// 这个函数中使用了 try-catch 处理异常,也即 C++ 异常处理
void test1()
{
A a1;
A a2,a3;
try
{
a2.f1();
a3.f2();
}
// 这里虽然有 catch 块,但是它捕获不到上面抛出的 C++ 异常对象
catch(int errorcode)
{
printf("catch exception,error code:%d/n", errorcode);
}
}
// 这个函数没什么改变,仍然采用 try-except 异常机制,也即 SEH 机制
void test()
{
int* p = 0x00000000; // pointer to NULL
__try
{
// 这里调用 test1 函数
// 注意, test1 函数中会抛出一个 C++ 异常对象
test1();
puts("in try");
__try
{
puts("in try");
*p = 13;
puts(" 这里不会被执行到 ");
}
__finally
{
puts("in finally");
}
puts(" 这里也不会被执行到 ");
}
__except(puts("in filter 1"), 0)
{
puts("in except 1");
}
}
void main()
{
puts("hello");
__try
{
test();
}
// 这里能捕获到 C++ 异常对象吗?拭目以待吧!
__except(puts("in filter 2"), 1)
{
puts("in except 2");
}
puts("world");
}
仔细阅读上面的程序,不难看出, SEH 与 C++ 异常模型两种机制确实真正地交互起来了,上层的 main() 函数和 test() 函数采用 try-except 语句处理异常,而下层的 test1() 函数采用标准的 try-catch 语句处理异常,并且,下层的 test1() 函数所抛出的 C++ 异常会被上层的 try-except 所捕获到吗?还是看运行结果吧! 如下:
hello
构造一个 A 对象
构造一个 A 对象
构造一个 A 对象
构造一个 MyException 对象
复制一个 MyException 对象
in filter 1
in filter 2
析构一个 MyException 对象
析构一个 A 对象
析构一个 A 对象
析构一个 A 对象
in except 2
world
Press any key to continue
结果是否和朋友们的预期一致呢?它的的确确是上层的 try-except 块,能够捕获到下层的 test1() 函数所抛出的 C++ 异常,而且流程还是正确的,即符合了 SEH 异常模型的规则,又同时遵循了 C++ 异常模型的规则。同时,最难能可贵的是,在 test1() 函数中的三个局部变量,都被正确的析构了(这非常神奇吧!具体的机制这里暂且不详细论述了,在后面阐述“异常机制的实现”的文章中再做论述)。
细心的程序员朋友们,也许从上面程序的运行结果中发现了一些“问题”,什么呢?那就是“ MyException对象 ”构造了两次,但它只被析构了一次。呵呵!这也许就是 MSDN 中不建议混合使用这两种异常处理机制的背后原因之一吧!虽然说,这种问题不至于对整个程序造成很大的破坏性影响,但主人公阿愚却坚持认为,如果我们编程时滥用 try-except 和 try-catch 在一起混用,不仅使我们程序的整体结构和语义受到影响,而且也会造成一定的内存资源泄漏,甚至其它的不稳定因素。
总之,在 C++ 程序中运用 try-except 机制,只有在顶层的函数作用域中(例如,系统运行库中,或 plugin 的钩子中)才有必要这样做。如在 VC 编写的程序中,每当我们程序运行中出现意外异常导致的崩溃事件时,系统总能够弹出一个“应用程序错误”框,如下:
C++ 异常处理模型能捕获 SEH 异常吗?
呵呵!阿愚笑了,这还用问吗?当然了, VC 提供的 C++ 异常处理模型的强大之处就是,它不仅能捕获 C++ 类型的异常,而且它还能捕获属于系统级别的 SEH 异常。它就是利用了 catch(…) 语法,在前面专门阐述 catch(…) 语法时,我们也着重论述了这一点。不过,这里还是给出一个实际的例子吧,代码如下:
class MyException
{
public:
MyException() {printf(" 构造一个 MyException 对象 /n");}
MyException(const MyException& e) {printf(" 复制一个 MyException 对象 /n");}
operator=(const MyException& e) {printf(" 复制一个 MyException 对象 /n");}
~MyException() {printf(" 析构一个 MyException 对象 /n");}
};
class A
{
public:
A() {printf(" 构造一个 A 对象 /n");}
~A() {printf(" 析构一个 A 对象 /n");}
void f1() {}
// 抛出 C++ 异常
void f2() {MyException e; throw e;}
};
void test()
{
int* p = 0x00000000; // pointer to NULL
__try
{
puts("in try");
__try
{
puts("in try");
// causes an access violation exception;
// 导致一个存储异常
*p = 13;
// 呵呵,注意这条语句
puts(" 这里不会被执行到 ");
}
__finally
{
puts("in finally");
}
// 呵呵,注意这条语句
puts(" 这里也不会被执行到 ");
}
__except(puts("in filter 1"), 0)
{
puts("in except 1");
}
}
void test1()
{
A a1;
A a2,a3;
try
{
// 这里会产生一个 SEH 类型的系统异常
test();
a2.f1();
a3.f2();
}
// 捕获得到吗?
catch(...)
{
printf("catch unknown exception/n");
}
}
void main()
{
puts("hello");
__try
{
test1();
}
__except(puts("in filter 2"), 0)
{
puts("in except 2");
}
puts("world");
}
上面的程序很简单的,无须进一步讨论了。当然,其实我们还可以更进一步深入进去,因为 C++ 异常处理模型不仅能够正常捕获到 SEH 类型的系统异常,而且它还能够把 SEH 类型的系统异常转化为 C++ 类型的异常。我想,这应该放在单独的一篇文章中来阐述了,其实这在许多关于 Window 系统编程的书籍中也有详细讨论。
SEH 与 C++ 异常模型在混合使用时的“禁区”
刚才我们看到,利用 try-except 来捕获 C++ 异常有点小问题,但这毕竟算不上什么禁区。那么,何为 SEH 与 C++ 异常模型在混合使用时的“禁区”呢?看个例子吧,代码如下:
// 注意,这是 C++ 程序,文件名为: SEH-test.cpp
#include "stdio.h"
void main()
{
int* p = 0x00000000; // pointer to NULL
// 这里是 SEH 的异常处理语法
__try
{
puts("in try");
// 这里是 C++ 的异常处理语法
try
{
puts("in try");
// causes an access violation exception;
// 导致一个存储异常
*p = 13;
// 呵呵,注意这条语句
puts(" 这里不会被执行到 ");
}
catch(...)
{
puts("catch anything");
}
// 呵呵,注意这条语句
puts(" 这里也不会被执行到 ");
}
__except(puts("in filter 1"), 1)
{
puts("in except 1");
}
}
朋友们!不要急于编译并测试上面的小程序,先猜猜它会有什么结果呢?想到了吗?不妨实践一下,呵呵!实际结果是否令你吃惊呢?对了,没错, VC 就是会报出一个编译错误(“ error C2713: Only one form of exception handling permitted per function ”)。那么原因何在呢?主人公阿愚在此一定“知无不言,言无不尽”,这是因为: VC 实现的异常处理机制,不管是 try-except 模型,还是 try-catch 模型,它们都是以函数作为一个最基本“分析和控制”的目标,也即,如果一个函数内使用了异常处理机制, VC 编译器在编译该函数时,它会给此函数插入一些“代码和信息”(代码指的是当该函数中出现异常时的回调函数,而信息主要是指与异常出现相关的一些必要的链表),因此每份函数只能有一份这样的东东(“代码和信息”),故一个函数只能采用一种形式的异常处理规则。 朋友们!恍然大悟了吧!其实这倒不算最令人不可思议的。还有一种更为特殊的情况,看下面的例子,代码如下:
class A
{
public:
A() {printf(" 构造一个 A 对象 /n");}
~A() {printf(" 析构一个 A 对象 /n");}
void f1() {}
void f2() {}
};
void main()
{
__try
{
A a1, a2;
puts("in try");
}
__except(puts("in filter 1"), 1)
{
puts("in except 1");
}
}
其实上面的程序表面上看,好像是没有什么特别的吗?朋友们!仔细看看,真的没什么特别的吗?不妨编译一下该程序,又奇怪了吧!是的,它同样也编译报错了,这是我机器上编译时产生的信息,如下:
--------------------Configuration: exception - Win32 Debug--------------------
Compiling...
seh-test.cpp
f:/exception/seh-test.cpp(214) : warning C4509: nonstandard extension used: 'main' uses SEH and 'a2' has destructor
f:/exception/seh-test.cpp(211) : see declaration of 'a2'
f:/exception/seh-test.cpp(214) : warning C4509: nonstandard extension used: 'main' uses SEH and 'a1' has destructor
f:/exception/seh-test.cpp(211) : see declaration of 'a1'
f:/exception/seh-test.cpp(219) : error C2712: Cannot use __try in functions that require object unwinding
Error executing cl.exe.
Creating browse info file...
exception.exe - 1 error(s), 2 warning(s)
那么,上面的错误信息代表什么意思,我想是不是有不少朋友都遇到过这种莫名奇妙的编译问题。其实,这确实很令人费解,明明程序很简单的吗?而且程序中只用到了 SEH 异常模型的 try-except 语法,甚至 SEH 与 C++ 异常模型两者混合使用的情况都不存在。那么编译出错的原因究竟何在呢?实话告诉你吧!其实主人公阿愚在此问题上也是费透了脑筋,经过了一番深入而细致的钻研之后,才知道真正原因的。
那原因就是: 同样还是由于在一个函数不能采用两种形式的异常处理机制而导致的编译错误。 啊!这岂不是更迷惑了。其实不然,说穿了,朋友们就会明白了,这是因为: 在 C++ 异常处理模型中,为了能够在异常发生后,保证正确地释放相关的局部变量(也即调用析构函数),它必须要跟踪每一个“对象”的创建过程,这种由于异常产生而导致的对象析构的过程,被称为“ unwind ”(记得前面的内容中,也多次讲述到了这个名词),因此,如果一个函数中有局部对象的存在,那么它就一定会存在 C++ 的异常处理机制(也即会给此函数插入一些用于 C++ 异常处理“代码和信息”),这样,如果该函数中在再使用 try-except 机制,岂不是就冲突了吗?所以编译器也就报错了,因为它处理不了了! 哈哈!朋友们,主人公阿愚把问题说清楚了吗?
总结
• SEH 与 C++ 异常模型,可以在一起被混合使用。但最好听从 MSDN 的建议:在 C 程序中使用 try-except 和 try-finally ;而 C++ 程序则应该使用 try-catch 。
• 混合使用时, C++ 异常模型可以捕获 SEH 异常;而 SEH 异常模型也可以捕获 C++ 类型的异常。而后者通常有点小问题,它一般主要运用在提高和保证产品的可靠性上(也即在顶层函数中使用 try-except 语句来 catch 任何异常)
• VC 实现的异常处理机制中,不管是 try-except 模型,还是 try-catch 模型,它们都是以函数作为一个最基本“分析和控制”的目标,也即一个函数中只能采用一种形式的异常处理规则。否则,编译这一关就会被“卡壳”。
下一篇文章中,主人公阿愚打算接着详细讨论一些关于 C++ 异常处理模型的高级使用技巧,也即 “ 如何把 SEH 类型的系统异常转化为 C++ 类型的异常? ”, 程序员朋友们,继续吧!
-------------------------------------------------------------------------------------------------------------------------------------------------------
如何把SEH类型的系统异常转化为C++类型的异常?
在上一篇文章中,详细讨论了“ SEH 与 C++ 异常模型的混合使用”,这一篇文章中,主人公阿愚仍将这一主题继续深入,那就是“如何把 SEH 类型的系统异常转化为 C++ 类型的异常?”(其实,这本质上仍然属于 SEH 与 C++ 异常模型的混合使用,也即 C++ 异常模型来捕获 SEH 系统类型的异常)。
为什么要 把 SEH 类型的系统异常转化为 C++ 类型的异常?
做一件事情之前,我们最好要搞清为什么!“十万个为什么” 可曾造就了多少顶级奇才!呵呵! WHY? ? WHY ? WHY ?这对任何一个人来说,都绝对是个好习惯,阿愚同学就一直把这个当“宝贝”。那么,究竟 为什么要 把 SEH 类型的系统异常转化为 C++ 类型的异常?朋友们,大家都想想,整理整理自己的意见和想法。这里,阿愚给出它个人的理解,如下:
• 首先是由于我们在编程时,仍然最好遵循 MSDN 给出的建议和忠告(“ C 程序中使用 try-except 和 try-finally ;而 C++ 程序则应该使用 try-catch ”)。但是,为了提高程序的可靠性(防止意外系统异常的出现,而导致的程序无规则崩溃现象),我们还必须要求在 C++ 程序中,使用 try-catch 模型 能够捕获并 处理系统异常, 这在第 3 集的文章中曾详细讨论过了它, 那就是它只能采用 catch(…) 语法来捕获 SEH 类型的系统异常。 catch(…) 的使用 虽然一定程度上提高了程序的可靠性,但是,“异常发生时的许多相关信息”它却什么也没有提供给程序员(包括何种类型的系统异常,出现的地点,以及其它有关异常的信息等等)。因此,我们需要一种有效途径,来把 SEH 类型的系统异常转化为 C++ 类型的异常?这无疑也就提供了一种在 C++ 异常处理模型中,不仅能够处理系统异常,而且还能够获取有关 SEH 类型系统异常中的许多详细信息的手段。
• 其次就是,阿愚多次阐述过, C++ 异常处理是和面向对象紧密联系的(它二位可是“哥俩好”),因此,如果把 SEH 类型的系统异常统一到面向对象体系结构设计的“异常分类”中去,那对程序员而言,岂不是妙哉!美哉!该解决方案真所谓是,即提高了可靠性;又不失优雅!
如何实现 把 SEH 类型的系统异常转化为 C++ 类型的异常?
虽然说把 SEH 类型的系统异常转化为 C++ 类型的异常,给 C++ 程序员带来的是好处多多,但是实现起来并不复杂,因为系统底层和 VC 运行库已经为我们铺路搭桥了,也即我们可以通过 VC 运行库中的“ _ set_se_translator ”函数来轻松搞定它。 MSDN 中对它解释如下:
Handles Win32 exceptions (C structured exceptions) as C++ typed exceptions.
typedef void (*_se_translator_function)( unsigned int, struct _EXCEPTION_POINTERS* );
_se_translator_function _set_se_translator( _se_translator_function se_trans_func );
The _set_se_translator function provides a way to handle Win32 exceptions (C structured exceptions) as C++ typed exceptions. To allow each C exception to be handled by a C++ catch handler, first define a C exception “wrapper” class that can be used, or derived from, in order to attribute a specific class type to a C exception. To use this class, install a custom C exception translator function that is called by the internal exception-handling mechanism each time a C exception is raised. Within your translator function, you can throw any typed exception that can be caught by a matching C++ catch handler.
To specify a custom translation function, call _set_se_translator with the name of your translation function as its argument. The translator function that you write is called once for each function invocation on the stack that has try blocks. There is no default translator function.
In a multithreaded environment, translator functions are maintained separately for each thread. Each new thread needs to install its own translator function. Thus, each thread is in charge of its own translation handling.
The se_trans_func function that you write must take an unsigned integer and a pointer to a Win32 _EXCEPTION_POINTERS structure as arguments. The arguments are the return values of calls to the Win32 API GetExceptionCode and GetExceptionInformation functions, respectively.
至于 _ set_se_translator 函数的具体机制,以及 把系统异常转化为 C++ 类型的异常的原理这里不再详细讨论,而仅仅是给出了大致的工作流程: 首先,通过 _ set_se_translator 函数设置一个对所有的 Windows 系统异常产生作用的回调处理函数(也是与 TLS 数据有关);因此,每当程序运行时产生了系统异常之后,前面我们设置的自定义回调函数于是便会接受程序的控制权;接着,我们在该函数的实现中,可以根据不同类型的系统异常( EXCEPTION_POINTERS ),来分别抛出一个 C++ 类型的异常错误 。呵呵!简单吧!不再白话了,还是来瞧瞧阿愚所设计的一个简单演示例程吧!代码如下:
// FILENAME:SEH-test.cpp
#include <windows.h>
#include <cstdio>
#include <eh.h>
#include <string>
#include <exception>
using namespace std;
class seh_exception_base : public std::exception
{
public:
seh_exception_base (const PEXCEPTION_POINTERS pExp, std::string what )
: m_ExceptionRecord(*pExp->ExceptionRecord),
m_ContextRecord(*pExp->ContextRecord),
m_what(what){};
~seh_exception_base() throw(){} ;
virtual const char* what() const throw()
{
return m_what.c_str();
}
virtual DWORD exception_code() const throw()
{
return m_ExceptionRecord.ExceptionCode;
}
virtual const EXCEPTION_RECORD& get_exception_record() const throw()
{
return m_ExceptionRecord;
}
virtual const CONTEXT& get_context() const throw()
{
return m_ContextRecord;
}
// 初始化函数
static void initialize_seh_trans_to_ce()
{
_set_se_translator( trans_func );
}
// 系统异常出现时的回调函数
static void trans_func( unsigned int u, EXCEPTION_POINTERS* pExp );
protected:
std::string m_what;
EXCEPTION_RECORD m_ExceptionRecord;
CONTEXT m_ContextRecord;
};
// 下面是系统异常被转换后的 C++ 类型的异常
// 篇幅有限,因此只简单设计了对几种常见的系统异常的转换
class seh_exception_access_violation : public seh_exception_base
{
public:
seh_exception_access_violation (const PEXCEPTION_POINTERS pExp, std::string what)
: seh_exception_base(pExp, what) {};
~seh_exception_access_violation() throw(){};
};
class seh_exception_divide_by_zero : public seh_exception_base
{
public:
seh_exception_divide_by_zero (const PEXCEPTION_POINTERS pExp, std::string what)
: seh_exception_base(pExp, what) {};
~seh_exception_divide_by_zero() throw(){};
};
class seh_exception_invalid_handle : public seh_exception_base
{
public:
seh_exception_invalid_handle (const PEXCEPTION_POINTERS pExp, std::string what)
: seh_exception_base(pExp, what) {};
~seh_exception_invalid_handle() throw(){};
};
// 系统异常出现时的回调函数
// 这里是实现,很关键。针对不同的异常,抛出一个 C++ 类型的异常
void seh_exception_base::trans_func( unsigned int u, EXCEPTION_POINTERS* pExp )
{
switch(pExp->ExceptionRecord->ExceptionCode)
{
case EXCEPTION_ACCESS_VIOLATION :
throw seh_exception_access_violation(pExp, " 存储保护异常 ");
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO :
throw seh_exception_divide_by_zero(pExp, " 被 0 除异常 ");
break;
case EXCEPTION_INVALID_HANDLE :
throw seh_exception_invalid_handle(pExp, " 无效句病异常 ");
break;
default :
throw seh_exception_base(pExp, " 其它 SEH 异常 ");
break;
}
}
// 来测试吧!
void main( void )
{
seh_exception_base::initialize_seh_trans_to_ce();
try
{
// 被 0 除
int x, y=0;
x = 5 / y;
// 存储保护
char* p =0;
*p = 0;
// 其它系统异常,例如中断异常
__asm int 3;
}
catch( seh_exception_access_violation& e )
{
printf( "Caught SEH_Exception. 错误原因: %s\n", e.what());
//other processing
}
catch( seh_exception_divide_by_zero& e )
{
printf( "Caught SEH_Exception. 错误原因: %s\n", e.what());
//other processing
}
catch( seh_exception_base& e )
{
printf( "Caught SEH_Exception. 错误原因: %s, 错误代码: %x\n", e.what(), e.exception_code());
}
}
总结
至此,关于 C++ 异常处理模型、 C 语言中的各种异常处理机制, Window 系统下的 SEH 原理,以及它们在使用上的各种技巧,和程序员在编程时使用这些异常处理机制时的注意事项等等,前面的相关文章中都做了较系统和深入的讨论和分析(也许错误多多!欢迎指出和讨论)。相信不仅是主人公阿愚,而且包括那些细细地读过、品味过,和阿愚一块走过来的人,现在都有了一种豁然开朗的感觉。感谢各种异常处理机制!感谢那些设计如此精妙技术和作品的天才!感谢天下所有有作为的程序员!
Java 语言中的异常处理模型是踩着 C++ 异常处理模型肩膀上过来的。它继承了 C++ 异常处理模型的风格和优点,同时,它也比 C++ 异常处理模型更安全,更可高,更强大和更丰富。下一篇文章中,主人公阿愚打算乘胜追击,来讨论一些关于 Java 语言中的异常处理模型。感兴趣的 朋友们,继续吧!
----------------------------------------------------------------------------------------------------------------------------------
Java语言中的异常处理模型
对于一个非常熟悉 C++ 异常处理模型的程序员来说,它几乎可以不经任何其它培训和学习,就可以完全接受和能够轻松地使用 Java 语言中的异常处理编程方法。这是因为 Java 语言中的异常处理模型几乎与 C++ 中异常处理模型有 99% 的相似度,无论是从语法规则,还是语义上来说,它们二者都几乎完全一致。
当然,如果你对 Java 语言中的异常处理模型有更多,或更深入的了解,你还是能够发现 Java 异常处理模型与 C++ 中异常处理模型还是存在不少差别的。是的, Java 语言本来就是 C++ 语言的完善改进版,所以, Java 语言中的异常处理模型也就必然会继承了 C++ 异常处理模型的风格和优点。但是,好的东西不仅仅是需要继承优点,更重要的是需要“去其糟粕,取其精华”,需要发展!!!毫无疑问, Java 语言中的异常处理模型完全达到了这一“发展”高度。它比 C++ 异常处理模型更安全,更可高,更强大和更丰富。
下面的内容中,阿愚不打算再向大家详细介绍一遍有关 Java 异常处理编程的具体语法和规则。因为这与 C++ 中的异常处理几乎完全一样,而且这些基础知识在太多太多的有关 java 编程的书籍中都有详细阐述。而阿愚认为: 这里更需要的是总结,需要的是比较,需要的是重点突出,需要的是关键之处 。所以,下面着重把 Java 语言中的异常处理模型与 C++ 异常处理模型展开比较,让我们透彻分析它到底有何发展?有何优势?与 C++ 异常处理模型到底有哪些细节上的不同?又为什么要这样做?
借鉴并引进了 SEH 异常模型中的 try-finally 语法
要说 Java 异常处理模型与 C++ 中异常处理模型的最大不同之处,那就是在 Java 异常处理模型中引入了 try-finally 语法,阿愚认为这是从微软的 SEH 借鉴而来。在前面的一些文章中,详细而深入阐述 SEH 异常处理模型的时候,我们从中获知, SEH 主要是为 C 语言而设计的,便于第三厂商开发的 Window 驱动程序有更好更高的安全保障。同时, SEH 异常处理模型中除了 try-except 来用于处理异常外,还有一个 try-finally 语法,它主要用来清除一些曾经分配的资源(由于异常出现,而导致这些资源不能够按正常的顺序被释放,还记得吗?这被称为“ UNWIND ”), try-finally 本质上有点类似于面向对象编程中的析构函数的作用,由于这项机制的存在,才导致 SEH 的强大和风光无比。
现在, Java 异常处理模型也吸收了这项设计。可我们知道,无论是 JAVA 中,还是 C++ 中,它们都有“析构函数”呀!它们完全可以利用面向的析构函数来自动释放资源呀!是的,没错!理论上是这样的。可是在实践中,我们也许会发现或经常碰到,仅仅利用析构函数来释放资源,并不是那么好使,例如,我们经常需要动态得从堆上分配的对象,这时,释放对象必须要显式地调用 delete 函数来触发该对象的析构函数的执行。如果这期间发生了异常,不仅该对象在堆中的内存不能达到被释放的结果,而且,该对象的析构函数中释放更多资源的一些代码也不能得以执行。因此这种后果非常严重,这也算 C++ 异常处理模型中一种比较大的缺陷吧!(虽然, C++ 有其它补救措施,那就是利用“智能指针”来保证一些对象的及时而有效地被释放)。另外,其实很有许多类似的例子,例如关闭一个内核句柄等操作( CloseHandle )。
在 Java 语言中,由于它由于采用了垃圾回收技术,所以它能有效避免上述所讲的类似于 C++ 语言中的由于异常出现所导致的那种资源不能得以正确释放的尴尬局面。但是,它仍然还是在它的异常处理模型中引入了 try-finally 语法,因为,无论如何,它至少会给程序员带来了极大的方便,例如如下的程序片断,就可以充分反映出 try-finally 对提高我们代码的质量和美观是多么的重要。
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
BufferedReader rd=null;
Writer wr=null;
try
{
File srcFile = new File((args[0]));
File dstFile = new File((args[1]));
rd = new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), args[2]));
wr = new OutputStreamWriter(new FileOutputStream(dstFile), args[3]);
while(true)
{
String sLine = rd.readLine();
if(sLine == null) break;
wr.write(sLine);
wr.write("\r\n");
}
}
finally
{
// 这里能保证在何种情况下,文件流的句柄都得以被正确关闭
// 该方法主要用于清理非内存性质的资源(垃圾回收机制无法
// 处理的资源,如数据库连接、 Socket 关闭、文件关闭等等)。
wr.flush();
wr.close();
rd.close();
}
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
}
所有的异常都必须从 Throwable 继承而来
在 C++ 异常处理模型中,它给予程序员最大的自由度和发挥空间(这与 C++ 中所主导的设计思想是一致的),例如它允许程序员可以抛出任何它想要的异常对象,它可以是语言系统中原本所提供的各种简单数据类型(如 int 、 float 、 double 等),也可以是用户自定义的抽象数据对象(如 class 的 object 实例)。虽然说,无论从何个角度上考量,我们都应该把异常处理与面向对象紧密结合起来(采用带有继承特点的层次化的类对象结构来描述系统中的各种异常),但是 C++ 语言规范中并无此约束;况且,即便大家都不约而同地采用面向对象的方法来描述“异常”,但也会由于各个子系统(基础运行库)不是一个厂商(某一个人)所统一设计,所以导致每个子系统所设计出的异常对象系统彼此相差甚远。这给最终使用(重用)这些库的程序员带来了很大的不一致性,甚至是很大的麻烦,我们不仅需要花费很多很宝贵的时间来学习和熟悉这些不同的异常对象子系统;而且更大的问题是,这些不同子系统之间语义上的不一致,而造成程序员在最终处理这些异常时,将很难把它们统一起来,例如, MFC 库系统中,采用 CMemoryException 来表示一个与内存操作相关的异常;而其它的库系统中很可能就会采用另外一个 class 来表示内存操作的异常错误。本质上说,这是由于缺乏规范和统一所造成的恶劣后果,所以说,如果在语言设计的时候,就充分考虑到这些问题,把它们纳入语言的统一规范之中,这对广大的程序员来说,无疑是个天大的好事情。
Java 语言毫无疑问很好地做到了这一点,它要求 java 程序中(无论是谁写的代码),所有抛出( throw )的异常都必须是从 Throwable 派生而来,例如,下面的代码编译时就无法通过。
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
BufferedReader rd=null;
Writer wr=null;
try
{
File srcFile = new File((args[0]));
File dstFile = new File((args[1]));
rd = new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), args[2]));
wr = new OutputStreamWriter(new FileOutputStream(dstFile), args[3]);
// 编译时,这里将被报错
// 而正确的做法可以是: throw new Exception("error! test!");
if (rd == null || wr == null) throw new String("error! test!");
while(true)
{
String sLine = rd.readLine();
if(sLine == null) break;
wr.write(sLine);
wr.write("\r\n");
}
}
finally
{
wr.flush();
wr.close();
rd.close();
}
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
}
编译时,输出的错误信息如下:
E:\work\\Trans.java:20: incompatible types
found : java.lang.String
required: java.lang.Throwable
if (rd == null || wr == null) throw new String("error! test!");
1 error
当然,实际的 Java 编程中,由于 JDK 平台已经为我们设计好了非常丰富和完整的异常对象分类模型。因此, java 程序员一般是不需要再重新定义自己的异常对象。而且即便是需要扩展自定义的异常对象,也往往会从 Exception 派生而来。所以,对于 java 程序员而言,它一般只需要在它的顶级函数中 catch(Exception ex) 就可以捕获出所有的异常对象,而不必像 C++ 中采用 catch(…) 那样不伦不类,但又无可奈何的语法。因此,在 java 中也就不需要(也没有了) catch(…) 这样的语法。
至于 JDK 平台中的具体的异常对象分类模型,主人公阿愚打算放在另外单独的一篇文章中详细讨论,这里只简单的概括一下: 所有异常对象的根基类是 Throwable , Throwable 从 Object 直接继承而来(这是 java 系统所强制要求的),并且它实现了 Serializable 接口(这为所有的异常对象都能够轻松跨越 Java 组件系统做好了最充分的物质准备)。从 Throwable 直接派生出的异常类有 Exception 和 Error 。 Exception 是 java 程序员所最熟悉的,它一般代表了真正实际意义上的异常对象的根基类。也即是说, Exception 和从它派生而来的所有异常都是应用程序能够 catch 到的,并且可以进行异常错误恢复处理的异常类型。而 Error 则表示 Java 系统中出现了一个非常严重的异常错误,并且这个错误可能是应用程序所不能恢复的,例如 LinkageError ,或 ThreadDeath 等。
对异常处理的管理更严格,也更严谨!
同样还是与 C++ 异常处理模型作比较,在 Java 系统中,它对异常处理的管理更严格,也更严谨!为什么这么说呢?下面请听阿愚娓娓道来!
首先还是看一个例子吧!代码如下:
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
BufferedReader rd=null;
Writer wr=null;
try
{
File srcFile = new File((args[0]));
File dstFile = new File((args[1]));
rd = new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), args[2]));
wr = new OutputStreamWriter(new FileOutputStream(dstFile), args[3]);
// 注意下面这条语句,它有什么问题吗?
if (rd == null || wr == null) throw new Exception("error! test!");
while(true)
{
String sLine = rd.readLine();
if(sLine == null) break;
wr.write(sLine);
wr.write("\r\n");
}
}
finally
{
wr.flush();
wr.close();
rd.close();
}
}
catch(IOException ex)
{
ex.printStackTrace();
}
}
}
熟悉 java 语言的程序员朋友们,你们认为上面的程序有什么问题吗?编译能通过吗?如果不能,那么原因又是为何呢?好了,有了自己的分析和预期之后,不妨亲自动手编译一下上面的小程序,呵呵!结果确实如您所料?是的,的确是编译时报错了,错误信息如下:
E:\Trans.java:20: unreported exception java.lang.Exception; must be caught or declared to be thrown
if (rd == null || wr == null) throw new Exception("error! test!");
1 error
上面这种编译错误信息,相信 Java 程序员肯定见过(可能还是屡见不鲜!),而且,这也是许多从 C++ 程序员刚刚转来写 Java 程序不久(还没有真正入行)时,最感到迷糊和不解的地方,“俺代码明明没什么问题吗?可为什么编译有问题呢”!
呵呵!阿愚相信老练一些的 Java 程序员一定非常清楚上述编译出错的原因。那就是如错误信息中(“ must be caught ”)描述的那样, 在 Java 的异常处理模型中,要求所有被抛出的异常都必须要有对应的“异常处理模块” 。也即是说,如果你在程序中 throw 出一个异常,那么在你的程序中(函数中)就必须要 catch 这个异常(处理这个异常)。例如上面的例子中,你在第 20 行代码处,抛出了一个 Exception 类型的异常,但是在该函数中,却没有 catch 并处理掉此异常的地方。因此,这样的程序即便是能够编译通过,那么运行时也是致命的(可能导致程序的崩溃),所以, Java 语言干脆在编译时就尽可能地检查(并卡住)这种本不应该出现的错误,这无疑对提高程序的可靠性大有帮助。相反, C++ 中则不是这样,它把更多的事情和责任交给 C++ 程序员去完成,它总相信 C++ 程序员是最优秀的,会自己处理好这一切的所有事情。如果程序中真的出现了未被捕获的异常,系统它(实际上是 C++ 运行库中)就认为这是出现了一个致命的、不可恢复的错误,接着调用 terminate 函数终止该进程( NT 系统中稍微负责任一些,还弹出个“系统错误”对华框,并且 report 出一些与异常相关的错误信息)。
从上面的分析可以看出, Java 异常处理模型的确比 C++ 异常处理模型更严谨和更安全。实际上,这还体现在更多的方面,例如,在 C++ 异常处理模型中,异常的声明已经可以成为声明函数接口的一部分,但这并非语言所强求的。虽然许多优秀的库系统在设计时,都把这当成了一种必须的约定,甚至是编程规范,例如 MFC 中,就有许多可能抛出异常的函数,都显式地做了声明,如 CFile::Read 函数,声明如下:
virtual UINT Read( void* lpBuf , UINT nCount );
throw( CFileException );
但是,在 Java 语言中,这就是必须的。 如果一个函数中,它运行时可能会向上层调用者函数抛出一个异常,那么,它就必须在该函数的声明中显式的注明(采用 throws 关键字,语法与 C++ 类似) 。还记得刚才那条编译错误信息吗?“ must be caught or declared to be thrown ”,其中“ must be caught ”上面已经解释了,而后半部分呢?“ declared to be thrown ”是指何意呢?其实指的就是“必须显式地声明某个函数可能会向外部抛出一个异常”,也即是说,如果一个函数内部,它可能抛出了一种类型的异常,但该函数内部并不想(或不宜) catch 并处理这种类型的异常,此时,它就必须( 注意,这是必须的!强求的!而 C++ 中则不是必须的 )使用 throws 关键字来声明该函数可能会向外部抛出一个异常,以便于该函数的调用者知晓并能够及时处理这种类型的异常。下面列出了这几种情况的比较,代码如下:
// 示例程序 1 ,这种写法能够编译通过
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
test();
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
static void test()
{
try
{
throw new Exception("test");
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
}
// 示例程序 2 ,这种写法就不能够编译通过
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
test();
}
// 虽然这里能够捕获到 Exception 类型的异常
catch(Exception ex)
{
ex.printStackTrace();
}
}
static void test()
{
throw new Exception("test");
}
}
// 示例程序 3 ,这种写法又能够被编译通过
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
test();
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
// 由于函数声明了可能抛出 Exception 类型的异常
static void test() throws Exception
{
throw new Exception("test");
}
}
// 示例程序 4 ,它又不能够被编译通过了
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
// 虽然 test() 函数并没有真正抛出一个 Exception 类型的异常
// 但是由于它函数声明时,表示它可能抛出一个 Exception 类型的异常
// 所以,这里仍然不能被编译通过。
// 呵呵!体会到了 Java 异常处理模型的严谨吧!
test();
}
catch(IOException ex)
{
ex.printStackTrace();
}
}
static void test() throws Exception
{
}
}
不知上面几个有联系的示例是否能够给大家带来“豁然开朗”的感觉,坦率的说, Java 提供的异常处理模型并不复杂,相信太多太多 Java 程序员有着比阿愚更深刻的认识。也许,阿愚对于这些简单而琐碎的事情如此婆婆妈妈絮叨个没完,有点招人笑话了!但是,阿愚还是坚持认为,非常有必要把这些简单但非常管用的准则阐述透彻。最后,补充一种例外情况,请看如下代码:
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
test();
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
static void test() throws Error
{
throw new Error(" 故意抛出一个 Error");
}
}
朋友们!上面的程序能被编译通过吗?注意,按照刚才上面所总结出的规律:在 Java 的异常处理模型中,要求所有被抛出的异常都必须要有对应的 catch 块!那么上面的程序肯定不能被编译通过,因为 Error 和 Exception 都是从 Throwable 直接派生而来,而 test 函数声明了它可能抛出 Error 类型的异常,但在 main 函数中却并没有 catch(Error) 或 catch(Throwable) 块,所以它理当是会编译出错的!真的吗?不妨试试!呵呵!结果并非我们之预料,而它恰恰是正确编译通过了。为何? WHY ? WHY ?
其实,原因很简单,那就是因为 Error 异常的特殊性。 Java 异常处理模型中规定: Error 和从它派生而来的所有异常,都表示系统中出现了一个非常严重的异常错误,并且这个错误可能是应用程序所不能恢复的 (其实这在前面的内容中已提到过)。因此,如果系统中真的出现了一个 Error 类型的异常,那么则表明,系统已处于崩溃不可恢复的状态中,此时,作为编写 Java 应用程序的你,已经是没有必要(也没能力)来处理此等异常错误。所以, javac 编译器就没有必要来保证:“在编译时,所有的 Error 异常都有其对应的错误处理模块”。当然, Error 类型的异常一般都是由系统遇到致命的错误时所抛出的,它最后也由 Java 虚拟机所处理。而作为 Java 程序员的你,可能永远也不会考虑抛出一个 Error 类型的异常。因此 Error 是一个特例情况!
特别关注一下 RuntimeException
上面刚刚讨论了一下 Error 类型的异常处理情况, Java 程序员一般无须关注它(处理这种异常)。另外,其实在 Exception 类型的异常对象中,也存在一种比较特别的“异常”类型,那就是 RuntimeException ,虽然它是直接从 Exception 派生而来,但是 Java 编译器( javac )对 RuntimeException 却是特殊待遇,而且是照顾有加。不信,看看下面的两个示例吧!代码如下:
// 示例程序 1
// 它不能编译通过,我们可以理解
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
test();
}
static void test()
{
// 注意这条语句
throw new Exception(" 故意抛出一个 Exception");
}
}
// 示例程序 2
// 可它却为什么能够编译通过呢?
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
test();
}
static void test()
{
// 注意这条语句
throw new RuntimeException(" 故意抛出一个 RuntimeException");
}
}
对上面两个相当类似的程序, javac 编译时却遭遇了两种截然不同的处理,按理说,第 2 个示例程序也应该像第 1 个示例程序那样,编译时报错!但是 javac 编译它时,却例外地让它通过它,而且在运行时, java 虚拟机也捕获到了这个异常,并且会在 console 打印出详细的异常信息。运行结果如下:
java.lang.RuntimeException: 故意抛出一个 RuntimeException
at Trans.test(Trans.java:13)
at Trans.main(Trans.java:8)
Exception in thread "main"
为什么对于 RuntimeException 类型的异常(以及从它派生而出的异常类型), javac 和 java 虚拟机都特殊处理呢?要知道,这可是与“ Java 异常处理模型更严谨和更安全”的设计原则相抵触的呀!究竟是为何呢?这简直让人不法理解呀!主人公阿愚劝大家稍安勿躁,我们不妨换个角度,还是从 C++ 异常处理模型展开对比和分析,也许这能使我们茅塞顿开!
在 VC 实现的 C++ 异常处理中(基于 SEH 机制之上),一般有两类异常。其一,就是通过 throw 语句,程序员在代码中人为抛出的异常(由于运行时动态地监测到了一个错误);另外一个呢?就是系统异常,也即前面我们一直重点讨论的 SEH 异常,例如,“被 0 除”,“段存储保护异常”,“无效句柄” 等,这类异常实际上程序员完全可以做得到避免它(只要我们写代码时足够小心,足够严谨,使得写出的代码足够安全可靠),但实际上,这也许无法完全做到,太理想化了。因此,为了彻底解决这种隐患,提高程序整体可靠性(可以容忍程序员留下一些 BUG ,至少不至于因为编码时考虑不周,或一个小疏忽留下的一个小 BUG ,而导致整个应用系统在运行时崩溃!), VC 提供的 C++ 异常处理中,它就能够捕获到 SEH 类型的系统异常。 这类异常在被处理时,实际上它首先会被操作系统的中断系统所接管(也即应用程序出现这类异常时,会导致触发了一个系统“中断”事件),接着,操作系统会根据应用程序中(实际上是 VC 运行库)所设置的一系列“异常”回调函数和其它信息,来把处理该“系统异常”的控制权转交给应用程序层的“ VC 异常处理模型”中。 还记得上一篇文章中,《第 28 集 如何把 SEH 类型的系统异常转化为 C++ 类型的异常》中的方法和技术原理吗?我们不妨回顾一下,“把 SEH 类型的系统异常转化为 C++ 类型的异常”之后,它就提供了一种很好的技术方法,使得 VC 提供的 C++ 异常处理中,完全把“系统异常”的处理容纳到了按 C++ 方式的异常处理之中。
毫无疑问,这种技术大大的好!大大的妙!同样,现在 Java 的异常处理模型中,它理当应该也具备此项技术功能(谁让它是 C++ 的发展呢?),朋友们,阿愚现在是否已经把该问题解释明白了呢?是的,实际上, RuntimeException 异常的作用,就相当于上一篇文章中,阿愚为 C++ 所设计的 seh_exception_base 异常那样,它们在本质上完成同样类似的功能。只不过, Java 语言中, RuntimeException 被统一纳入到了 Java 语言和 JDK 的规范之中。请看如下代码,来验证一下我们的理解!
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
test();
}
static void test()
{
int i = 4;
int j = 0;
// 运行时,这里将触发了一个 ArithmeticException
// ArithmeticException 从 RuntimeException 派生而来
System.out.println("i / j = " + i / j);
}
}
运行结果如下:
java.lang.ArithmeticException: / by zero
at Trans.test(Trans.java:16)
at Trans.main(Trans.java:8)
Exception in thread "main"
又如下面的例子,也会产生一个 RuntimeException ,代码如下:
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
test();
}
static void test()
{
String str = null;
// 运行时,这里将触发了一个 NullPointerException
// NullPointerException 从 RuntimeException 派生而来
str.compareTo("abc");
}
}
所以,针对 RuntimeException 类型的异常, javac 是无法通过编译时的静态语法检测来判断到底哪些函数(或哪些区域的代码)可能抛出这类异常(这完全取决于运行时状态,或者说运行态所决定的),也正因为如此, Java 异常处理模型中的“ must be caught or declared to be thrown ”规则也不适用于 RuntimeException (所以才有前面所提到过的奇怪编译现象,这也属于特殊规则吧)。但是, Java 虚拟机却需要有效地捕获并处理此类异常。当然, RuntimeException 也可以被程序员显式地抛出,而且为了程序的可靠性,对一些可能出现“运行时异常( RuntimeException )”的代码区域,程序员最好能够及时地处理这些意外的异常,也即通过 catch(RuntimeExcetion) 或 catch(Exception) 来捕获它们。如下面的示例程序,代码如下:
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
test();
}
// 在上层的调用函数中,最好捕获所有的 Exception 异常!
catch(Exception e)
{
System.out.println("go here!");
e.printStackTrace();
}
}
// 这里最好显式地声明一下,表明该函数可能抛出 RuntimeException
static void test() throws RuntimeException
{
String str = null;
// 运行时,这里将触发了一个 NullPointerException
// NullPointerException 从 RuntimeException 派生而来
str.compareTo("abc");
}
}
总结
• Java 异常处理模型与 C++ 中异常处理模型的最大不同之处,就是在 Java 异常处理模型中引入了 try-finally 语法,它主要用于清理非内存性质的一些资源(垃圾回收机制无法处理的资源),例如,数据库连接、 Socket 关闭、文件流的关闭等。
• 所有的异常都必须从 Throwable 继承而来,不像 C++ 中那样,可以抛出任何类型的异常。因此,在 Java 的异常编程处理中,没有 C++ 中的 catch(…) 语法,而它的 catch(Throwable e) 完全可以替代 C++ 中的 catch(…) 的功能。
• 在 Java 的异常处理模型中,要求所有被抛出的异常都必须要有对应的“异常处理模块”。也即是说,如果你在程序中 throw 出一个异常,那么在你的程序中(函数中)就必须要 catch 这个异常(处理这个异常)。但是,对于 RuntimeException 和 Error 这两种类型的异常(以及它们的子类异常),却是例外的。其中, Error 表示 Java 系统中出现了一个非常严重的异常错误;而 RuntimeException 虽然是 Exception 的子类,但是它却代表了运行时异常(这是 C++ 异常处理模型中最不足的,虽然 VC 实现的异常处理模型很好)
• 如果一个函数中,它运行时可能会向上层调用者函数抛出一个异常,那么,它就必须在该函数的声明中显式的注明(采用 throws 关键字,语法与 C++ 类似)。
本篇文章虽然有点长,但是如果你能够坚持完整地看下来,相信你对 Java 的异常处理模型一定有了更深一步的了解(当然,由于阿愚水平有限,难免会有不少认识上或理解上的错误,欢迎大家不吝赐教!呵呵!讨论和交流也可以)。在下一篇文章中,阿愚将继续分析和阐述一些有关 Java 异常处理模型中更细节的语法特点和使用上的技巧,以及注意事项。感兴趣的 朋友们,继续吧! GO !
------------------------------------------------------------------------------------------------------------------------------------
Java 异常处理模型之细节分析
本文转自 http://se.csai.cn/ExpertEyes/No166.htm
上一篇文章中,与其是说对 Java 语言中的异常处理模型展开讨论;倒不如是说,把 Java 异常处理模型与 C++ 异常处理模型展开了全面的比较和分析。俗话说得好,“不怕不识货,就怕货比货”,因此,通过与 C++ 异常处理模型的综合比较,使我们能够更清楚地认识 Java 中异常处理模型的特点、优势,以及设计时的一些背景及原因。
这篇文章,阿愚打算对 Java 异常处理模型中的一些重要的细节问题展开详细讨论,让我们更上一层楼吧!
finally 区域内的代码块在 return 之前被执行
由于 Java 程序中,所有的对象都是在堆上( Heap )分配存储空间的,这些空间完全由垃圾回收机制来对它们进行管理。因此,从这一点可以分析得出一个推论: Java 中的异常处理模型的实现,其实要比 C++ 异常处理模型简单得多。例如,它首先不需要像 C++ 异常处理模型中那样,必须要跟踪栈上的每一个“对象”的构造和析构过程(只有跟踪并掌握了这些信息,发生异常时, C++ 系统它才会知道当前应该析构销毁哪些对象呀!),这是因为 Java 程序中,栈上是绝对没有“对象”的(实际只是对堆上对象的引用)。另外,还有 Java 语言中的异常对象的传递也更为简单和容易了,它只需传递一个引用指针而已,而完全不用考虑异常对象的构造、复制和销毁过程。
当然, Java 异常处理模型较 C++ 异常处理模型复杂的地方是,它引入了 finally 机制(主要用于数据库连接的关闭、 Socket 关闭、文件流的关闭等)。其实,我们也知道 finally 语法最早是在微软的 SEH 所设计出的一种机制,虽然它功能很强大,但是实现起来却并不是很难,从表象上来理解:当代码在执行过程中,遭遇到 return 和 goto 等类似的语句所引发作用域(代码执行流)转移时,便会产生一个局部展开( Local Unwinding );而由于异常而导致的 finally 块被执行的过程,往往被称为全局展开( Global Unwinding )。由于展开( Unwinding )而导致的 finally 块被执行的过程,非常类似于一个子函数(或子过程)被调用的过程。例如,当在 try 块中最后一条语句 return 被执行到的时候,一个展开操作便发生了,可以把展开操作想象成,是编译器在 return 语句之前插入了一些代码(这些代码完成对 finally 块的调用),因此可以得出结论: finally 区域内的代码块,肯定是在 return 之前被执行。
但是,请特别注意, finally 块区域中的代码虽然在 return 语句之前被执行,但是 finally 块区域中的代码是不能够通过重新赋值的方式来改变 return 语句的返回值。请看如下的示例代码:
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
// 你认为 test 函数返回的值是多少呢?
System.out.println("test 的返回值为: " + test());
}
public static int test()
{
int ret = 1;
try
{
System.out.println("in try block");
// 是返回 1 ,还是返回 2 呢?
return (ret);
}
catch(Exception e)
{
System.out.println("in catch block");
e.printStackTrace();
}
finally
{
// 注意,这里重新改变了 ret 的值。
ret = 2;
System.out.println("in finally block!");
}
return 0;
}
}
上面的示例程序中,本来是想在 finally 区域中通过改变 ret 的值,来影响 test 函数最终 return 的值。但是真的影响了吗?否!否!否!不信,看看运行结果吧!
in try block
in finally block!
test 的返回值为: 1
其实,在 SEH 异常处理模型中, try-finally 语句对此情况也是有相同结果的处理结果,同样是上面的那个程序,把它改称 C 语言的形式,用 VC 编译运行一把,验证一下结果,代码如下:
#include "stdio.h"
int test()
{
int ret = 1;
__try
{
printf("in try block\n");
return ret;
}
__finally
{
ret = 2;
printf("in finally block!\n");
}
return 0;
}
void main()
{
printf("test 的返回值为: %d\n", test());
}
为了更清楚了认识一下这究竟是如何道理,还是看看对这个程序的 debug 调试情况,截图如下图所示:
所以说,无论是在 SEH 异常处理模型中,还是 Java 的异常处理模型中, finally 块区域中的代码都是不能够通过重新赋值的方式来改变 try 区域中 return 语句的返回值。
强烈建议不要在 finally 内部使用 return 语句
上面刚刚说了, finally 块区域中的代码不会轻易影响 try 区域中 return 语句的返回值,但是有一种情况例外,那就是在 finally 内部使用 return 语句。示例程序如下:
// 示例程序 1 , Java 程序
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
System.out.println("test 的返回值为: " + test());
}
public static int test()
{
int ret = 1;
try
{
System.out.println("in try block");
return (ret);
}
catch(Exception e)
{
System.out.println("in catch block");
e.printStackTrace();
}
finally
{
ret = 2;
System.out.println("in finally block!");
// 这里添加了一条 return 语句
return ret;
}
}
}
// 示例程序 2 , C 程序
#include "stdio.h"
int test()
{
int ret = 1;
__try
{
printf("in try block\n");
return ret;
}
__finally
{
ret = 2;
printf("in finally block!\n");
return ret;
}
printf(" 多余的 \n");
return 0;
}
void main()
{
printf("test 的返回值为: %d\n", test());
}
上面的程序运行结果如下:
in try block
in finally block!
test 的返回值为: 2
也许大多数朋友都估计到,上面的 test 函数返回值是 2 。也即是说, finally 内部使用 return 语句后,它影响(覆盖了) try 区域中 return 语句的返回值。这真是一种特别糟糕的情况,虽然它表面上看起来不是那么严重,但是这种程序极易给它人造成误解(使得阅读该代码的人总得担心或考虑,是否有其它地方影响了这里的 return 的返回值)。
之所以出现这种现象的真正原因是,由于 finally 区域中的代码先于 return 语句( try 作用域中的)被执行,但是,如果此时在 finally 内部也有一个 return 语句,这将会导致该函数直接就返回了,而致使 try 作用域中的 return 语句再也得不到执行机会(实际就是无效代码,被覆盖了)。
面对上述情况,其实更合理的做法是,既不在 try block 内部中使用 return 语句,也不在 finally 内部使用 return 语句,而应该在 finally 语句之后使用 return 来表示函数的结束和返回,把上面的程序改造一下,代码如下
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
System.out.println("test 的返回值为: " + test());
}
catch(Exception e)
{
e.printStackTrace();
}
}
public static int test() throws RuntimeException
{
int ret = 1;
try
{
System.out.println("in try block");
}
catch(RuntimeException e)
{
System.out.println("in catch block");
e.printStackTrace();
throw e;
}
finally
{
ret = 2;
System.out.println("in finally block!");
}
// 把 return 语句放在最后,这最为妥当
return ret;
}
}
另一种更糟糕的情况
上面刚刚讲到, finally 内部使用 return 语句会覆盖 try 区域中 return 语句的返回值。不仅与此, finally 内部使用 return 语句还会导致出现另外一种更为糟糕的局面。到底是何种糟糕情况呢?还是先看看下面的示例程序再说吧!代码如下:
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
System.out.println("test 的返回值为: " + test());
}
catch(Exception e)
{
e.printStackTrace();
}
}
public static int test() throws RuntimeException
{
int ret = 0;
try
{
System.out.println("in try block");
// 这里会导致出现一个运行态异常
int i=4,j=0;
ret = i/j;
}
catch(RuntimeException e)
{
System.out.println("in catch block");
e.printStackTrace();
// 异常被重新抛出,上层函数可以进一步处理此异常
throw e;
}
finally
{
System.out.println("in finally block!");
// 注意,这里有一个 return 语句
return ret;
}
}
}
是不是觉得上面示例程序中的代码写的挺好的,挺简洁的,还挺严谨的,应该不会有什么 BUG !阿愚告诉你,错了!绝对错了!而且问题很严重!要不,朋友们在编译运行此程序前,先预期一下它的运行结果,大家是不是觉得运行流程应该是这样子的: 首先在终端输出“ in try block ”,接着,由于程序运行时出现了一个被 0 处的异常( ArithmeticException );于是,进入到 catch block 中,这里的代码将继续向终端输出了“ in catch block ”信息,以及输出异常的堆栈信息等;接着,由于异常在 catch block 中又被重新抛出了,所以控制权返回到 main 函数的 catch block 中;对了,补充一点,也许大家会觉得,由于异常的 rethrow ,使得控制权离开 test 函数作用域的时候, finally 内的代码会被执行,也即“ in finally block ”信息也会被打印到终端上了 。仅仅如此吗?不妨看一下实际的运行结果,如下:
in try block
in catch block
java.lang.ArithmeticException: / by zero
at Trans.test(Trans.java:27)
at Trans.main(Trans.java:10)
in finally block!
test 的返回值为: 0
看了实际的运行结果,是不是觉得大吃一惊!被重新抛出( rethrow )的异常居然“丢弃了”,也即 main 函数中,并没有捕获到 test 函数中的任何异常,这也许大大出乎当初写这段代码的程序员的预料吧!究竟原因何在呢?其实, 罪魁祸首就是 finally block 内部的那条 return 语句 。因为这段程序的运行流程基本如刚才我们预期描述的那样,但是有一点是不对的,那就是当 test 函数中把异常重新抛出后,这将导致又一次的 catch 的搜索和匹配过程,以及 test 函数中的 UnWinding 操作,也即 finally 被调用执行,但是由于 finally 内部的 return 语句,不仅使得它结束了 test 函数的执行(并返回一个值给上层函数),而且这还使得上面的那个对异常进行进一步的操作过程给终止了(也即控制权进入到 catch block 的过程)。瞧瞧!后果严重吧!
是呀!如果 Java 程序员不注意这种问题,养成一个严谨的、好的编程习惯,它将会导致实际的许多 Java 应用系统中出现一些莫名奇妙的现象(总感觉系统中出现了某类异常,有一些问题,但上层的模块中却总是捕获不到相关的异常,感觉一些良好!其实不然,它完全是由于 finally 块中的 return 语句不小心把异常给屏蔽丢弃掉了)。
总结
• finally 区域内的代码总在 return 之前被执行;
• 强烈建议不要在 finally 内部使用 return 语句。它不仅会影响函数的正确返回值,而且它可能还会导致一些异常处理过程的意外终止,最终导致某些异常的丢失。
关于 Java 异常处理模型的阐述暂时就到此为止吧!虽然,有关 Java 的异常处理知识和技巧,还有不少方面需要深入讨论的,例如异常的嵌套,异常的分类,函数接口中有关异常的声明等等,阿愚打算在后面“相爱篇”的一些文章中,将继续讨论这些有关的东东。
接下来一篇文章中,会详细讨论类 Unix 操作系统中对异常处理的支持,方法,以及思想等。感兴趣的朋友, Let's go!