异常处理机制 --- 相知篇 (六)

-----------------------------------------------------------------------------------------------------------------------------


setjmp与longjmp机制,很难与C++和睦相处


        本文转自http://se.csai.cn/ExpertEyes/No155.htm
 
  在《第16集 C语言中一种更优雅的异常处理机制》中,就已经提到过,“setjmp和longjmp并不能很好地支持C++中面向对象的语义。因此在C++程序中,请使用C++提供的异常处理机制”。它在MSDN中的原文如下:
 
  setjmp and longjmp do not support C++ object semantics. In C++ programs, use the C++ exception-handling mechanism.
  这究竟是为什么?大家知道,C++语言中是基本兼容C语言中的语义的。但是为什么,在C++程序中,唯独却不能使用C语言中的异常处理机制?虽然大家都知道,在C++程序中,实际上是没有必要这么做,因为C++语言中提供了更完善的异常处理模型。但是,在许多种特殊情况下,C++语言来开发的应用程序系统中,可能采用了C语言中的异常处理机制。例如说,一个应用程序由于采用C++开发,它里面使用了C++提供的异常处理机制;但是它可能需要调用其它已经由C语言实现的程序库,而恰恰在这个被复用的程序库中,它也采用了异常处理机制。因此对于整个应用程序系统而言,它不可避免地出现了这种矛盾的局面。并且这种情况是非常多见的,也可能是非常危险的。因为毕竟,“setjmp and longjmp do not support C++ object semantics”。所以,我们非常有必要来了解它究竟为什么不会兼容。

  在本篇文章中,主人公阿愚将和程序员朋友们一起,深入探讨setjmp与longjmp机制,为什么它很难与C++和睦相处?另外还有,如果C++语言来开发的应用程序系统中,不得不同时使用这两种异常处理模型时,又如何来尽可能保证程序系统的安全?

C++语言中使用setjmp与longjmp

  闲话少说,还是看例程先吧!代码如下:

// 注意,这是一个C++程序。文件扩展名应该为.cpp或其它等。例如,c++setjmp.cpp  
#include <stdio.h>  
#include <setjmp.h>  
#include <stdlib.h>  
//定义一个测试类  
class MyTest  
{  
public:  
    MyTest ()  
    {  
        printf("构造一个MyTest类型的对象/n");  
    }  
    virtual ~ MyTest ()   
    {  
        printf("析构销毁一个MyTest类型的对象/n");  
    }  
};  
jmp_buf mark;  
void test1()  
{  
    // 注意,这里抛出异常  
    longjmp(mark, 1);  
}  
void test()  
{  
    test1();  
}  
void main( void )  
{  
    int jmpret;  
    // 设置好异常出现时,程序的回溯点  
    jmpret = setjmp( mark );  
    if( jmpret == 0 )  
    {  
        // 建立一个对像  
        MyTest myobj;  
        test();  
    }  
    else  
    {  
        printf("捕获到一个异常/n");  
    }  
}  
 
  请编译运行一下,程序的运行结果如下:
  构造一个MyTest类型的对象
  析构销毁一个MyTest类型的对象
  捕获到一个异常
  上面的程序运行结果,那么到底是不是合理的呢?阿愚感到有些纳闷,这结果肯定是合乎情理的呀!从这个例程来看,setjmp和longjmp并不能破坏C++中面向对象的语义,它们之间融洽得很好呀!那么为什么会说,“setjmp and longjmp do not support C++ object semantics”。请不要着急,沉住气!继续看看其它的情况,代码如下:

#include <stdio.h>  
#include <setjmp.h>  
#include <stdlib.h>  
class MyTest  
{  
public:  
    MyTest ()  
    {  
        printf("构造一个MyTest类型的对象/n");  
    }  
    virtual ~ MyTest ()   
    {  
        printf("析构销毁一个MyTest类型的对象/n");  
    }  
};  
jmp_buf mark;  
void test1()  
{  
    // 注意,这里在上面程序的基础上,进行了一点小小的改动  
    // 把对像的构造挪到这里来  
    MyTest myobj;  
    longjmp(mark, 1);  
}  
void test()  
{  
    test1();  
}  
void main( void )  
{  
    int jmpret;  
    jmpret = setjmp( mark );  
    if( jmpret == 0 )  
    {  
        test();  
    }  
    else  
    {  
        printf("捕获到一个异常/n");  
    }  
}  
 
  同样也编译运行一下,看程序的运行结果,如下:
  构造一个MyTest类型的对象
  捕获到一个异常
  呵呵!那个对像的构造建立过程只不过是被稍稍挪了一下位置,而且先后顺序还没有改变,都是在if( jmpret == 0 )语句之后,longjmp(mark, 1)之前。可为什么程序运行的结果却不同了呢?显然,从这个例程的运行结果来看,setjmp和longjmp已经破坏C++中面向对象的语义,因为那个MyTest类型的对像只被构造了,但是它却没有被析构销毁!这与大家所知道的面向对象的理论是相违背的。程序员朋友们,不要小看这个问题,有时这种错误将给应用程序系统带来极大的灾难(不仅仅是内存资源得不到释放,更糟糕的是可能引发系统死锁或程序崩溃)。

  由此可以看出,setjmp与longjmp机制,有时的确是不能够与C++和睦相处。那么,为什么第1个例子中会安然无恙呢?它有什么规律吗?请继续看另外的一个例子,代码如下:
#include <stdio.h>  
#include <setjmp.h>  
#include <stdlib.h>  
class MyTest  
{  
public:  
    MyTest ()  
    {  
        printf("构造一个MyTest类型的对象/n");  
    }  
    virtual ~ MyTest ()   
    {  
        printf("析构销毁一个MyTest类型的对象/n");  
    }  
};  
jmp_buf mark;  
void test1()  
{  
    longjmp(mark, 1);  
}  
void test()  
{  
    / // 注意,现在又把它挪到了这里  
        MyTest myobj;  
    test1();  
}  
void main( void )  
{  
    int jmpret;  
    jmpret = setjmp( mark );  
    if( jmpret == 0 )  
    {  
        test();  
    }  
    else  
    {  
        printf("捕获到一个异常/n");  
    }  
}  
 
  请编译运行一下,程序的运行结果如下:
  构造一个MyTest类型的对象
  析构销毁一个MyTest类型的对象
  捕获到一个异常
  呵呵!这里的运行结果也是对的。所以主人公阿愚总结出了一条结论,那就是,“在longjmp被调用执行的那个函数作用域中,绝对不能够存在局部变量形式的对象(也即在堆栈中的对象,longjmp执行时还没有被析构销毁),否则这些对象将得不到析构的机会”。切忌切忌!又例如下面的例子同样也会有问题,代码如下:

#include <stdio.h>  
#include <setjmp.h>  
#include <stdlib.h>  
class MyTest  
{  
public:  
    MyTest ()  
    {  
        printf("构造一个MyTest类型的对象/n");  
    }  
    virtual ~ MyTest ()   
    {  
        printf("析构销毁一个MyTest类型的对象/n");  
    }  
};  
jmp_buf mark;  
void main( void )  
{  
    int jmpret;  
    jmpret = setjmp( mark );  
    if( jmpret == 0 )  
    {  
        MyTest myobj;  
        longjmp(mark, 1);  
    }  
    else  
    {  
        printf("捕获到一个异常/n");  
    }  
}  
 
总结
   虽然说,setjmp与longjmp机制,很难与C++和睦相处。但是它并非那么可怕,只要掌握了它的规律,整个应用程序系统的安全性仍掌握在你手心。而且,本文开头提到的例子(采用C++开发的应用程序,它里面调用了C语言实现的其它程序库,并且库代码中使用了setjmp和longjmp机制),它并没有任何的问题。因为这个C库中,其中调用longjmp的函数域内,决不会有对象的构造定义。

  现在为止,对setjmp和longjmp的讨论暂告一个段落。在“爱的秘密”篇中,会进一步阐述它的实现。下一篇文章将讨论在C++中,如何兼容并支持C语言中提供的其它方式的异常处理机制(例如,C++中对goto语句的支持)。哈哈! goto next!





----------------------------------------------------------------------------------------------------------------------------------



C++中如何兼容并支持C语言中提供的异常处理机制



         本文转自 http://se.csai.cn/ExpertEyes/No156.htm
 
  C语言中提供的异常处理机制并不是十分严谨,而且比较杂,功能也非常有限。最常见的除了setjmp与longjmp之外,goto语句在实际编程中也使用很广泛(虽然不建议使用它)。大家现在也都知道,在C++语言中,它并不完全兼容并支持setjmp与longjmp函数的使用。但是  C++语言对待goto语句又将如何呢?

C++语言中如何处理goto语句

  大家知道,在C语言程序中,goto语句被编译成机器指令后,它只对应一条jmp指令。但是在C++语言程序中,goto语句也会这么简单吗?no!为什么这么说呢?因为C++语言是面向对象的语言,如果goto语句只会简单地对应一条jmp指令,那么在许多情况下,这会破坏面向对象的一些特性。例如下面的示例程序,代码如下:

#include <stdio.h>  
#include <setjmp.h>  
#include <stdlib.h>  
class MyTest  
{  
public:  
    MyTest ()  
    {  
        printf("构造一个MyTest类型的对象/n");  
    }  
    virtual ~ MyTest ()   
    {  
        printf("析构销毁一个MyTest类型的对象/n");  
    }  
};  
void main( void )  
{  
    MyTest myobj0;  
    {  
        int error;  
        MyTest myobj1;  
        MyTest myobj2;  
        MyTest myobj3;  
        error = 1;  
        // 注意下面这条goto语句,如果它只是一条简单的jmp指令,  
        // 那么myobj1,myobj2,myobj3对象将如何被析构销毁呢?  
        if(error) goto Error;  
        printf("no error, continue/n");  
    }  
Error:  
    return;  
}  
 
  请编译运行一下,程序的运行结果如下:
  构造一个MyTest类型的对象
  构造一个MyTest类型的对象
  构造一个MyTest类型的对象
  构造一个MyTest类型的对象
  析构销毁一个MyTest类型的对象
  析构销毁一个MyTest类型的对象
  析构销毁一个MyTest类型的对象
  析构销毁一个MyTest类型的对象
  呵呵!从程序的运行结果来看,显然,它符合面向对象的规则定义,“一个对象被构造了,就必然会有析构的过程”。所以说,在C++语言中,它是能够很好兼容并支持goto语句的语义,这也与C++是C语言的继承、扩充、完善的版本等承诺是相一致的。但是同时我们也知道,C++中对象的析构,是由编译器来予以支持的,那就是当编译器在编译程序时,如果局部对象在离开它的作用域时,编译器会显式地插入一些调用对象析构函数的代码,来销毁这些即将无效掉的局部对象。但是程序中如果遭遇到goto语句时,显然,编译器也需要插入对局部对象的析构函数的显式调用。请在上面的程序中goto语句那一行,按F9设置一个断点;然后F5,调试程序;接着Alt+8切换到汇编代码的显示状态下,注意查看if(error) goto Error语句对应的汇编程序。截图如下:

 
  呵呵!从上图可以很明显的看出,编译器在处理goto语句时,需要进行更多的工作,它必须要插入所有当前局部对象的析构函数的显式调用代码,然后才能真正执行jmp指令。其它对于许多其它类似的语句,编译器的处理也是类似,例如对于return语句的处理也是如此,把上面的那个程序小小改动一点,代码如下:

#include <stdio.h>  
#include <setjmp.h>  
#include <stdlib.h>  
class MyTest  
{  
public:  
    MyTest ()  
    {  
        printf("构造一个MyTest类型的对象/n");  
    }  
    virtual ~ MyTest ()   
    {  
        printf("析构销毁一个MyTest类型的对象/n");  
    }  
};  
void main( void )  
{  
    MyTest myobj0;  
    {  
        int error;  
        MyTest myobj1;  
        MyTest myobj2;  
        MyTest myobj3;  
        error = 1;  
        // 用return语句直接返回  
        if(error) return; //goto Error;  
        printf("no error, continue/n");  
    }  
Error:  
    return;  
}  
 
  同样也调试程序,接着Alt+8切换到汇编代码的显示状态下,注意查看if(error) return语句对应的汇编程序。截图如下:

总结
   虽然说,在C++语言中,它能够很好兼容并支持goto语句的语义(也包括其它一些与异常处理相关的语句)。但是,主人公阿愚强烈建议程序员朋友在编写C++程序代码时,不要轻易使用goto语句,因为与C程序中的goto语句相比,它不仅破坏了结构化的程序设计,破坏了程序代码的整体美感,而且它更导致了C++程序模块的臃肿(编译器因此而导致需要插入了太多重复性代码)。

  到目前为止,主人公阿愚引领大家,对C++和C语言中的异常处理机制,进行了广泛而深入的探讨,阿愚深感收获甚多,当然也有可能认识上的不少错误,欢迎朋友们指出并共同讨论。

  从下一篇文章中,开始对操作系统提供的异常处理机制进行一个全面而系统的介绍和较深入的研究。尤其是Windows平台提供的结构化异常处理机制,也即大名鼎鼎的SEH(Structured Exception Handling)。熟悉SHE的朋友们,请GO!因为阿愚期待与大家一同学习探讨;当然,哪些不太熟悉SHE的朋友们,也请GO!因为阿愚在这里,一定把最深切的学习体会和经验总结奉献给大家!继续吧!









你可能感兴趣的:(编程,c,exception,mfc,语言)