C++异常处理详解

C++异常处理

异常处理机制使得我们将问题的检测与处理过程分开。

异常:运行时存在的反常行为。

程序的一部分负责检查,另一部分负责解决问题。

C++的异常处理包括:

  • throw表达式:throw引发了异常
  • try语句块:try关键字开始,一个或者多个catch自居结束,try中抛出的异常会被某个catch处理。catch处理异常也被称作异常处理代码。
  • 一套异常类:throw和catch之间传递具体信息

1、throw表达式

C++中通过throw一个表达式来引发一个异常。

throw表示是包含关键字throw和紧随其后的一个表达式,表达式的类型就是抛出的异常类型。

在之前,我们处理异常的时候通常在出错的地方打印出错然后return -1表示结束,但是这样并没有做到异常的检测和处理的分离,我们进行如下处理:

//首先检查异常是否存在
if(item1.isbn()!=item2.isbn())
    throw runtime_error("Data must be refer to the same ISBN");
//如果执行到这里,表示两个isbn是相同的
cout<<item1+item2<<endl;

runtime_error是标准库异常类型的一种。初始化这类型的对象(用一个string或者是C字符串)即可初始化。

2、try语句块

try语句块的通常写法是:

try{
	//program-statements
}catch(//exception-declaration){
	//handler-statements
}catch(//exception-declaration){
	//handler-statements
}

catch子句通常由三部分组成:

  • catch关键字
  • 声明的异常对象,称为异常声明
  • 一个处理问题的块

catch的某一块执行完成后转跳到try语句最后一个catch子句之后的那条语句继续执行。

try语句块中的program-statements组成正常的逻辑,像其他任何块一样可以有任何的正确的C++语句。同时,和其他快一样,块内的局部变量在块外无法访问。

while (cin >> item1 >> item2) {
try {
    //执行添加两个Sales_ item对象的代码
    //如果添加失败,代码抛出一个runtime_ error异常
} catch (runtime_ error err) {
	//提醒用户两个ISBN必须一致,询问是否重新输入
	cout << err.what ()<< " \nTry Again? Enter y or n" << endl ;
    char C;
    cin >> C;
    if(!cin|C=='n')
    	break; //跳出while循环
}

标准库的每一个异常类都定义了名为what的成员函数,这些函数没有参数,返回值是C风格的字符串。

3、抛出异常

throw一个异常之后跟在throw后面的语句将不再被执行。程序的控制权由throw转到了catch。或者是直接或者间接调用发生异常函数的另一个函数中。

3.1栈展开

当throw语句出现在一个try语句块中,检查该try语句块中有无管理链的catch子句。

如果能找到匹配的catch子句,就使用该catch子句处理异常。

如果没有找到,且该try嵌套在其他的try语句块中,那么继续检查外层try匹配的catch子句。如果还是找不到匹配的catch,那么就退出当前的函数,在该层函数的外层进行寻找。

这个过程被称作栈展开

找到处理的catch之后进入处理阶段,处理完成后,找到与try块关联的最后一个catch子句之后的点,并从这里继续执行。

如果没有找到处理的catch子句,程序将退出。程序调用标准库函数terminate中止程序执行。

3.2栈展开的过程中对象被自动销毁

块退出后,块内的对象也会被自动销毁。

3.3析构与异常

使用类来控制资源的分配,那么就能确保无论函数正常结束还是遭遇异常,资源都能被正确释放。

且析构函数不应该抛出异常:如果抛出了异常但是析构函数自身没有捕获到该异常,那么程序将被终止。

3.4异常对象

在上述栈展开的过程中,异常对象的销毁不会进行——异常对象位于编译器管理的空间中,编译器确保无论调用那个catch子句都能访问该空间。在异常处理完毕后,异常对象被销毁。

注意:

  • 抛出一个局部对象的指针是一个绝对错误的行为

  • 从函数中返回的时候也不能返回局部对象的指针

  • 抛出异常时,该表达式的静态编译时的类型决定了异常对象的类型。

    throw表达式解引用一个基类指针,而该指针指向的其实是一个派生类对象,则抛出异常的部分将会被切掉一部分,只有基类部分被抛出。

4、捕获异常

catch子句中的异常声明看起来像是一个只包含一个形参的函数形参列表。如果catch无需访问抛出表达式的话,那么我们可以忽略捕获形参的名字。

当进入一个catch子句后,通过异常对象初始化异常声明中的参数。

这一点和函数的参数列表也相似:

  • 如果catch的参数类型是非引用类型,则该参数是异常对象的一一个副本,在catch
    语句内改变该参数实际上改变的是局部副本而非异常对象本身;
  • 相反,如果参数是引用类型,则和其他引用参数-一样,该参数是异常对象的一 个别名,此时改变参数也就是改变异常对象。

此外,如果catch的参数是基类类型,我们可以使用派生类类型对其初始化

此时如果参数类型是非引用类型,那么一场对象将会被切掉一部分。这与派生类传值给一个普通函数差不多。

特别注意:!!!

  • 异常声明的静态类型将会决定所能执行的操作。

    如果catch的类型是基类类型,那么catch无法使用派生类特有的任何成员!

4.1查找匹配的处理代码

查询到的catch子句不一定是异常的最佳匹配,而是第一个与异常匹配的catch语句。

catch子句是注意匹配的,所以越是专门的catch语句应该置于catch子句组的前端。

4.2重新抛出

当一个单独的catch不能完整处理某个异常,在执行了校正操作后,catch子句可能会调用链更上一层的函数接着处理异常。一条catch语句通过重新抛出的操作将一个异常传递给另一个catch语句。重新抛出的语句仍是一个throw语句,只不过不包含任何的表达式:

throw;

这个语句只能出现在catch语句或者catch语句直接或间接调用的函数之内。

如果在处理代码之外的区域遇到了空throw语句,那么编译器将调用terminate

很多时候,catch语句会改变其参数的内容。如果在改变了参数的内容后catch语句重新抛出异常,则只有当catch异常声明是引用类型时我们对参数所做的改变才会被保留并继续传播:

catch (my_error &e0bj) {// 引用类型
    e0bj.status = errCodes: :severeErr; // 修改了异常对象
    throw;// 异常对象的 status成员是severeErr
} catch (other_ error e0bj ){//非引用类型.
    e0bj .status = errCodes::badErr; // 只修改了异常对象的局部副本
    throw; //异常对象的status成员没有改变
}

4.3捕获所有异常的代码

在不知道异常类型时,一次性捕获所有异常的代码catch(...)。这个语句称为catch-all,一条捕获语句可以与任意类型的异常匹配。

catch(...)常常与throw;联合使用,catch执行局部能完成的操作,随后重新抛出异常。

void manip(){
	try{
		//引发一个异常
	}
	catch(...){
		//处理某些异常的操作
		throw;
	}
}

catch(...)可以单独出现,也可以与其他的catch语句一起出现。

如果是后者,那么catch(...)必须放在最后的位置。出现在catch(...)后面的语句将永远不会被匹配。

你可能感兴趣的:(C++,c++,编程语言,指针)