C++异常处理须知

来源:互联网 发布:js禁用按钮 编辑:程序博客网 时间:2024/06/09 22:18
http://blog.csdn.net/hikaliv/archive/2009/05/24/4212864.aspx 

帖子内容: 

http://blog.csdn.net/CARL_SEN/archive/2009/05/04/4148426.aspx


第一部分:

1. 异常发生时,异常对象会沿函数调用栈的反方向抛出,这个过程常称为栈展开(堆栈解退)。

2. 在栈展开过程中,如果异常对象始终都没遇到可行的 catch 处理块,系统将调用 terminate 函数强制终止程序。当然如果连 try 块都没有,系统将直接调用 terminate 函数。

3. 在栈展开过程中,编译器保证适当的撤销局部对象。每个函数在栈展开退出时,它的局部存储会释放,如果局部对象是类类型,则自动调用对象的析构函数

4. 析构函数应该从不抛出异常,因为析构函数都是自动调用的,不会自动加上 try 测试块,因此析构函数中异常的抛出将直接导致系统调用 terminate 强制退出。在实践中,由于析构函数释放资源,不太可能出现异常,此外标准库类型都保证它们的析构函数不会引发异常。

5. 如果在构造函数中发生异常,则该对象可能只是部分被构造,即使对象只是部分被构造,也要保证将会适当的撤销已构造的成员。

6. 不能不处理异常,异常是足够重要的,使程序不能按正常情况执行的正常事件。不去捕获异常将直接导致程序的强制终止。

7. catch 子句接收的异常类型可以是内置类型,也可以是类类型,也就是说我们可以抛出(throw)一个如 int 的一般类型作为异常对象。

8. 如果 catch 子句只需了解异常的类型,则可以省去形参名,像这样:catch(runtime_error) {cout<<"runtime error"<<endl;}。当然,如果需要详细信息必须用形参名来访问异常对象:catch(runtime_error e) {cout<<e.what()<<endl;}。

9. 使用异常处理,我们能将问题的检测和问题的解决分离。程序的一部分检测到的问题可以简单的将其抛出(throw),检测部分可以不必了解问题如何处理;程序的另一部分(指调用可能抛出异常的模块实现具体功能的部分)则可以通过 try-catch 捕获异常,然后处理,这样把异常的处理留给具体功能实现时不失为一个最佳的选择。

10. 异常处理中,检测部分抛出对象的类型决定了哪个处理部分的代码被激活,被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个。

11. 异常对象与 catch 进行匹配的规则很严格,一般除了以下几种情况外,异常类型必须与 catch 的说明类型完全匹配:允许非const到const的转换,允许派生类到基类的转换,将数组和函数类型转换为对应的指针。

12. 异常类型匹配将选择第一个找到的可以处理该异常的 catch,因此 catch 子句列表中,最特殊的 catch 必须最先出现。而带有因继承而相关的类型的多个 catch 子句,必须将派生类的处理代码放在基类类型处理代码之前。

13. throw 表达式抛出的异常对象不同于一般的局部对象,局部对象会在局部模块退出时撤销,而异常对象由编译器管理,而且保证驻留在可能被激活的任意 catch 都可以访问到的空间中。这个由编译器管理的异常对象由 throw 表达式创建,并被初始化为被抛出表达式的副本,异常对象将传给对应的 catch 并在完全处理后才撤销

14. 由于异常抛出时都进行了一次副本拷贝,因此异常对象必须是可以复制的。

15. 抛出一个表达式时,被抛出对象的静态编译时类型将决定异常对象的类型。

16. 抛出指针通常是一个主意,因为抛出指针要求在对应处理代码存在的任意地方都存在指针所指向的对象(注意此时 throw 抛出时复制的是指针本身,不会去复制指针指向的内容);而且如果该指针是指向派生类对象的基类指针,则那个对象将被分割只抛出基类部分(第 15 条中的静态类型规则)。

17. 基类异常对象可以用于捕获派生类的异常对象,因此如果 catch 子句处理因继承而相关的类型,它就应该将自己的形参定义为引用来激活运行时调用的多态性。

18. catch 可以继续将捕获到的异常抛出,它使用不带表达式的 throw 语句重新将异常抛出,如:throw;被重新抛出的异常对象是原来的异常对象,与 catch 的形参无关(如原来抛出的是派生类 Deriver,catch 形参是基类 Base,则重新抛出后的异常类型是 Deriver),当然如果 catch 形参是引用的话,原来的异常对象可能已被catch修改了。

19. 可以用 catch(...){} 来捕获所有的异常,catch(...){} 经常与重新抛出表达式结合使用,catch(...) 完成可做的所有局部工作,然后重新抛出异常。

20. 构造函数包括初始化列表的异常处理:

  1. Foo::Foo(int n)    
  2. try:size(n), array(new int[n])    
  3. {    
  4.       //...    
  5. }    
  6. catch(const bad_alloc& e)    
  7. {    
  8.       //...    
  9. }   

这里的函数测试块将初始化列表和函数体中的代码都纳入 try 块中。


第二部分:

1. 标准异常类定义在四个头文件中:exception,new,type_info,stdexcept。

2. exception 中定义了 exception 类,new 中定义了 bad_alloc 类,type_info 中定义了 bad_cast 类,stdexcept 中定义了 runtime_error、logic_error 类。

3. runtime_error 类(表示运行时才能检测到的异常)包含了 overflow_error、underflow_error、range_error 几个子类;logic_error 类(一般的逻辑异常)包含了 domain_error、invalid_argument、out_of_range、length_error 几个子类;而所有的这些类都是 exception 类的子类。

4. exception、bad_alloc、bad_cast 类只定义了默认构造函数,无法在创建这些异常的时候提供附加信息。其它异常类则只定义了一个接受字符串的构造函数,字符串初始化式用于为所发生的异常提供更多的信息。

5. 所有异常类都有一个 what 虚函数,它返回一个指向 C 风格字符串的指针。

6. 应用程序可以从 exception 或者中间基类派生自已的异常类来扩充 exception 类层次。

7. 异常说明跟在函数形参表之后,一个异常说明在关键字 throw 之后跟着一个由圆括号括住的异常类型表(由逗号分隔),如:void foo(int) throw(bad_alloc, invalid_argument);。异常列表还可以为空:void foo(int) throw();,表示该函数不抛出任何异常。

8. 异常说明有用的一种重要情况是,如果函数可以保证不会抛出异常。确定函数将不抛出任何异常,对函数的使用者和对编译器都是非常有用的。知道函数不抛出异常会简化编写该函数异常安全的代码工作,而编译器则可以执行被抛出异常抑制的代码优化。

9. 标准异常类中的析构函数和 what 虚函数都承诺不抛出异常,如what的完整声明为:virtual const char* what() const throw();。

10. 派生类中的虚函数不能抛出基类虚函数中没有声明的新异常,这样在编写代码时才有一个可依赖的事实:基类中的异常列表是虚函数的派生类版本可以抛出的异常列表的超集

11. 上半部分说过,在异常抛出栈展开的时候,编译器会适当撤销函数退出前分配的局部空间,如果局部对象是类类型,则自动调用它的析构函数。但如果在函数内单独地使用 new 动态的分配了内存,而且在释放资源之前发生了异常,那么栈展开时这个动态空间将不会被释放。而由类类型对象分配的资源不管是静态的还是动态的一般都会适当的被释放,因为栈展开时保证调用它们的析构函数。因此,在可能存在异常的程序以及分配资源的程序最好使用类来管理那些资源,看一个例子:

  1. void f()    
  2. {    
  3.     const int N=10;    
  4.     int* p=new int[N];    
  5.     if(...)    
  6.     {    
  7.         throw exception;     
  8.     }    
  9.     delete[] p;    
  10. }   

当这个异常发生时,p 指向的动态空间将不会被正常撤销。现在我们用类来管理这个资源:

  1. template <typename T>    
  2. class Resource    
  3. {    
  4. private:    
  5.     unsigned int size;    
  6.     T* data;    
  7. public:    
  8.     Resource(unsigned int _size=0):size(_size),data(new T[_size])    
  9.     {    
  10.         for(unsigned int i=0; i<size; ++i) data[i]=T();    
  11.     }    
  12.     Resource(const Resource& r):size(r.size),data(new T[r.size])    
  13.     {    
  14.         for(unsigned int i=0; i<size; ++i) data[i]=r.data[i];    
  15.     }    
  16.     Resource& operator=(const Resource& r)    
  17.     {    
  18.         if(&r!=this)    
  19.         {    
  20.             size=r.size;    
  21.             delete[] data;    
  22.             data=new T[size];    
  23.             for(int i=0; i<size; ++i) data[i]=r.data[i];    
  24.         }    
  25.         return *this;    
  26.     }    
  27.     ~Resource() { delete[] data; }    
  28.     T operator[](unsigned int index);    
  29.     const T operator[](unsigned int index) const;    
  30. };    
  31. void f()    
  32. {    
  33.     const int N=10;    
  34.     Resource<int> p(N);    
  35.     if(...)    
  36.     {    
  37.         throw exception;     
  38.     }    
  39. }   

这里即使抛出了异常,也会自动调用对象 p 的析构函数。

12. 异常抛出栈展开的时候,编译器对局部类对象析构函数的自动运行导致了一个重要的编程技巧的出现,它使程序更为异常安全的。通过定义一个类来封装资源的分配和释放,可以保证正确的释放资源。这一技术常称为“资源分配即初始化”,简称为 RAII。第 11 条已给出了它的用法。


更多参考:

 

C++ 类构造函数初始化列表的异常机制 function-try block

续:为何说 C++ 构造函数初始化列表异常机制是必要的

C++ try 块里 new 类对像构造异常时发生“回退”并对资源自动释放

原创粉丝点击