C++异常处理

jopen 10年前

问题

  • 抛出异常时发生了什么?

解答


#include <stdio.h>  #include <string.h>    #define _mNT    throw()    class Exce{   char exceName[16];  public:   Exce(const char *__name)_mNT{    strcpy(exceName,__name);    printf("Exce: %s\n",exceName);   }     ~Exce()_mNT{ printf("Exce: ~%s\n",exceName); }     Exce(const Exce & __exce)_mNT{    exceName[0]='_';exceName[1]='\0';    strcat(exceName,__exce.exceName);    printf("Exce: %s -> %s\n",      __exce.exceName,      this->exceName);   }    };    void exce1()throw(Exce){   Exce exce11("exce1");   throw exce11;   return ;  }    void exce2()throw(Exce){   Exce exce22("exce2");   try{ exce1(); }   catch(int __e){ ; }   return ;  }    void exce3()throw(Exce){   Exce exce33("exce3");   try{ exce2(); }   catch(const Exce &__e){ ; }   return ;  }    int main(int argc,char *argv[]){   exce3();   return 0;  }
运行程序:
Exce: exce3  Exce: exce2  Exce: exce1  Exce: exce1 -> _exce1     Exce: ~exce1  /* throw表达式没有处在try块中,所以执行堆栈展开 */  Exce: ~exce2  /* 虽然处在try快中但是没有匹配的catch子句,所以继续执行堆栈展开 */  Exce: ~_exce1  /* 找到匹配的catch子句,执行完毕后.处在全局区的异常对象会被释放 */  Exce: ~exce3
所以,抛出异常(执行throw语句)时发生的事情:
  1. 在全局区创建异常对象的副本(对于类类型,调用复制构造函数,所以用作异常的类型复制构造函数必须可用) 即上方的'Exce: exce1 -> _exce1';
  2. 如果throw表达式没有处在try块中或者没有匹配的catch子句,则执行堆栈展开:
    • 释放函数所占的内存空间
    • 对于类类型的局部对象调用她们的析钩函数(所以析钩函数应该不抛出任何异常)来清理对象
  3. 否则执行catch子句,并且一般情况下执行完catch子句后,处在全局区的异常对象会被释放(除非catch子句中使用了重新抛出'throw;)

抛出时对指针解引用


#include <stdio.h>    class A{  public:   A(){ printf("A\n"); }   A(const A &__a){ printf("A -> "); }   virtual ~A(){ ; }  };    class B:public A{  public:   B():A(){ printf("B\n"); }   B(const B &__b):A(){ printf("B ->"); }   virtual ~B(){ ; }  };    int main(int argc,char *argv[]){   B b;   A *a=&b;   try{    throw *a;       }catch(const B &__b){    printf("Catch: B\n");   }catch(const A &__a){    printf("Catch: A\n");   }     return 0;  }
throw a;对a解引用,无论指针指向的实际类型是什么, 抛出的异常对象(也即在全局区创建的异常对象类型)总与指针的静态类型相匹配
所以 catch(const A &a) 匹配


匹配catch子句

一般情况下,异常对象必须与catch子句的形参类型完全匹配才会进入相应的catch子句中,除了下列三种情况:

  • 允许非const到const的转换,非const对象的throw可以与指定接受const引用的catch子句匹配;
  • 允许派生类型到基类型的转换
  • 允许数组转换为数组类型的指针,函数转换为函数类型的指针

重新抛出

当catch子句中使用了重新抛出时,处在全局区的异常对象不会被释放,如:


#include <stdio.h>    struct A{      int a;      A():a(0){ printf("A:%p\n",this); }      A(const A &a){ printf("A:%p->%p\n",&a,this); }      ~A(){ printf("A:~%p\n",this); }  };    void f(){      A a1;      throw a1;      return ;  }    void f1(){      try{ f(); }      catch(A &a1){ ++__a1.a; throw; }      / throw与throw a1是不同 */      return ;  }    void f2(){      try{ f1(); }      catch(A &a){ printf("%d\n",a.a); }  }    int main(int argc,char *argv[]){      f2();      return 0;  }</pre> 
  • throw;:重新抛出,是将全局区的异常对象继续沿着函数调用链向上传递;不会释放该异常对象;
  • throw a1:此时根据a1重新在全局区创建一个新的异常对象,然后将处在全局区的a1释放;然后在堆栈展开...balabala </li> </ul>

    匹配所有类型

    catch(...){};匹配所有类型的异常对象,如果'catch(...)'处在catch子句的第一位,那么其他catch是不会得到机会的;

    函数测试块

    用于捕获构造函数初始化列表中的异常
    不过测试发现:会在捕获处理后将初始化列表中发生的异常重新抛出('throw;'那种)


    #include <stdio.h>    struct A{   int a;   A():a(0){ printf("A:%p\n",this); }   A(const A &__a){ printf("A:%p->%p\n",&__a,this); }   ~A(){ printf("A:~%p\n",this); }  };    int f(){   A a1;   throw a1;   return 0;  }    class B{   int a;  public:   B()try:a(f()){/* 注意try的位置,初始化列表之前 */    ;   }catch(...){/* 会捕获初始化列表与构造函数体中抛出的异常,不过在处理后又会重新抛出 */    printf("B:Catch\n");   }  };      int main(int argc,char *argv[]){   try{ B b; }   catch(...){ printf("main:Catch\n"); }   return 0;  }    /* 执行结果: */  A:0x7fffc45fbae0  A:0x7fffc45fbae0->0x1afd090  A:~0x7fffc45fbae0  B:Catch  main:Catch      A:~0x1afd090 /* 确定是重新抛出 */    

    RAII

    资源分配即初始化,即通过一个类来包装资源的分配与释放,这样可以保证异常发生时资源会被释放

    异常说明


    void f()throw(Type)  /* f 会抛出Type类型或其派生类型的异常 */  void f()throw()  /* f()不会抛出任何异常,此时编译器可能会执行一些被可能抛出异常的代码抑制的优化 */  void f()  /* f()会抛出任何类型的异常 */


    违反了异常说明

    如果抛出了不在异常说明列表中的异常,则会执行堆栈展开退出当前函数后直接调用标准库函数 unexcepted()[默认调用 terminate()终止程序 ] ;而不会沿着函数调用链向上...如:


    #include <stdio.h>    struct A{   int a;   A():a(0){ printf("A:%p\n",this); }   A(const A &__a){ printf("A:%p->%p\n",&__a,this); }   ~A(){ printf("A:~%p\n",this); }  };    int f()throw(){   A a1;   throw a1;   return 0;  }    void f1(){   A a2;   f();   return ;  }      int main(int argc,char *argv[]){   try{ f1(); }   catch(...){ printf("main:Catch\n"); }   return 0;  }

    继承层次中的异常说明

    派生类虚函数异常说明中的异常列表⊆基类虚函数异常说明中的异常列表,这个主要是为了:

    当通过基类指针调用派生类虚函数,这条限制可以保证派生类虚函数不会抛出新的异常
    #include <stdio.h>    class A1{ virtual ~A1(){ ; } };  class B1:public A1{  };    class A{  public:   virtual void print()throw(A1){    return ;   }   virtual ~A(){ ; }  };    class B:public A{  public:   virtual void print()throw(B1){    return ;   }   virtual ~B(){ ; }  };    int main(int argc,char *argv[]){   B b; b.print();   return 0;  }
    只要满足批注中的条件即可,如上例也编译通过


    异常说明与析钩函数


    class A{  public:   virtual void print()throw(A1){    return ;   }   virtual ~A()throw(){ ; }  };    class B:public A{  public:   virtual void print()throw(B1){    return ;   }   virtual ~B(){ ; }  };
    因为 ~A() 不会抛出任何类型的异常,所以 ~B() 也不能抛出任何类型的异常,如上例编译不会通过;


    异常说明与函数指针


    int (*fptr)()throw(int,double);  /* fptr作为一个函数指针,指向着一个函数:   * 该函数没有参数,返回类型为int   * 并且可能抛出int,double类型的异常 */  int (*fptr1)()throw();


    当给函数指针赋值的时候,源指针异常声明的类型列表⊆目的指针异常声明的类型列表,这样主要是为了保证:

    当通过目的指针调用函数时,函数抛出的异常不会多于目的函数指针异常列表中的异常
    但实际上,下列代码编译成功了:


    #include <stdio.h>      int f()throw(int,double){   throw 1;   return 0;  }    int main(int argc,char *argv[]){   int (*fptr1)()throw();   fptr1=f;/* 这里赋值应该是失败的... */   try{ fptr1(); }      /* fptr1的异常说明不会抛出任何异常,所以这里抛出异常时应该是       * 调用unexpected()的;但实际上异常被捕获了 */   catch(...){ ; }   return 0;  }