C++学习——异常处理

异常处理

异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理。

异常使得我们能够将问题的检测与解决过程分离开来。检测环节无须知道问题处理模块的所有细节,反之亦然。

抛出异常

在C++语言中,通过抛出一条表达式来引发一个异常。
被抛出的表达式的类型以及当前的调用链共同决定了哪段处理代码将被用来处理该异常。

当执行一个 throw 时,跟在 throw 后面的语句将不再被执行。相反,程序的控制权从 throw 转移到与之匹配的 catch 模块。

控制权从一处转移到另一处,这有两个重要的含义:

  • 沿着调用链的函数可能会提早退出。
  • 一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁。

栈展开

当抛出一个异常后,程序暂停当前函数的执行过程并立即开始寻找与异常匹配的 catch 子句。

当 throw 出现在一个 try语句块 内时,检查与该 try 块关联的 catch 子句。
如果找到了匹配的 catch ,就使用该 catch 处理异常。如果这一步没有找到匹配的 catch 且该 try 语句嵌套在其他 try 块中,则继续检查与外层 try 匹配的 catch 子句。如果还是找不到匹配的 catch ,则退出当前的函数,在调用当前函数的外层函数中继续寻址。

如果对抛出异常的函数的调用语句位于一个 try 语句块内,则检查与该 try 块关联的 catch 子句。
如果找到了匹配的 catch ,就使用该 catch 处理异常。否则,如果该 try 语句嵌套在其他 try 块中,则继续检查与外层 try 匹配的 catch 子句。
如果仍没有找到匹配的 catch ,则退出当前这个主调函数,继续在调用了刚刚退出的这个函数的其他函数中寻找,以此类推。

上述过程被称为栈展开过程。

栈展开过程沿着嵌套函数的调用链不断查找,直到找到了与异常匹配的 catch 子句为止;或者也可能一直没找到匹配的 catch ,则退出主函数后查找过程终止。

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

如果栈展开过程中退出了某个块,编译器将负责确保在这个块中创建的对象能被正确地销毁。
如果某个局部对象的类型是类类型,则该对象的析构函数将被自动调用。

如果异常发生前已经构造了一部分元素,则应该确保这部分元素被正确地销毁。

析构函数与异常

析构函数在栈展开的过程中执行。
在栈展开的过程中,已经引发了异常但是我们还没有处理它。如果异常抛出后没有被正确捕获,则系统将调用 terminate 函数。

析构函数不应该抛出不能被它自身处理的异常。
如果析构函数需要执行某个可能抛出异常的操作,则该操作应该被放置在一个 try 语句块当中,并且在析构函数内部得到处理。

异常对象

异常对象是一种特殊的对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化。

异常对象位于由编译器管理的空间中,编译器确保无论最终调用的哪个 catch 子句都能访问该空间。当异常处理完毕后,异常对象被销毁。

捕获异常

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

声明的类型决定了处理代码所能捕获的异常类型。这个类型必须是完全类型,它可以是左值引用,但不能是右值引用。

查找匹配的处理代码

要求异常的类型和 catch 声明的类型是精确匹配的:

  • 允许从非常量向常量的类型转换,也就是说,一条非常了对象的 throw 语句可以匹配一个接受常量引用的 catch 语句。
  • 允许从派生类向基类的类型转换。
  • 数组被转换成指向数组(元素)类型的指针,函数被转换成指向该函数类型的指针。

重新抛出

有时,一个单独的 catch 语句不能完整地处理某个异常。在执行了某些校正操作之后,当前 catch 可能会决定由调用链更上一层的函数接着处理异常。

一条 catch 语句通过重新抛出的操作将异常传递给另外一个 catch 语句。
这里的重新抛出仍然是一条 throw 语句,只不过不包含任何表达式:

throw;

空的 throw 语句只能出现在 catch 语句或 catch 语句直接或间接调用的函数之内。如果在处理代码之外的区域遇到了空 throw 语句,编译器将调用 terminate。

一个重新抛出语句并不指定新的表达式,而是将当前的异常对象沿着调用链向上传递。

捕获所有异常的处理代码

为了一次性捕获所有异常,我们使用省略号作为异常声明,这样的处理代码称为捕获所有异常的处理代码,形如 catch(…) 。

一条捕获所有异常的语句可以与任意类型的异常匹配。

catch (…) 通常与重新抛出语句一起使用,其中 catch 执行当前局部能完成的工作,随后重新抛出异常:

void manip() {
	try{
		// 这里的操作将引发并抛出一个异常
	}
	catch (...) {
		// 处理异常的某些特殊操作
		throw;
	}	
}

catch (…) 既能单独出现,也能与其他几个 catch 语句一起出现。

函数 try 语句块与构造函数

要想处理构造函数初始值抛出的异常,我们必须将构造函数写成函数 try 语句块(也成函数测试块)的形式。

函数 try 语句块使得一组 catch 语句既能处理构造函数体(或析构函数体),也能处理构造函数的初始化过程(或析构函数的析构过程)。

在初始化构造函数的参数时也可能发生异常,这样的异常不属于函数 try 语句块的一部分。
函数 try 语句块只能处理构造函数开始执行后发生的异常。

和其他函数调用一样,如果在参数初始化的过程中发生了异常,则该异常属于调用表达式的一部分,并将在调用者所在的上下文中处理。

noexcept 异常说明

可以通过提供 noexcept 说明指定某个函数不会抛出异常。
其形式是关键字 noexcept 紧跟在函数的参数列表后面,用以标识该函数不会抛出异常:

void recoup(int) noexcept;    // 不会抛出异常
void alloc(int);              // 可能抛出异常

这两条声明语句指出 recoup 将不会抛出任何异常,而 alloc 可能抛出异常。我们说 recoup 做了不抛出说明

对于一个函数来说,noexcept 说明要么出现在该函数的所有声明语句和定义语句中,要么一次也不出现。
该说明应该在函数的尾置返回类型之前。

也可以在函数指针的声明和定义中指定 noexcept 。
在 typedef 或类型别名中则不能出现 noexcept 。

在成员函数中,noexcept 说明符需要跟在 const 及引用限定符之后,而在 final、override 或虚函数的 =0 之前。

异常说明的实参

noexcept 说明符接受一个可选的实参,该实参必须能转换为 bool 类型:如果实参是 true ,则函数不会抛出异常;如果实参是 false ,则函数可能抛出异常:

void recoup(int) noexcept(true);   // recoup 不会抛出异常
void alloc(int) noexcept(false);   // alloc 可能抛出异常

noexcept 运算符

noexcept 说明符的实参常常与 noexcept 运算符混合使用。

noexcept 运算符是一个一元运算符,它的返回值是一个 bool 类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。
和 sizeof 类似,noexcept 也不会求其运算对象的值。

异常类层次

在这里插入图片描述

类型 exception 仅仅定义了拷贝构造函数、拷贝赋值运算符、一个虚析构函数和一个名为 what 的虚成员。
其中 what 函数返回一个 const char*,该指针指向一个以 null 结尾的字符数组,并且确保不会抛出任何异常。

学习参考资料:

C++ 中文版 Primer   (第5版)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值