• C++异常(exception)第一篇--综合讲解


    摘要:catch(exception &ex)是捕获所有标准库定义中的类std:exception;catch(...)则是捕获所有的异常。


    1.简介

       异常是由语言提供的运行时刻错误处理的一种方式。提到错误处理,即使不提到异常,你大概也已经有了丰富的经验,但是为了可以清楚的看到异常的好处,我们还是不妨来回顾一下常用的以及不常用的错误处理方式。

    C++异常之网络知识

    1.1常用的错误处理方式

    返回值。我们常用函数的返回值来标志成功或者失败,甚至是失败的原因。但是这种做法最大的问题是如果调用者不主动检查返回值也是可以被编译器接受的,你也奈何不了他:)这在C++中还导致另外一个问题,就是重载函数不能只有不同的返回值,而有相同的参数表,因为如果调用者不检查返回值,则编译器会不知道应该调用哪个重载函数。当然这个问题与本文无关,我们暂且放下。只要谨记返回值可能被忽略的情况即可。

    全局状态标志。例如系统调用使用的errno。返回值不同的是,全局状态标志可以让函数的接口(返回值、参数表)被充分利用。函数在退出前应该设置这个全局变量的值为成功或者失败(包括原因),而与返回值一样,它隐含的要求调用者要在调用后检查这个标志,这种约束实在是同样软弱。全局变量还导致了另外一个问题,就是多线程不安全:如果多个线程同时为一个全局变量赋值,则调用者在检查这个标志的时候一定会非常迷惑。如果希望线程安全,可以参照errno的解决办法,它是线程安全的。

    1.2不常用的处理方式

    setjmp()/longjmp()。可以认为它们是远程的goto语句。根据我的经验,它们好象确实不常被用到,也许是多少破坏了结构化编程风格的原因吧。在C++中,应该是更加的不要用它们,因为致命的弱点是longjmp()虽然会unwindingstack(这个词后面再说),但是不会调用栈中对象的析构函数--够致命吧。对于不同的编译器,可能可以通过加某个编译开关来解决这个问题,但太不通用了,会导致程序很难移植。

    1.3异常

    现在我们再来看看异常能解决什么问题。对于返回值和errno遇到的尴尬,对异常来说基本上不存在,如果你不捕获(catch)程序中抛出的异常,默认行为是导致abort()被调用,程序被终止(coredump)。因此你的函数如果抛出了异常,这个函数的调用者或者调用者的调用者,也就是在当前的callstack上,一定要有一个地方捕获这个异常。而对于setjmp()/longjmp()带来的栈上对象不被析构的问题对异常来说也是不存在的。那么它是否破坏了结构化(对于OOparadigms,也许应该说是破坏了流程?)呢?显然不是,有了异常之后你可以放心的只书写正确的逻辑,而将所有的错误处理归结到一个地方,这不是更好么?

    综上所述,在C++中大概异常可以全面替代其它的错误处理方式了,可是如果代码中到处充斥着try/throw/catch也不是件好事,欲知异常的使用技巧,请保持耐心继续阅读:)

    2.异常的语法

    在这里我们只讨论一些语法相关的问题。

    2.1try

    try总是与catch一同出现,伴随一个try语句,至少应该有一个catch()语句。try随后的block是可能抛出异常的地方。

    2.2catch

    catch带有一个参数,参数类型以及参数名字都由程序指定,名字可以忽略,如果在catch随后的block中并不打算引用这个异常对象的话。参数类型可以是build-intype,例如int,long, char等,也可以是一个对象,一个对象指针或者引用。如果希望捕获任意类型的异常,可以使用“...”作为catch的参数。

    catch不一定要全部捕获tryblock中抛出的异常,剩下没有捕获的可以交给上一级函数处理。

    2.3throw

    throw后面带一个类型的实例,它和catch的关系就象是函数调用,catch指定形参,throw给出实参。编译器按照catch出现的顺序以及catch指定的参数类型确定一个异常应该由哪个catch来处理。

    throw不一定非要出现在try随后的block中,它可以出现在任何需要的地方,只要最终有catch可以捕获它即可。即使在catch随后的block中,仍然可以继续throw。这时候有两种情况,一是throw一个新类型的异常,这与普通的throw一样。二是要rethrow当前这个异常,在这种情况下,throw不带参数即可表达。例如:

    try{
       ...
    }
    catch(int){
        throwMyException("hello exception");    //
    抛出一个新的异常
    }
    catch(float){
       throw;                  //
    重新抛出当前的浮点数异常
    }

    2.4函数声明

    还有一个地方与throw关键字有关,就是函数声明。例如:

    void foo() throw (int);     //只能抛出int型异常
    voidbar() throw ();         //
    不抛出任何异常
    voidbaz();                 //
    可以抛出任意类型的异常或者不抛出异常

    如果一个函数的声明中带有throw限定符,则在函数体中也必须同样出现:

    void foo() throw (int)
    {
       ...
    }

    这里有一个问题,非常隐蔽,就是即使你象上面一样编写了foo()函数,指定它只能抛出int异常,而实际上它还是可能抛出其他类型的异常而不被编译器发现:

    void foo() throw (int)
    {
       throw float;     //
    错误!异常类型错误!会被编译器指出
       ...
        baz();          //
    正确!baz()可能抛出非int异常而编译器又不能发现!
    }

    voidbaz()
    {
        throw float;
    }

    这种情况的直接后果就是如果baz()抛出了异常,而调用foo()的代码又严格遵守foo()的声明来编写,那么程序将abort()。这曾经让我很恼火,认为这种机制形同虚设,但是还是有些解决的办法,请参照“使用技巧”中相关的问题。

    3.异常使用技巧


    3.1
    异常是如何工作的

    为了可以有把握的使用异常,我们先来看看异常处理是如何工作的。

    3.1.1unwinding stack

    我们知道,每次函数调用发生的时候,都会执行保护现场寄存器、参数压栈、为被调用的函数创建堆栈这几个对堆栈的操作,它们都使堆栈增长。每次函数返回则是恢复现场,使堆栈减小。我们把函数返回过程中恢复现场的过程称为unwindingstack

    异常处理中的throw语句产生的效果与函数返回相同,它也引发unwindingstack。如果catch不是在throw的直接上层函数中,那么这个unwinding的过程会一直持续,直到找到合适的catch。如果没有合适的catch,则最后std::unexpected()函数被调用,说明发现了一个没想到的异常,这个函数会调用std::terminate(),这个terminate()调用abort(),程序终止(coredump)

    在“简介”中提到的longjmp()也同样会unwindingstack,但是这是一个C函数,它就象free()不会调用对象的析构函数一样,它也不知道在unwindingstack的过程中调用栈上对象的析构函数。这是它与异常的主要区别。

    3.1.2RTTI

    unwindingstack的过程中,程序会一直试图找到一个“合适”的catch来处理这个异常。前面我们提到throwcatch的关系很象是函数调用和函数原型的关系,多个catch就好象一个函数被重载为可以接受不同的类型。根据这样的猜测,好象找到合适的catch来处理异常与函数重载的过程中找到合适的函数原型是一样的,没有什么大不了的。但实际情况却很困难,因为重载的调用在编译时刻就可以确定,而异常的抛出却不能,考虑下面的代码:

    void foo() throw (int)
    {
       throw int;
    }

    void bar()
    {
       try{
            foo();
       }
        catch(int){
           ...
        }
       catch(float){
            ...
       }
    }

    void baz()
    {
       try{
            foo();
       }
        catch(int){
           ...
        }
       catch(float){
            ...
       }
    }

    foo()在两个地方被调用,这两次异常被不同的catch捕获,所以在为throw产生代码的时候,无法明确的指出要由哪个catch捕获,也就是说,无法在编译时刻确定。

    仍然考虑这个例子,让我们来看看既然不能在编译时刻确定throw的去向,那么在运行时刻如何确定。在bar()中,一列catch就象switch语句中的case一样排列,实际上是一系列的判断过程,依次检查当前异常的类型是否满足catch指定的类型,这种动态的,在运行时刻确定类型的技术就是RTTI(RuntimeTypeIdentification/Information)。深度探索C++对象模型[1]中提到,RTTI就是异常处理的副产品。关于RTTI又是一个话题,在这里就不详细讨论了。

    3.2是否继承std::exception

    是的。而且std::exception已经有了一些派生类,如果需要可以直接使用它们,不需要再重复定义了。

    3.3每个函数后面都要写throw()?

    尽管前面已经分析了这样做也有漏洞,但是它仍然是一个好习惯,可以让调用者从头文件得到非常明确的信息,而不用翻那些可能与代码不同步的文档。如果你提供一个库,那么在库的入口函数中应该使用catch(...)来捕获所有异常,在catch(...)中捕获的异常应该被转换(rethrow)为throw列表中的某一个异常,这样就可以保证不会产生意外的异常。

    3.4guard模式

    异常处理在unwindingstack的时候,会析构所有栈上的对象,但是却不会自动删除堆上的对象,甚至你的代码中虽然写了delete语句,但是却被throw跳过,导致内存泄露,或者其它资源的泄露。例如:

    void foo()
    {
       ...
        MyClass * p = new MyClass();
       bar(p);
        ...
        deletep;       //
    如果bar()中抛出异常,则不会运行到这里!
    }

    voidbar(MyClass * p)
    {
        throw MyException();
    }

    对于这种情况,C++提供了std::auto_ptr这个模板来解决问题。这个常被称为“智能指针”的模板原理就是,将原来代码中的指针用一个栈上的模板实例保护起来,当发生异常unwindingstack的时候,这个模板实例会被析构,而在它的析构函数中,指针将被delete例如:

    void foo()
    {
       ...
        std::auto_ptr<MyClass> p(newMyClass());
        bar(p.get());
       ...
        // delete p;      //
    这句不再需要了
    }

    voidbar(MyClass * p)
    {
        throw MyException();
    }

    不论bar()是否抛出异常,只要p被析构,内存就会被释放。

    不光对于内存,对于其他资源的管理也可以参照这个方法来完成。在ACE[2]中,这种方式被称为Guard,用来对锁进行保护。

    3.5构造函数和析构函数

    构造函数没有返回值,很多地方都推荐通过抛出异常来通知调用者构造失败。这是肯定是个好的办法,但是也不很完美。主要是因为在构造函数中抛出异常并不会引发析构函数的调用,例如:

    class foo
    {
    public:
       ~foo() {} //
    这个函数将被调用
    };

    class bar
    {
    public:
       bar() { c_ = new char[10]; throw -1;}
        ~bar() {delete c_;} //
    这个函数不会被调用!
    private:
       char * c_;
        foo f_;
    };

    void baz()
    {
       try{
            bar b;
       }
        catch(int){
        }
    }

    在这个例子中,bar的析构函数不会被调用,但是尽管如此,foo的析构函数还是可以被调用。危险的是在构造函数中分配空间的c_,因为析构函数没有被调用而变成了leak最好的解决办法还是auto_ptr,使用auto_ptr后,bar类的声明变成:

    class bar
    {
    public:
       bar() {
    c_.reset(newchar[10]); throw -1;}
        ~bar() { } //
    不需要再deletec_了!
    private:
      
    auto_ptr<char> c_;
       foo f_;
    };

    析构函数中则不要抛出异常,这一点在ThinkingIn C++ Volume 2[3]中有明确表述。如果析构函数中调用了可能抛出异常的函数,则应该在析构函数内部catch它。


    3.6什么时候使用异常

    到现在为止,我们已经讨论完了异常的大部分问题,可以实际操作操作了。实际应用中遇到的最让我头疼的问题就是什么时候应该使用异常,是否应该用异常全面代替“简介”中提到的其它错误处理方式呢?

    首先,不能用异常完全代替返回值,因为返回值的含义不一定只是成功或失败,有时候是一个可选择的状态,例如:

    if(customer->status() ==active){
        ...
    }
    else{
       ...
    }

    在这种情况下,不论返回值是什么,都是程序可以接受的正常的结果。而异常只能用来表达“异常”--也就是错误的状态。这好象是显而易见的事情,但是实际编程的过程中有很多更加模棱两可的时候,遇到这样的情况,首先要考虑的就是这个原则。

    第二,看看在特定的情况下异常是否会发挥它的优点,而这个优点正好又不能使用其他技术达到(或者简单的达到)。比如,如果你正在为电信公司写一个复杂计费逻辑,那么你当然希望在整个计算费用的过程中集中精力去考虑业务逻辑方面的问题,而不是到处需要根据当前返回值判断是否释放前面步骤中申请的资源。这时候使用异常可以让你的代码非常清晰,即使你有100处申请资源的地方,只要一个地方集中释放他们就好了。例如:

    bool bar1();
    boolbar2();
    bool bar3();

    bool foo()
    {
       ...
        char * p1 = new char[10];
       ...
        if(!bar1()){
           delete p1;
            returnfalse;
        }
        ...
       char * p2 = new char[10];
        ...
       if(!bar2()){
            deletep1;            //
    要释放前面申请的所有资源
           delete p2;
            returnfalse;
        }
        ...
       char * p3 = new char[10];
        ...
       if(!bar2()){
            deletep1;            //
    要释放前面申请的所有资源
           delete p2;
            deletep3;
            return false;
       }
    }

    这种流程显然不如:

    void bar1() throw(int);
    voidbar2() throw(int);
    void bar3() throw(int);

    void foo() throw(int)
    {
        char * p1 = NULL;
       char * p2 = NULL;
        char * p3 = NULL;
       try{
            char * p1 = newchar[10];
           bar1();
            char * p2 = newchar[10];
           bar2();
            char * p3 = newchar[10];
            bar3();
       }
        catch(int){
           delete p1;        //
    集中释放资源
           delete p2;
            deletep3;
            throw;
       }
    }

    第三,在ThinkingIn C++ Volume 2[3]中列了一个什么时候不应该用,什么时候应该用的表,大家可以参考一下。

    最后,说一个与异常无关的东西,但也跟程序错误有关的,就是断言(assert),我在开发中使用了异常后,很快发现有的人将应该使用assert处理的错误定义成了异常。这里稍微提醒一下assert的用法,非常简单的原则:只有对于那些可以通过改进程序纠正的错误,才可以用assert。返回值、异常显然与其不在一个层面上,这是C的入门知识。

    4   c++ 捕获所有异常的写法

        try
        {
            device = createDevice(video::EDT_DIRECT3D9,
                core::dimension2d<u32>(512, 384));
        }
        catch (...)
        {
            device = 0;
        }
        


    ===2013.1.9 根据zhouaihui1010 的指出修改=========================

    上面这种异常捕获方式,对于c++ 除零错误, 内存错误等异常无法捕获。

    除零错误,可以用signal函数处理硬件中断信号来处理。


    try-catch异常捕获的两种模式有关。同步模式和异步模式。其中前者不能捕获内存访问错误,后者可以捕获内存访问错误。 
    /EHs是启用同步模式。(同   /GX)       
    /EHa是起用异步模式。


    VC的工程的调试版本缺省使用异步模式,工程的发布版本缺省使用同步模式。


    实际上,win32开发中还有个Windows SEH 结构化异常。结构化异常是Windows操作系统提供的与语言无关的异常处理机制, SHE使用Win32API中的RaiseException()函数来抛出异常,在VC中使用关键字__try和关键字__except来捕获,并用宏函数GetExceptionCode和GetExceptionInfo来获取捕获的异常由什么原因产生,和产生异常时环境状态。__finally关键字保证无论是否发生异常,finally代码段都会被执行。


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

    由上种种可以看出,c++由于比较接近底层,因此程序员在拥有更大自由度的同时,也需要处理更多的问题。



    异常的定义

    如果一个位置所发生的事情最终能够被正确的处理并使得程序如期正常运行,那么这件事情又怎么能被认为是一个错误呢?”事实上,我们把这类异常事件(或简称异常)以及用来处理这类事件的语言机制一起成为异常处理(exception handling)。


    C++异常之我的见解

    标准的c++库里包含<exception><stdexcept>

    其中<exception>之定义了最基本的exception

    <stdexcept>中定义了从exception类继承出来的其他一般异常类

    --------------------------------------------------------------------------------
    exception                C++标准库为所有抛出异常的类提供的类库.使用what()函数可以取得exception对象初始化时被设置的可选字符串.
    logic_error                exception类派生.报告程序逻辑错误,通过检查代码,能够发现这类错误.
    ------------------------------------------------------------------------------
    runtime_error           exception类派生.报告运行时错误,只有在程序运行时,这类错误才可能被检测到.
    ios::failure                exception类派生,没有子类从logic_error派生的异常:
    domian_error                报告违反了前置条件
    invalid_argument          表明抛出这个异常的函数接收到了一个无效的参数
    length_error                表明程序试图产生一个长度大于npos的对象
    out_of_range                报告一个参数越界错误
    -------------------------------------------------------------------------
    bad_cast                      抛出这个异常的原因是在运行时类型识别(runtimetype identification)中发现程序执行了                        一个无效的动态类型转换(dynamic_cast)表达式
    ------------------------------------------------------------------------------
    bad_typeid                当表达式typeid(*p)中的参数p是一个空指针时抛出这个异常
    --------------------------------------------------------------------------------runtime_error派生的异常--------------------------------------------------------------------------------
    range_error                报告违反了后置条件.
    ----------------------------------------------------------------------------
    overflow_error                报告一个算术溢出错误
    -----------------------------------------------------------------------------
    bad_alloc                      报告一个失败的存储分配

    本质

    异常类虽然多但是,只是定义一种规范而已,类本身没有和类名相对应任何处理语句,他就像书的目录一样,只是起到一种归类的作用本身没有任何内容.

    真正实现的异常的地方是在一个独立的框架或平台中通过判断不同的条件throw出来的,throw的信息可以穿越多层函数最后返回到catch的地方.

    但是有一点需要主意有时在面向过程的编程(c++c编程风格)他会打乱程序的顺序,有可能是某些状态无效,所以在处理完异常后需要重新设置状态.


    还有一点就是通过没有经过验证需进一步来验证

    A--B(B继承自A)

    ThrowB类的异常

    catchA类异常的时候可以捕获B类的异常

    所以在CATCH的时候要把子类放在前面?????


    作者:张笑猛

    原文出处:http://objects.nease.net/


  • 相关阅读:
    4. Median of Two Sorted Arrays
    3. Longest Substring Without Repeating Characters
    695. Max Area of Island
    2015 Benelux Algorithm Programming Contest E-Excellent Engineers
    URAL
    Codeforces Round #309 (Div. 2) -D. Kyoya and Permutation
    Codeforces Round #144 (Div. 2) D table
    Codeforces Round #429 (Div. 2)
    Codeforces 610D Vika and Segments
    Codeforces 757D
  • 原文地址:https://www.cnblogs.com/catkins/p/5270610.html
Copyright © 2020-2023  润新知