深入理解C++异常

异常是指程序在运行时存在异常行为,这些异常的行为让函数不能正常执行。异常应该捕获的应该是你能够处理的错误,比如:不能连接服务器,不能连接数据库等,但异常不应该是你修复代码的bug的手段,比如:数组访问越界。死锁等情况。在异常处理中,你需要的做的是尽可能的修补你错误,比如:释放内存。解锁互斥量。

c++的异常包括下面的三个部分:
1.    throw子句:throw 子句用于抛出异常,被抛出的异常可以是C++的内置类型(例如: throw int(1);),也可以是自定义类型。
2.    catch子句:每个catch子句都代表着一种异常的处理。catch子句用于处理特定类型的异常。
3.    try区段:这个区段中包含了可能发生异常的代码,在发生了异常之后,需要通过throw抛出。

编写异常:
通常的形式是:
try {
    // do something
    throw exception;
} catch (exception declaration) {
    //  excepetion handling code 
    // throw or no
} catch (exception declaration) {
    //  another excepetion handling code 
    // throw or no
}


示例:
//  定义自己的异常类,public 继承C++标准库的exception
class myexception : public std::exception {
public :
  myexception(std::string s) : exception(s.c_str()) {

  }
  virtual const char *what() const {
    return exception::what();
  }
};

void Func() {
  try{
    int *p = NULL;
    if (p == NULL) {
      throw myexception("p is NULL");
    }
  }
  catch(myexception e) {
    printf("%s\n", e.what());
    throw;    //重新抛出异常,异常类型为myexception
  }
  catch (std::exception e) {
    printf("%s", e.what());
  }
}



在try的语句块内声明的变量在外部是不可以访问的,即使是在catch子句内也不可以访问。

寻找异常处理(exception handling)代码:
当一个exception被抛出的时候,控制权会从函数调用中释放出来,并需找一个可以处理的catch子句。对于一个抛出异常的try区段,程序会先检查与该try区段关联的catch子句,如果找到了匹配的catch子句,就使用这个catch子句处理这个异常。
否则,如果这个try区段嵌套在其他try区段中,则继续检查与外层try匹配的catch子句。如果仍然没有找到匹配的catch子句,则退出当前这个主调函数,并在调用了刚刚退出的这个函数的其他函数中寻找。这个过程就是所谓的栈展开,栈展开会沿着嵌套函数的调用链不断查找,知道找到了已抛出的异常匹配的catch子句。如果在最后还是没有找到对应的catch子句的话,则退出主函数后查找过程终止,程序调用标准函数库的terminate()函数,终止该程序的执行。

catch子句的查找:
catch子句是按照出现的顺序进行匹配的(以上面的代码为例,异常先会匹配catch(myexception e)子句,然后在匹配  catch (std::exception e)子句,一步一步的栈展开)。在寻找catch子句的过程中,抛出的异常可以进行类型转换,但是比较严格:
1.    允许从非常量转换到常量的类型转换。
2.    允许从派生类到基类的转换。
3.    允许数组被转换成为指向数组(元素)类型的指针,函数被转换成指向该函数类型的指针。
4.    标准算术类型的转换(比如:把bool型和char型转换成int型)和类类型转换(使用类的类型转换运算符和转换构造函数)。

如果你想一次性捕获全部的异常的话,C++也提供了一种简便的做法,在catch子句的异常声明中使用省略号来作为异常声明:
void Func() {
    try {
        //  your code
    }
    catch(...) {
        // special operation for handling exception
        throw;
    }
}



决定一个throw是否发生在发生在try区段中,我们修改一下上面的例子,新增两个类A和类B :
class myexception : public std::exception {
public :
  myexception(std::string s) : exception(s.c_str()) {

  }
  virtual const char *what() const {
    return exception::what();
  }
};

class A {
public :
  ~A() {
    printf("A's destructor\n");
  }

};

class B {
public :
  ~B() {
    printf("B's destructor\n");
  }

};

void Func() {
  A a;
  throw myexception("p is NULL");
  try{
    int *p = NULL;
    if (p == NULL) {
      //throw myexception("p is NULL");
    }
  }
  catch(myexception e) {
    printf("%s\n", e.what());
    throw;
  }
  catch (std::exception e) {
    printf("%s", e.what());
  }

  B b;
}


void Func2() {
  Func();
}



我们可以很明显的看到,  throw myexception("p is NULL")子句出现在了try区段之外,而注释了的  throw myexception("p is NULL")在try区段内。那么编译器是如何知道,一个throw子句在不在try区段之内的呢?这个做法C++并没有规定,但是有一个很好的做法就是使用program couter-range表格。program couter(程序计数器)中存放着下一个即将执行的指令。为了标志出某一段代码是否在try区段内,可以讲程序计数器的起始值和结束值存放在一个表格中。当一个异常被throw时,我们可以比较当前的程序计数器的值是否在对应try语句的范围表格中,已决定这个throw子句是否在try区段中。如果异常不能在主调函数中被处理(或者catch之后继续抛出),程序计数器就会被设置为调用端地址,然后继续比较对应的try区段范围表格,以此往复,知道终止程序(调用terminate()函数)。

关于活动的local object:
在上面的例子,我们可以看到,在throw myexception("p is NULL")子句出现之前,a对象已经被创建在函数的堆栈中,因此它是一个活动的local object,但是b对象还没有被创建,因此b不是活动的local object。在栈展开的过程中,a对象将会被自动销毁(调用析构函数,如果是内置类型的话则什么都不做)。有一点需要注意的是,如果你是动态分配的内存,这个动态分配的内存是不会被释放的。

从上面的信息可以得出,下面这个流程图:
深入理解C++异常_第1张图片

从函数堆栈的角度来看待栈展开的行为的话。还是一上面的代码为例,我们可以知道,在调用函数Func的过程中,a对象在throw子句之前,所以,在调用throw之前,程序会在Func1对应的堆栈上调用类A的构造函数,建立a对象。在调用throw之后,因为此时a对象已经在堆栈中了,而throw子句并没有与之关联的catch子句处理,因此,对象a在堆栈中的空间被释放,异常传播到Func2 中。

异常对象在发生异常而被抛出的过程中,异常对象的行为:
    异常对象是一种特殊的对象,之所以说它特殊,不是因为它的类型特殊,而是因为它所存在的空间特殊。异常对象位于编译器管理的空间中,编译器确保无论最终调用的是哪个catch子句都能访问这个空间。
    当抛出异常时, 编译器使用异常抛出表达式来对异常对象进行拷贝初始化,因此,throw语句中的表达式必须要用完整的类型(也即是要看到这个类型完整的定义,而不可以是前置声明这类声明),而且如果这个类型是类类型的话,必须还要有一个可以访问的析构函数和复制构造函数(或者移动构造函数);如果是数组类型,则表达式会被转换成为与之对应的指针类型(指针退化)。
    当执行到抛出异常的语句(也就是throw子句), 如果throw子句是在try区段或者是不在try区段内,也不在catch子句被的话,这个异常对象会被复制一次,无论在匹配到的catch子句中的异常声明是否是引用类型。此时,被复制的对象会被加入到编译器的特殊空间中。如果这个时候catch子句继续抛出异常,那么就会有两种情况需要考虑:
    1.匹配到的是引用类型:编译器不会生成一个副本,而是直接将catch子句中的引用绑定到上一次throw子句中的对象。
    2. 匹配到的不是引用类型:编译器将会产生一个副本,而这个副本就是catch子句中声明的那个变量。
考虑下面一段代码:
 
#include <cstdio>
#include <cstdlib>
#include <exception>
#include <string>

class myexception : public std::exception {
public :
  myexception(std::string s) : exception(s.c_str()) {

  }
  virtual ~myexception() {
    printf("myexception's destructor\n");
  }
  virtual const char *what() const {
    return exception::what();
  }
public :
  std::string _s;
};

class A {
public :
  ~A() {
    printf("A's destructor\n");
  }

};

class B {
public :
  ~B() {
    printf("B's destructor\n");
  }

};

void Func() {
  A a;
   printf("dasd\n");
   myexception e("p is NULL");
   e._s = "haha";
  try{
    int *p = NULL;
    if (p == NULL) {
      throw e;
    }
  }
  catch(myexception &e) {
    e._s = "heiheihei";
    printf("%s\n", e.what());
    throw;
  }

  printf("%s\n", e._s.c_str());

  B b;
}

void Func2() {
  try {
    Func();
  }
  catch (myexception &e) {
    printf("Func2 : %s\n", e._s.c_str());
    throw;
  }
}

int main() {
  try {
    Func2();
  }
  catch (myexception &e) {
    printf("main: %s\n", e._s.c_str());
  }
  printf ("hahah\n");

  system("PAUSE");
  return 0;
}


输出的是:
深入理解C++异常_第2张图片

我们可以清楚的看到,当执行到Func()函数的try区段中的throw的时候,立即执行了myexception的复制构造函数,然后在查找匹配的catch子句的时候,因为使用的都是引用类型,因此,这个被复制出来的变量不用继续被复制。而这个异常对象会在异常对象会在catch子句评估确定不再抛出异常或者下一个匹配的catch不是引用类型的时候被析构。需要注意的是,如果是指针类型的话,被复制的也只是这个指针,而不是指针所指的对象。因此,如果你返回一个指向局部对象的指针,在这个局部对象被析构之后解引用这个指针的话,是会出错的。

最后,加上一个C++标准库的异常类型结构图:

深入理解C++异常_第3张图片

异常类型

说明

exception

最常见的问题

runtime_error

只有在运行时才能检查的错误

range_error

运行时错误:生成的结果超出了有意义的范围

overflow_error

运行时错误:计算上溢

underflow_error

运行时错误:计算下溢

logic_error

程序逻辑错误

domain_error

逻辑错误:参数对应的结果值不存在

invalid_error

逻辑错误:无效参数

length_error

逻辑错误:试图创建一个超出该类型最大长度的对象

out_of_range

逻辑错误:使用一个超出有效范围的值



你可能感兴趣的:(C++,异常,std)