在上一篇文章中我们看到了,在 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 编写的程序中,每当我们程序运行中出现意外异常导致的崩溃事件时,系统总能够弹出一个“应用程序错误”框,如下:
NT 操作系统是如何实现的呢?很简单,它就是在在 VC 运行库中的 顶层的函数内采用了 try-except 机制,不信,看看如下截图代码吧!
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++ 类型的异常? ”, 程序员朋友们,继续吧!
评论