当前位置: 首页 > news >正文

《C++ Primer》 异常

1、throw抛出异常

当执行一个throw语句,必然会引发并抛出一个异常,throw后面的语句不再被执行。程序的控制权从throw转到与抛出对象类型匹配的最近的catch模块。

栈展开(栈解旋)

指的是当发生异常后,不断向外层匹配catch模块的过程。

如果异常在try里,检查与该try关联的catch,没有;若该try嵌套在另一个try里,就查找与另一个try关联的catch,没有;直接退出当前函数,在调用当前函数的外层函数中继续匹配。

栈展开就是一心寻找匹配的catch,别的啥都不管。

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

(这里说的对象是那些抛出异常之前创建的对象)

栈展开过程中(实际上是离开作用域之后),编译器负责正确的销毁对象,对于内置类型,编译器什么也不用做;对于类类型,编译器调用他们的析构函数。

若异常发生在构造函数中,程序员要确保已构造的成员能被正确销毁。
类似的,异常也可能发生在数组或标准库容器的元素初始化过程中,程序员也要确保一构造的元素被正确销毁。

但若一个块分配了资源,并在负责释放这些资源的代码前发生了异常,则释放资源的代码将不会被执行。这时应在捕获代码中释放这些资源。

析构函数与异常

在这里插入图片描述
所有标准库类型都能确保他们的析构函数不会引发异常。

异常对象

异常对象就是throw抛出的那个东西,这里的对象是指广义的对象,即,指向对象的指针也算是异常对象。异常对象可以是内置类型,像intdouble这种的;也可以是C++标准库中定义的异常对象(except族系);当然喽,也可以是我们自己定义的异常类对象。

注意①:
异常对象比较特殊,他们既不在堆区,也不在栈区,而是位于由编译器管理的内存空间中,编译器确保无论最终调用的是哪个catch模块,都能访问该空间,且该空间的数据都有效。当异常处理完毕后,编译器调用析构函数将其销毁。虽说编译器做了上述保证,但还是不能确保catch真的能访问到异常对象。如果throw的异常对象是个裸指针的话,编译器在特殊空间中维护的是指针的值,而不是其指向的对象,当throw抛出异常,离开{}后,裸指针指向的局部对象被销毁,此时执行catch一定会出现错误。

throw一个指针的正确姿势是,使用new返回的指针,这样产生的对象存储在堆区,由程序员管理,不会被编译器销毁。

注意②:
throw一个表达式时,编译器真正维护的异常对象的类型是 该表达式的静态编译时类型。即,此处无法使用多态。如果throw表达式解引用一个基类(except)指针,而指针实际指向的是子类对象,则抛出的对象将被切掉一部分,即,编译器真正在特殊空间中维护的对象只有基类的部分。

整个C++异常处理,只有这里无法使用多态,其他的地方都可以使用多态。

2、捕获对象

catch捕获异常有三种方式。

  • ①:使用异常对象的普通类型捕获,此时catch捕获到的实际上是特殊空间中异常对象的一个副本。
  • ②:使用指针捕获,此时可以使用多态,catch操作的就是特殊空间中的异常对象,catch的末尾必须delete掉该指针。
  • ③:使用引用捕获,也能使用多态,操作的也是特殊空间中的异常对象,不需要delete。一般我们只使用引用来捕获。

三种捕获在代码上的区别:

#include<iostream>
#include<exception>

using namespace std;

class MyException : public exception
{
public:
	MyException()
	{
		cout << "默认构造" << endl;
	}
	MyException(const MyException& v)
	{
		m_str = v.m_str;
		cout << "拷贝构造" << endl;
	}
	MyException(const string& s)
	{
		m_str = s;
		cout << "string 的构造函数" << endl;
	}
	const char* what() const noexcept	// 此处的 noexcept 必须加
	{
		return m_str.c_str();
	}
	~MyException()
	{
		cout << "析构函数" << endl;
	}
private:
	std::string m_str;
};

int main()
{
	try
	{
		throw MyException("我自己的异常");
	}
	catch (MyException e)
	{
		cout << e.what() << endl;
	}

	cout << endl;

	try
	{
		throw (new MyException("我自己的异常"));
	}
	catch (exception* e)
	{
		cout << e->what() << endl;
		delete e;
	}

	cout << endl;

	try
	{
		throw MyException("我自己的异常");
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

程序输出:
在这里插入图片描述

重新抛出

有时,一个单独的 catch 语句不能完整地处理某个异常。在执行了某些校正操作之后,当前的catch可能会决定由调用链更上一层的函数接着处理异常。此时要做的操作就是重新抛出,体现在代码上就是一条throw语句,但不包括任何表达式:

throw;

捕获所有异常

catch(...)能够匹配所有异常类型。
在这里插入图片描述

处理构造函数初始值时发生了异常该怎么办?

通常情况下,程序执行的任何时刻都可能发生异常,处理构造函数初始值的过程中也不例外。构造函数在进入其函数体之前首先执行初始值列表。因为在初始值列表抛出异常时构造函数体内的try语句块还未生效,所以构造函数体内的catch 语句无法处理构造函数初始值列表抛出的异常。
在这里插入图片描述

class X
{
public:

	X(std::inirializer_list<T> il) try : m_vec(std::make_shared<std::vector<T>>(il)
	{
		// 构造函数的的函数体
	}
	catch(catch std::bad_alloc& e)
	{
		handle_out_of_memory(e);
	}
	
private:
	vector<int>* m_vec;
};

函数try语句块中:关键字try出现在构造函数初始值列表的冒号之前,catch出现在函数体之后。这个try关联的catch既能处理构造函数体抛出的异常,也能处理初始化列表抛出的异常。

还有一种情况需要注意,在初始化构造函数的参数时也可能发生异常,这样的异常不属于函数try语句块的一部分。函数try语句块只能处理构造函数开始执行后发生的异常。和其他函数调用一样,如果在参数初始化的过程中发生了异常,则该异常属于调用表达式的一部分,需要在调用者所在的上下文中处理。

3、noexcept异常说明

noexcept关键字有两种含义,noexcept说明符,以及noexcept运算符。其具体含义由出现位置决定。

noexcept说明符

noexcept关键字指定某个函数不会抛出异常。通过这个指定,我们就能在函数体内部执行某些特殊的优化操作。(这些优化操作不适用于可能出错的代码)

noexcept的出现位置很有讲究,具体有如下要求:

  • 要想用noexcept修饰一个函数,该函数的所有声明语句和定义语句中都必须有noexcept关键字
  • noexcept要在函数的位置返回类型之前
  • typedef和类型别名中不能出现noexcept
  • 成员函数中,noexcept需要跟在const以及引用限定符之后,在finaloverride以及虚函数的=0之前。

noexcept说明符可以接收一个实参,该实参必须能转换为bool类型,最终,若实参是true,指定函数不会抛出异常;若实参是false,则指定函数可能抛出异常。

void func1() noexcept(true);	func1() 不会抛出异常
void func2() noexcept(false);	func2() 可能抛出异常

noexcept运算符

noexcept做运算符时是个一元运算符,返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。它的返回值一般会作为另一个noexcept说明符的实参,如下代码所示:

void function1() noexcept(noexcept(func1()));	func1()承诺不会抛出异常,则function1()也不会抛出异常
void function2() noexcept(noexcept(func2()));	func2()可能抛出异常,则function2()也可能抛出异常

对于noexcept运算符需要注意:只有当它接受的运算对象中调用的所有函数都做了不抛出说明且它自己本身不含有throw语句时,noexcept运算符才会返回true,否则都会返回false

异常说明 与 函数指针、虚函数、拷贝控制

异常说明 与 函数指针
函数指针以及该指针所指向的函数必须具有意志的异常说明。即,如果为某个函数指针做了不抛出异常的声明,则该指针只能指向不抛出异常的函数。但是,说明了可能抛出异常的函数指针 可以指向任何函数。

void (*pf1)() noexcept = func1();	正确
void (*pf2)() = func1();			也正确

pf1 = func2();	错误
pf2 = func2();	正确

异常说明 与 虚函数
如果一个虚函数承诺了它不会抛出异常,则后续派生出来的虚函数也必须做出同样的承诺;与之相反,如果基类的虚函数允许抛出异常,则子类的对应函数既可以允许抛出异常,也可以不允许抛出异常。
异常说明 与 拷贝控制
在这里插入图片描述

4、异常类层次

标准库的异常类层次

在这里插入图片描述
基类exception仅仅定义了默认构造函数、拷贝构造函数、拷贝赋值运算符、虚析构函数、名为what的纯虚成员函数。what返回const char*,该指针指向以NULL结尾的字符数组,并确保不抛出任何异常。
在这里插入图片描述

自定义异常对象

注意事项:

  • 要继承自标准异常基类exception
  • 必须写默认构造、有参构造、析构
  • 必须重写what方法,并使用noexcept修饰,因为exceptwhat就使用了noexcept

相关文章:

  • C++父类和子类指针的相互赋值和转换
  • 算法设计与分析————期末死亡冲刺
  • 现代软件工程————期末死亡冲刺
  • std::string::npos 常量解析
  • 练习4-3 求给定精度的简单交错序列部分和 (15 分)本题要求编写程序,计算序列部分和 1 - 1/4 + 1/7 - 1/10 + ... 直到最后一项的绝对值不大于给定精度eps。
  • 浙大版《C语言程序设计(第4版)》题目集 练习4-6 猜数字游戏 (15 分)
  • 练习4-7 求e的近似值 (15 分)自然常数 e 可以用级数 1+1/1!+1/2!+⋯+1/n!+⋯ 来近似计算。本题要求对给定的非负整数 n,求该级数的前 n+1 项和。
  • 数组输入输出的方法
  • 习题:贴邮票
  • 习题:遍历搜寻
  • 习题:哥德巴赫猜想
  • 习题:数字拆分
  • 习题:质数统计
  • 商品管理系统超详细讲解
  • HTTP自学笔记
  • $translatePartialLoader加载失败及解决方式
  • Java 多线程编程之:notify 和 wait 用法
  • Java 网络编程(2):UDP 的使用
  • JS字符串转数字方法总结
  • Linux各目录及每个目录的详细介绍
  • Mac 鼠须管 Rime 输入法 安装五笔输入法 教程
  • node学习系列之简单文件上传
  • ReactNative开发常用的三方模块
  • React系列之 Redux 架构模式
  • Redis字符串类型内部编码剖析
  • RxJS 实现摩斯密码(Morse) 【内附脑图】
  • SQLServer之索引简介
  • vue数据传递--我有特殊的实现技巧
  • Vue学习第二天
  • 关于Java中分层中遇到的一些问题
  • 基于游标的分页接口实现
  • 实战|智能家居行业移动应用性能分析
  • 详解NodeJs流之一
  • 想使用 MongoDB ,你应该了解这8个方面!
  • 协程
  • #传输# #传输数据判断#
  • $NOIp2018$劝退记
  • (2)STM32单片机上位机
  • (java版)排序算法----【冒泡,选择,插入,希尔,快速排序,归并排序,基数排序】超详细~~
  • (附源码)springboot学生选课系统 毕业设计 612555
  • (转)Android中使用ormlite实现持久化(一)--HelloOrmLite
  • (转贴)用VML开发工作流设计器 UCML.NET工作流管理系统
  • (转载)Google Chrome调试JS
  • .MSSQLSERVER 导入导出 命令集--堪称经典,值得借鉴!
  • .NET Core 和 .NET Framework 中的 MEF2
  • .NET Core 中插件式开发实现
  • .NET Framework 4.6.2改进了WPF和安全性
  • .net FrameWork简介,数组,枚举
  • .NET 的静态构造函数是否线程安全?答案是肯定的!
  • .NET精简框架的“无法找到资源程序集”异常释疑
  • .Net下C#针对Excel开发控件汇总(ClosedXML,EPPlus,NPOI)
  • .net最好用的JSON类Newtonsoft.Json获取多级数据SelectToken
  • [ 云计算 | AWS 实践 ] 基于 Amazon S3 协议搭建个人云存储服务
  • [100天算法】-二叉树剪枝(day 48)
  • [20140403]查询是否产生日志