异常处理

1 C++ 异常处理

     使用异常处理,程序中独立开发的各部分能够就程序执行期间出现的问题相互通信,并处理这些问题。程序的一个部分能够检测出本部分无法解决的问题,这个问题检测部分可以将问题传递给准备处理问题的其它部分。

1.1 处理基本结构

    C++的异常处理中,需要由问题检测部分抛出一个对象给处理代码部分,通过这个对象的类型和内容,两个部分能够就出现了什么错误进行通信。

C++的异常处理中包括:

  • throw表达式:错误检查部分使用这种表达式来说明遇到了不可处理的错误。可以说,throw引发(raise)了异常条件。
  • try块:错误处理部分使用它来处理异常。try语句块以try关键字开始,并以一个或多个catch子句结束。在try块中执行的代码所抛出(throw)的异常,通常会被其中一个catch字句处理。由于它们"处理"异常,catch子句也称为处理代码。
  • 异常类:由标准定义的一组异常类,用来在throw和相应的catch之间传递有关的错误信息。

1.2 抛出与捕获

      异常由throw表达式抛出异常对象而引发(raise),最后由catch子句捕获。异常对象这种抛出和捕获过程类似于将实参传递给函数的方式,可以简单将throw表示式认为是函数调用,并将所有catch子句认为是多态的函数声明。而具体调用到哪个catch子句,由throw传递的实参类型和catch声明的形参类型匹配决定。同时在普通函数的调用-声明模型都可以使用于throw-catch模式,包括参数传递、实参的作用范围、派生类转换为基类等语法都适用。

1.2.1 throw

    throw表达式是在try块中执行,并且当执行throw表达式的时候,不会执行跟在throw后面的语句,而是将控制流从throw转移到匹配的catch子句,该catch可以是同一函数中局部的catch,也可以在直接或间接调用发生异常的函数的另一个函数中,控制从一个地方传到另一个地方。

通过throw抛出任意类型的异常对象,包括对象、指针、引用和智能指针等类型。 可以通过智能指针对异常类型进行封装,从而在其他地方也能够正常捕捉。

其中在throw-catch过程中需要注意的问题:

  1. 若传递对象类型,那么被传递的异常类型必须可复制的类型。
  2. 若传递指针类型,那么需保证该指针所指向的对象仍有效,若是栈对象会在退出try块后被自动释放;若是堆对象,那么需要手动释放。

    可以采用boost的智能指针进行封装异常指针类型,从而实现自动释放,如下所示:

  1. try
  2. {
  3.     bad_alloc *e = new bad_alloc();
  4.     throw(shared_ptr<bad_alloc> (e));
  5. } catch (shared_ptr<bad_alloc> e)
  6. {
  7.     printf("hello:%s\n", e->what());
  8. }

1.2.2 catch

    catch子句的中的异常说明符看起来像只包含一个形参形参表,异常说明符是在其后跟一个(可选)形参名的类型名。 说明符的类型决定了处理代码能够捕获的异常种类。类型必须是完全类型,即必须是内置类型或者是已经定义的程序员自定义类型。

     1)查找匹配的处理代码

    在查找匹配的catch期间,找到的catch不必是与异常最匹配的那个catch,相反,将选中第一个找到的可以处理该异常的catch。因此,在catch子句列表中,最特殊的catch必须最先出现。

异常与catch异常说明符匹配的规则比匹配实参和形参类型的规则更严格,大多数转换都不允许——除了下面几种可能的区别之处,异常的类型与catch说明符的类型必须完全匹配:

  • 运行从const到const的转换。也就是说,非const对象的throw可以与指定接受const引用的catch匹配。
  • 允许从派生类型到基类类型的转换。
  • 将数组转换为指向数组类型的指针,将函数转换为指向数组类型的适当指针。

      2)异常说明符与继承

    像形参声明一样,基类的异常说明符可以用于捕获派生类型的异常对象,即可以由try块中抛出派生类型,在catch生明为基类,从而能够接收这种转换。

 

1.2.3 rethrow

      有可能单个catch不能完全处理一个异常。在进行了一些校正行动之后,catch可能确定该异常必须由函数调用链中更上层的函数来处理,catch可以通过重新抛出(rethrow)将异常传递给函数调用链中更上层的函数。重新抛出是后面不跟类型或表达式的一个throw:

  1. throw

      虽然重新抛出不指定自己的异常,但仍然将一个异常对象沿链向上传递,被抛出的异常是原来的异常对象,而不是catch形参。并且在同一个try块中的后面catch不会捕捉到重新抛出的异常对象。

      一般而言,catch可以改变它的形参。在改变它的形参之后,如果catch重新抛出异常,那么,只有单异常说明符是引用或指针类型的时候,才会传播那些改变。即只有指针类型的形参才能发生更改原来的异常对象。

1.2.4 catch(…)

      即使函数不能处理被抛出的异常,它也可能想要在随抛出异常退出之前执行一些动作。所以c++提供一种机制,捕获所有异常的catch子句。捕获所有异常的catch子句形式为(…)。例如:

  1. void manip()
  2. {
  3.      try{
  4.      }
  5.      catch(…)
  6.     {
  7.         throw
  8.     }
  9. }

注意:

    catch(…)子句可以单独使用,也可以与其它catch子句结合使用,但是若与其它catch子句结合使用,它必须是最后一个。

1.3 异常栈展开

      抛出异常的时候,将暂停当前函数的执行,开始查找匹配的catch子句。首先检查throw本身是否在try块内部,如果是,检查与该try相关的catch子句,看是否其中之一与被抛出对象相匹配。如果找到匹配的catch,就处理异常;如果找不到,就退出当前函数(释放当前函数的内存并撤销局部对象),并且继续在调用函数中查找。

      异常栈开展是指异常对象的查找catch的过程,它沿着嵌套函数调用链继续向上,直至为异常找到一个catch子句。只要找到能够处理异常的catch子句,就进入该catch子句,并在该处理代码中继续执行。当catch结束的时候,在紧接在与该try块相关的最后一个catch子句之后的点继续执行,意思是说当catch捕捉到异常后,则从try-catch块后的其它语句开始执行。

      1)未捕获的异常终止程序

如果在栈展开的过程中,如果找不到匹配的catch子句,那么程序就调用库函数terminate,从而终止程序的执行。

      2)内存空间释放

      由于当发生异常时,会提早退出程序,特别是当前函数不能处理抛出的异常对象时,那么执行过程将偏离正常的执行过程。此时,栈空间的对象都能够进行自动释放,而堆空间是需要手动进行释放,所以必须做相应的处理操作。

      3)异常与析构函数

      在为某个异常进行栈展开的时候,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库terminate函数。一般而言,terminate函数将调用abort函数,强制从整个程序非正常退出。即在析构函数中不能抛出异常,若抛出异常将导致程序非正常退出。

      4)异常与构造函数

      与析构函数不同,构造函数内部所做的事情经常会抛出异常。如果在构造函数对象的时候发生异常,则该对象可能只是部分被构造,它的一些成员可能已经初始化,而另一些成员在异常发生之前还没有初始化。及时对象至少四部分被构造了,也要保证将会适当地撤销已构造的成员。

1.4 函数异常说明

      查看普通函数声明的时候,不可能确定该函数会抛出什么异常。但是,为了编写适当的catch子句,了解函数是否抛出异常以及会抛出哪种异常是很有用的。异常说明(exception specification)指定,如果函数抛出异常,被抛出的异常将包含在该说明中的一种,或者是从列出的异常中派生的类型。

      1)定义异常说明

      异常说明跟在函数形参表之后,由一个异常说明在关键字throw之后跟着一个由园括号括住的异常类型列表,如下所示:

  1. void recoup(int) throw (runtime_error);
  2. 这个声明指出,recoup是接收int值的函数,并返回void如果recoup抛出一个异常,该异常将是runtime_error对象,或者是由runtime_error派生的类型的异常。

注意:

  • 空说明列表支出函数不抛出任何异常,如下所示:
  1. void no_problem() throw();
  • 如果一个函数声明没有指定异常说明,则该函数可以抛出任意类型的异常。

      2)违反异常说明

     但是,不可能在编译时知道程序是否抛出异常以及会抛出哪些异常,只有在运行时才能检测是否违反函数异常说明。

如果抛出抛出了没有在其异常说明列出的异常,就调用标准库函数unexpected。默认情况下,unexpected函数调用terminate函数,terminate函数一般会终止程序。

      3)虚函数异常说明

      基类中虚函数的异常说明,可以与派生类中对应虚函数的异常说明不同。但是派生类中虚函数的异常说明在基类中的异常说明必须都有,即基类虚函数的异常声明比派生类虚函数的异常声明要多。

 

2 boost异常处理

      boost::exception库针对标准库中异常类的缺陷进行了强化,提供<<操作符重载,可以向异常传入任意数据,有助于增加异常的信息和表达力。 其中exception位于名字空间boost,为使用exception,需要包含头文件<boost/exception/all.hpp>。

2.1 类摘要

boost的exception库提供了两个基本类:exception和error_info;

2.1.1 exception类

      exception类几乎没有公开的成员函数,它的设计意图:它是一个抽象类,除了它的子类,任何人都不能创建或销毁它,这保证了exception不会被误用,其类摘要如下:

  1. class exception
  2. {
  3. protected:
  4.    exception();
  5.    exception(exception const &x);
  6.    virtual ~exception();
  7. private:
  8.    template <class E, class Tag, class T>
  9.    friend E const &operator<<(E const &, error_info<Tag, T> const &);
  10. }
  11.  
  12. typename T* get_error_info(E & x);
  • operator<<函数

    exception类定义了友元函数operator<<,该函数是个自由函数,不是任何类的成员函数。其功能是将error_info<Tag, T>的对象输入到exception对象中。

  • get_error_info函数

    该函数也是自由函数,其功能是从exception类型的E对象中获得写入的error_info<Tag, T>对象值,注意是T类型值,不是输入的error_info<Tag, T>对象。

2.1.2 error_info类

      error_info类是exception对象的信息存储体,即boost的异常处理信息是以该结构为介质。

  1. template <class Tag, class T>
  2. class error_info
  3. {
  4. public:
  5.    typedef T value_type;
  6.    error_info(value_type const &v);
  7.    value_type &value();
  8. }
  • error_info类的T类型是存储信息的类型;
  • error_info类的Tag类型是区分不同error_info类型的标识;
  • error_info类有一个构造函数,从而通过传递T类型的数组进行对象创建;
  • 在boost::exception对象中可以存储多个error_info对象,但是每种类型的error_info对象只能存放一个,而error_info的类型是由Tag来区分。  

2.2 异常使用

      boost的异常使用非常简单,只需继承boost::exception抽象类,并创建继承的异常对象;接着创建error_info对象;然后将error_info对象输入自定义异常对象中;最后抛出自定义异常对象。

2.2.1 继承异常类

      因为boost::exception被定义为抽象类,因此我们的程序必须定义它的子类才能使用它,同时可以继承std::exception一起工作,从而获得两者的能力。但是C++进行多继承,最好使用虚拟继承的方式。

      通常,继承完成后自定义异常类的实现也就结束了,不需要"画蛇添足"地向它增加成员变化或者成员函数,这些工作都已经由boost::exception完成了。如下所示:

  1. #include <exception>
  2. #include <boost/exception/all.hpp>
  3.  
  4. class myException: virtual public std::exception, virtual public boost::exception
  5. {
  6. public:
  7.    myBootstException();
  8. };

2.2.2 创建信息对象

       由于boost::exception对象需要存储的信息是模板类error_info。因为error_info是一个模板类,所以在创建对象时,需要指定相应的类型;其中用一个struct作为第一个模板参数来标志信息类型,再用第二个模板参数指定信息的数据类型。

由于error_info<>的类型定义较长,为了使用方便起见,通常需要使用typedef。如下所示:

  1. typedef boost::error_info<struct tag_err_no, int> err_no;
  2. typedef boost::error_info<struct tag_err_str, string> err_str;

2.2.3 简单实例

      当发生异常时,可以创建一个自定义异常类,并用<<操作符向它存储error_info类型的任意信息,这些信息可以在任何时候使用get_error_info()函数提取。

如在上述自定义的myException类和宏定义的err_no对象基础上,进行如下的使用:

  1. #include <boost/exception/all.hpp>
  2. using namespace boost;
  3.  
  4. int main()
  5. {
  6. try
  7.    {
  8.         throw myException() << err_no(123456);
  9.    } catch (myException &e)
  10.    {
  11.       cout << *get_error_info< err_no > (e) << endl;
  12.    }
  13. }

输出:

  1. 123456

2.3 错误信息类

      error_info类型是异常对象的存储信息,如上述我们宏定义了err_no和err_str类型。其中boost也提供了若干个预先定义好的错误信息类,这样使得程序员使用起来更轻松:

  1. typedef error_info<struct errinfo_api_function_,char const *> errinfo_api_function;
  2. typedef error_info<struct errinfo_at_line_,int> errinfo_at_line;
  3. typedef error_info<struct errinfo_errno_,int> errinfo_errno;
  4. typedef error_info<struct errinfo_file_handle_,weak_ptr<FILE> > errinfo_file_handle;
  5. typedef error_info<struct errinfo_file_name_,std::string> errinfo_file_name;
  6. typedef error_info<struct errinfo_file_open_mode_,std::string> errinfo_file_open_mode;
  7. typedef error_info<struct errinfo_type_info_name_,std::string> errinfo_type_info_name;

2.4 boost增强功能

2.4.1 包装标准异常

      若要使用boost的异常处理功能,需要继承boost::exception。boost库提供一个模板函数来封装标准异常类std::exception或其子类,从而能够不需手动继承boost::exception类就可使用boost的异常处理功能。该函数为:enable_error_info(T &e),它可以包装类型T,产生一个从boost::exception和T派生的类。

如下是enable_error_info()的用法实例:

  1. #include <boost/execption/all.hpp>
  2. using namespace boost;
  3.  
  4. struct my_err();
  5. int main()
  6. {
  7. try
  8. {
  9.          throw enable_error_info(my_err())<<errinfo_errno(10);
  10. }
  11. catch(boost::exception &e)
  12. {
  13.          cout<<*get_error_info(errrinfo_errno)(e)<<endl;
  14. }
  15. }

2.4.2 包装throw表达式

      C++抛出异常,使用throw表达;而且对原来存在的标准异常std::exception需使用enable_error_info()函数进行封装。为了方便抛出异常,boost库提供了两种方式封装throw表达式:

2.4.2.1 throw_exception()

      该函数用于简化enable_error_info()函数的调用,同时可以代替原始的throw语句来抛出异常。会自动使用enable_error_info()来包装异常对象,而且支持线程安全,比直接使用throw更好,例如:

  1. throw_exception(std::runtime_error("runtime"));
  2. 相当于:
  3. throw ( boost::enable_error_info((std::runtime_error("runtime")) )

2.4.2.2 BOOST_THROW_EXCEPTION()

      在throw_exceptio()函数的基础上,boost又提供这个非常有用的宏BOOST_THROW_EXCEPTION(),它调用了boost::throw_exception()和enable_error_info(),因而可以接受任意的异常类型,同时又使用throw_function、throw_file和throw_line自动向异常添加了发送异常的函数名、文件名和行号等信息。

2.4.3 增强get_error_info函数

      boost::exception提供了方便存储信息的能力,可以向它添加任意数量的信息,但当异常对象被用operator<<多次追加数据时,会导致它存储有大量的信息。如果还是采用自由函数get_error_info()来逐项检索的话可能会很麻烦甚至不可能。这时boost库提供另一个函数:diagnostic_information()。

diagnostic_information()可以输出异常对象包含的所有信息,如果异常是由宏BOOST_THROW_EXCEPTION抛出的,则可能相当多并且不是用户友好的,但对于程序开发者可以提供很好的诊断错误的信息。

如下是BOOST_THROW_EXCEPTION配合diagnostic_information一起使用的示例:

  1. void boostException()
  2. {
  3.    try
  4.    {
  5.       BOOST_THROW_EXCEPTION( myBootstException () <<errinfo_api_function("api_function") );
  6.    } catch (myBootstException &e)
  7.    {
  8.       cout << diagnostic_information(e) << endl;
  9.    }
  10. }

输出:

  1. ../src/boost.cpp(45): Throw in function void boostException()
  2. Dynamic exception type: boost::exception_detail::clone_impl<myBootstException>
  3. std::exception::what: std::exception
  4. [boost::errinfo_api_function_*] = api_function

 

posted @ 2016-04-23 09:40  xiuneng  阅读(607)  评论(0)    收藏  举报