C++异常处理机制就可以捕获并处理程序运行过程中出现的错误,接着让程序沿着一条不会出错的路径继续执行,或者不得不结束程序,但在结束前可以做一些必要的工作,例如释放分配的内存等。C++ 异常处理机制会涉及try、catch、throw三个关键字。
1、程序错误分类
程序的错误大致可以分为三种,分别是语法错误、逻辑错误和运行时错误:
①语法错误在编译和链接阶段就能发现,只有100%符合语法规则的代码才能生成可执行程序。语法错误是最容易发现、最容易定位、最容易排除的错误,程序员最不需要担心的就是这种错误。
②逻辑错误是说我们编写的代码思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决。
③运行时错误是指程序在运行期间发生的错误,例如除数为 0、内存分配失败、数组越界、文件不存在等。
C++异常(Exception)机制就是为解决运行时错误而引入的。运行时错误如果放任不管,系统就会执行默认的操作,终止程序运行,即程序崩溃(Crash)。C++提供了异常(Exception)机制,让我们能够捕获运行时错误,给程序一次“起死回生”的机会,或者至少告诉用户发生了什么再终止程序。
2、捕获异常
捕获异常的语法如下:
try
{ // 可能抛出异常的语句 } catch(exceptionType variable) { // 处理异常的语句 }
try
和catch
都是C++中的关键字,后跟语句块,不能省略{ }
。try中包含可能会抛出异常的语句,一旦有异常抛出就会被后面的catch捕获。try只是“检测”语句块有没有异常,如果没有发生异常,它就“检测”不到。catch用来捕获并处理try检测到的异常;如果try语句块没有检测到异常(没有异常抛出),那么就不会执行catch中的语句。catch关键字后面的exceptionType variable
指明了当前catch可以处理的异常类型,以及具体的出错信息。
发生异常时必须将异常明确地抛出,try才能检测到;如果不抛出来,即使有异常try也检测不到。异常一旦抛出,会立刻被try检测到,并且不会再执行异常点(异常发生位置)后面的语句。意思就是:检测到异常后程序的执行流会发生跳转,从异常点跳转到catch所在的位置,位于异常点之后的、并且在当前try块内的语句就都不会再执行;即使catch语句成功地处理了错误,程序的执行流也不会再回退到异常点,所以这些语句永远都没有执行的机会。执行完 catch 块所包含的代码后,程序会继续执行 catch 块后面的代码,就恢复了正常的执行流。
到此可了解异常的的处理流程为:抛出(Throw)--> 检测(Try) --> 捕获(Catch)。
2.1 发生异常的位置
异常可以发生在当前的try块中,也可以发生在try块所调用的某个函数中,或者是所调用的函数又调用了另外的一个函数,这个另外的函数中发生了异常。这些异常,都可以被 try检测到。示例:
try{
throw "Unknown Exception"; //抛出异常
cout<<"This statement will not be executed."<<endl;
}
catch(const char* &e)
{
cout<<e<<endl;
}
throw
关键字用来抛出一个异常,这个异常会被try检测到,进而被catch捕获。发生异常后,程序的执行流会沿着函数的调用链往前回退,直到遇见try才停止。在这个回退过程中,调用链中剩下的代码(所有函数中未被执行的代码)都会被跳过。
3、异常类型以及多级catch匹配
这里主要是关于catch关键字后边的exceptionType variable值。
exceptionType
是异常类型,它指明了当前的catch可以处理什么类型的异常;variable
是一个变量,用来接收异常信息。当程序抛出异常时,会创建一份数据,这份数据包含了错误信息,程序员可以根据这些信息来判断到底出了什么问题,接下来怎么处理。
异常既然是一份数据,那么就应该有数据类型。C++规定,异常类型可以是int、char、float、bool等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型。C++语言本身以及标准库中的函数抛出的异常,都是exception类或其子类的异常。也就是说,抛出异常时,会创建一个exception类或其子类的对象。
exceptionType variable
和函数的形参非常类似,当异常发生后,会将异常数据传递给variable这个变量,这和函数传参的过程类似。当然只有跟exceptionType类型匹配的异常数据才会被传递给variable,否则catch不会接收这份异常数据,也不会执行catch块中的语句。换句话说catch不会处理当前的异常。可以将catch看做一个没有返回值的函数,当异常发生后catch会被调用,并且会接收实参(异常数据)。
3.1 catch和函数区别
- 真正的函数调用,形参和实参的类型必须要匹配,或者可以自动转换,否则在编译阶段就会报错
- 而对于catch,异常是在运行阶段产生的,它可以是任何类型,没法提前预测,所以不能在编译阶段判断类型是否正确,只能等到程序运行后,真的抛出异常了,再将异常类型和 catch能处理的类型进行匹配,匹配成功的话就“调用”当前的 catch,否则就忽略当前的 catch。
catch和真正的函数调用相比,多了一个「在运行阶段将实参和形参匹配」的过程。
3.2 catch不处理数据
语法如下:
try{
// 可能抛出异常的语句
}
catch(exceptionType)
{
// 处理异常的语句
}
即是将捕获类型的variable 省略掉,这样只会将异常类型和catch所能处理的类型进行匹配,不会传递异常数据。
3.3 多级catch
如下:
try{
//可能抛出异常的语句
}catch (exception_type_1 e){
//处理异常的语句
}catch (exception_type_2 e){
//处理异常的语句
}
//其他的catch
catch (exception_type_n e){
//处理异常的语句
}
当异常发生时,程序会按照从上到下的顺序,将异常类型和catch所能接收的类型逐个匹配。一旦找到类型匹配的catch就停止检索,并将异常交给当前的catch处理(其他的 catch 不会被执行)。如果最终也没有找到匹配的catch,就只能交给系统处理,终止程序的运行。在catch中,可以只给出异常类型,不给出接收异常信息的变量e。catch在匹配异常类型时会发生向上转型。典型的就是:希望捕获派生类的异常但是却被基类提前捕获了。
3.4 catch在匹配过程中的类型转换
C/C++中存在多种多样的类型转换,以普通函数(非模板函数)为例,发生函数调用时,如果实参和形参的类型不是严格匹配,那么会将实参的类型进行适当的转换,以适应形参的类型,这些转换包括:
- 算数转换:int转换为float,char转换为int,double转换为int等;
- 向上转型:也就是派生类向基类的转换;
- const转换:也即将非const类型转换为const类型,如将char *转换为const char *;
- 数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针;
- 用户自定的类型转换。
Catch在匹配异常类型的过程中也会进行类型转换,但只能进行[向上转型]、[const转换]和[数组或函数指针转换],其他的都不能应用于catch。
向上转型示例:
try{ throw Derived(); //抛出自己的异常类型,实际上是创建一个Derived类型的匿名对象 cout<<"This statement will not be executed."<<endl; }catch(Base) { //匹配成功(向上转型) cout<<"Exception type: Base"<<endl; }catch(Derived) { cout<<"Exception type: Derived"<<endl; }
const转换以及数组和指针的转换:
int nums[] = {1, 2, 3}; try{ throw nums; cout<<"This statement will not be executed."<<endl; }catch(const int *) { cout<<"Exception type: const int *"<<endl; }
nums本来的类型是int [3]
,但是catch中没有严格匹配的类型,所以先转换为int *
,再转换为const int *
。
4、throw 抛出异常
上面已经说过,异常必须显示地抛出,才能被检测和捕获到;若没有显示的抛出,即使有异常也检测不到。
用法:
throw exceptionData; //exceptionData异常数据
异常数据可以包含任意的信息,完全是由程序员决定的。exceptionData可以是int、float、bool等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型。
4.1 自定义动态数组异常示例
动态数组,是指数组容量能够在使用的过程中随时增大或减小。
自定义异常类型:
1 class OutOfRange{ 2 public: 3 OutOfRange(): m_flag(1){ }; 4 OutOfRange(int len, int index): m_len(len), m_index(index), m_flag(2){ } 5 public: 6 void what() const; //获取具体的错误信息 7 private: 8 int m_flag; //不同的flag表示不同的错误 9 int m_len; //当前数组的长度 10 int m_index; //当前使用的数组下标 11 }; 12 void OutOfRange::what() const { 13 if(m_flag == 1) 14 { 15 cout<<"Error: empty array, no elements to pop."<<endl; 16 } 17 else if(m_flag == 2) 18 { 19 cout<<"Error: out of range( array length "<<m_len<<", access index "<<m_index<<" )"<<endl; 20 } 21 else 22 { 23 cout<<"Unknown exception."<<endl; 24 } 25 }
动态数组实现:
1 class Array{ 2 public: 3 Array(); 4 ~Array(){ free(m_p); }; 5 public: 6 int operator[](int i) const; //获取数组元素 7 int push(int ele); //在末尾插入数组元素 8 int pop(); //在末尾删除数组元素 9 int length() const{ return m_len; }; //获取数组长度 10 private: 11 int m_len; //数组长度 12 int m_capacity; //当前的内存能容纳多少个元素 13 int *m_p; //内存指针 14 private: 15 static const int m_stepSize = 50; //每次扩容的步长 16 }; 17 Array::Array(){ 18 m_p = (int*)malloc( sizeof(int) * m_stepSize ); 19 m_capacity = m_stepSize; 20 m_len = 0; 21 } 22 int Array::operator[](int index) const 23 { 24 if( index<0 || index>=m_len ) 25 { //判断是否越界 26 throw OutOfRange(m_len, index); //抛出异常(创建一个匿名对象) 27 } 28 return *(m_p + index); 29 } 30 31 int Array::push(int ele) 32 { 33 if(m_len >= m_capacity){ //如果容量不足就扩容 34 m_capacity += m_stepSize; 35 m_p = (int*)realloc( m_p, sizeof(int) * m_capacity ); //扩容 36 } 37 *(m_p + m_len) = ele; 38 m_len++; 39 return m_len-1; 40 } 41 42 int Array::pop() 43 { 44 if(m_len == 0) 45 { 46 throw OutOfRange(); //抛出异常(创建一个匿名对象) 47 } 48 m_len--; 49 return *(m_p + m_len); 50 } 51 52 //打印数组元素 53 void printArray(Array &arr) 54 { 55 int len = arr.length(); 56 //判断数组是否为空 57 if(len == 0) 58 { 59 cout<<"Empty array! No elements to print."<<endl; 60 return; 61 } 62 for(int i=0; i<len; i++) 63 { 64 if(i == len-1) 65 { 66 cout<<arr[i]<<endl; 67 } 68 else 69 { 70 cout<<arr[i]<<", "; 71 } 72 } 73 } 74
Array类实现动态数组的主要思路为:在创建对象时预先分配出一定长度的内存(通过 malloc()分配),内存不够用时就再扩展内存(通过 realloc()重新分配)。Array数组只能在尾部一个一个地插入(通过 push() 插入)或删除(通过 pop()删除)元素。
4.2 throw用作异常的规范
throw关键字除了可以用在函数体重抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,这称为异常规范(Exception specification),也称为异常指示符或异常列表。
用法:
double func (char param) throw (int); double func (char param) throw (int, char, exception); double func (char param) throw (); //不抛出任何异常
声明一个名为func的函数,它的返回值类型为double,有一个char类型的参数,并且只能抛出int/int,char,exception类型的异常。如果抛出其他类型的异常,try将无法捕获,只能终止程序。使用逗号","隔开多种类型的异常。
4.2.1 虚函数中的异常规范
C++规定,派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格,或者更严格。只有这样,当通过基类指针(或者引用)调用派生类虚函数时,才能保证不违背基类成员函数的异常规范。
1 class Base{ 2 public: 3 virtual int fun1(int) throw(); 4 virtual int fun2(int) throw(int); 5 virtual string fun3() throw(int, string); 6 }; 7 class Derived:public Base{ 8 public: 9 int fun1(int) throw(int); //错!异常规范不如 throw() 严格 10 int fun2(int) throw(int); //对!有相同的异常规范 11 string fun3() throw(string); //对!异常规范比 throw(int,string) 更严格 12 }
4.2.2 异常规范与函数定义和函数声明
C++规定,异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。
1 //错!定义中有异常规范,声明中没有 2 void func1(); 3 void func1() throw(int) { } 4 //错!定义和声明中的异常规范不一致 5 void func2() throw(int); 6 void func2() throw(int, bool) { } 7 //对!定义和声明中的异常规范严格一致 8 void func3() throw(float, char*); 9 void func3() throw(float, char*) { }
异常规则的初衷是好的,希望程序员看到函数的定义或声明后,可以知道函数会抛出什么类型的异常,程序员就可以通过try-catch进行异常捕获,不然只能通过阅读源码才能知道函数会抛出的异常类型。但是异常规范的初衷实现有点困难,因此最好不要使用异常规范。异常规范是C++98新增的,但C++11已经抛弃。