C++异常

1.异常

1.1 异常的概念

  • 异常处理机制允许程序中独立开发的部分能够在运行时出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题是否出现,解决问题的任务传递给程序的另一部分,检测环境无须知道问题处理模块的细节。
  • C语言主要通过错误码的形式处理错误,错误码本身是对错误信息进行分类编号,拿到错误码之后需要查询错误信息,比较麻烦。另一方面就是,需要层层往上返,如果调用栈过深,很是麻烦;发现异常情况时抛出一个对象,可以传递更为丰富的异常信息。

1.2 异常的抛出和捕获

  • 程序出现问题时,我们通过抛出(throw)一个对象来引发异常,该对象的类型以及当前的调用链决定了应该由哪个catch的处理代码处理该异常。
  • 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误。
  • 当throw执行时,throw后面的语句将不再执行。程序的执行从throw位置跳到与之匹配的catch模块,chtach可能是同一函数中的一个局部catch,也可能是调用链中另一函数中的catch,执行流从throw位置转移到了catch位置。因此,沿着调用链的函数可能提前退出;一旦程序开始执行异常处理程序,沿着调用链创建的对象都将销毁。
  • 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象(类似函数的传值返回,如果不是在当前函数中catch,而是在之前的函数中catch,这个对象是一个即将随函数栈帧销毁的对象,也就是右值,那么会移动构造,甚至现在编译器优化后,直接在外面进行构造,当前的对象是其引用),这个拷贝对象会在catch子句后销毁。

1.3 栈展开

  • 抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否造try语句块内部,如果在则查找一赔的catch语句,如果有匹配的,跳到catch的地方处理。
  • 如果当前函数没有try/catch子句,或者有但是类型不匹配,则退出当前函数,继续在外层调用链中查找,上述查找catch的过程称为栈展开。
  • 如果到达main函数,依旧没有找到匹配的catch子句,程序会调用标准库的terminate函数终止程序。
  • 如果找到匹配的catch子句处理后,catch子句代码会继续执行。

1.4 查找匹配的处理代码

  • 一般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配,就选择距离位置最近的那个
  • 但也有一些例外,允许非常量向常量的类型转换,也就是权限缩小;允许数组转换成执行数组元素类型的指针(函数传参传数组也是这样做的);允许从派生类向基类类型的转换,实际中继承体系基本都是用这个方式设计的。
  • 如果到main函数,异常依旧没有被匹配就会终止程序,但不是发生严重错误的情况下,我们不期望程序终止,所以一般main函数中最后都会使用catch(...),可以捕获任意类型的异常,但问题是不知道异常错误是什么。比如我们使用的三端,PC端,Web端,APP,小程序是APP的网页版,页面上有很多功能,我们使用其中一个功能如果出现问题,给出相应提示,但其他功能正常,如果是后端需要记录日志;每一次功能调用都是一次事件,或者事件循环;写日志时需要使用单例模式,使用一个对象进行捕捉和写入
cpp 复制代码
//一般大型项目程序才会使用异常,下面模拟设计一个服务的几个模块
//摸个模块都是继承Exception的派生类,每个模块可以添加各自的数据
//最后捕获基类,实现多态
class Exception {
public:
	Exception(const string& errmsg,int id)
		:_errmsg(errmsg),
		_id(id)
	{}
	virtual string what()const {
		return _errmsg;
	}
	int getid()const {
		return _id;
	}
protected:
	string _errmsg;
	int _id;
};

class SqlException :public Exception {
public:
	SqlException(const string& errmsg,int id,const string& sql)
		:Exception(errmsg,id),
		_sql(sql)
	{}
	virtual string what()const {
		string str = "SqlException: ";
		str += _errmsg;
		str += "->";
		str += _sql;
		return str;
	}
private:
	const string _sql;
};

class CacheException :public Exception {
public:
	CacheException(const string& errmsg, int id)
		:Exception(errmsg, id)
	{}
	virtual string what()const {
		string str = "CacheException: ";
		str += _errmsg;
		return str;
	}
};

class HttpException :public Exception {
public:
	HttpException(const string& errmsg, int id, const string& type)
		:Exception(errmsg, id),
		_type(type)
	{
	}
	virtual string what()const {
		string str = "SqlException: ";
		str += _type;
		str += ": ";
		str += _errmsg;
		return str;
	}
private:
	const string _type;
};

void SQLMgr() {
	if (rand() % 7 == 0) {
		throw SqlException("权限不足", 100, "select * from name='张三'");
	}
	else
		cout << "SQLMgr调用成功" << endl;
}
void CacheMgr() {
	if (rand() % 5 == 0) {
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
		throw CacheException("数据不存在", 101);
	else
		cout << "CacheMgr调用成功" << endl;
	SQLMgr();
}

void HttpServer() {
	if (rand() % 3 == 0)
		throw HttpException("请求资源不存在", 100, "get");
	else if (rand() % 4 == 0)
		throw HttpException("权限不足", 101, "post");
	else
		cout << "HttpServer调用成功" << endl;
	CacheMgr();
}
#include <thread>
int main() {
	srand(time(0));

	while (1) {
		this_thread::sleep_for(chrono::seconds(1));//休眠一秒,便于观察,在线程库,需要包含头文件thread
		try {
			HttpServer();
		}
		catch (const Exception& e) {
			cout << e.what() << endl;
		}
		catch (...) {
			cout << "Unkonwn Exception" << endl;
		}
	}

	return 0;
}

1.5 异常重新抛出

有时catch到一个异常对象后,需要对错误进行分类,其中某种错误异常需要进行特殊处理,其它错误则重新抛出异常给外层调用链处理。捕获异常后需要重新抛出,直接throw

cpp 复制代码
//下面程序模拟展示了聊天时发送消息,发送失败捕获异常
//可能实在电梯/地下室等手机信号不好的场景,需要多次尝试,如果多次尝试都发不出去,需要捕获异常重新抛出,或者不是网络差导致也需要重新抛出
class Exception {
public:
	Exception(const string& errmsg,int id)
		:_errmsg(errmsg),
		_id(id)
	{}
	virtual string what()const {
		return _errmsg;
	}
	int getid()const {
		return _id;
	}
protected:
	string _errmsg;
	int _id;
};

class SqlException :public Exception {
public:
	SqlException(const string& errmsg,int id,const string& sql)
		:Exception(errmsg,id),
		_sql(sql)
	{}
	virtual string what()const {
		string str = "SqlException: ";
		str += _errmsg;
		str += "->";
		str += _sql;
		return str;
	}
private:
	const string _sql;
};

class CacheException :public Exception {
public:
	CacheException(const string& errmsg, int id)
		:Exception(errmsg, id)
	{}
	virtual string what()const {
		string str = "CacheException: ";
		str += _errmsg;
		return str;
	}
};

class HttpException :public Exception {
public:
	HttpException(const string& errmsg, int id, const string& type)
		:Exception(errmsg, id),
		_type(type)
	{
	}
	virtual string what()const {
		string str = "SqlException: ";
		str += _type;
		str += ": ";
		str += _errmsg;
		return str;
	}
private:
	const string _type;
};

void _SendMsg(const string& s) {
	if (rand() % 2 == 0)
		throw HttpException("网络不稳定,发送失败", 102, "put");
	else if (rand() % 7 == 0)
		throw HttpException("你已经不是对方好友,发送失败", 103, "put");
	else
		cout << "发送成功" << endl;
}

void SendMsg(const string& s) {
	//发送消息失败,再重试三次
	for (size_t i = 0; i < 4; i++) {
		try {
			_SendMsg(s);
			break;
		}
		catch (const Exception& e) {
			//捕获异常,如果是102号错误,网络不稳定,重新发送
			//如果不是,异常重新抛出
			if (e.getid() == 102) {
				//重试三次失败,网络太差,重新抛出异常
				if (i == 3)
					throw;
				cout << "开始第" << i + 1 << "重试" << endl;
			}
			else
				throw;
		}
	}
}
#include <thread>
int main() {
	srand(time(0));
	string str;
	while (cin>>str) {
		this_thread::sleep_for(chrono::seconds(1));
		try {
			SendMsg(str);
		}
		catch (const Exception& e) {
			cout << e.what() << endl;
		}
		catch (...) {
			cout << "Unkonwn Exception" << endl;
		}
	}
	return 0;
}

Java中内存不需要手动回收♻️,采用垃圾处理器,但是也有消耗;但是锁🔒还是需要手动释放

1.6 异常安全问题

  • 异常抛出后,抛出位置之后,catch之前的代码不再执行,在抛出之前申请的资源(内存、锁等),后面进行释放,但是中间可能会抛异常导致资源没有释放,引发资源泄露,引发安全性问题。因此需要捕获异常,释放资源后重新抛出,但C++11推出智能指针解决。
  • 析构函数中抛异常也要谨慎处理,比如析构函数要释放10个资源,释放到第5个时抛出异常,捕获处理,但是后五个资源没有释放,资源泄露。《Effective C++》第8个条款专门讲了这个问题,别让异常逃离析构函数。
cpp 复制代码
double Divide(int a, int b) {
	if (b == 0) {
		throw "Division by zero condition!";
		return -1;
	}
		
	else
		return (double)a / (double)b;
}
void Func() {
	int* array = new int[10];
	try {
		int len, time;
		cin >> len >> time;
		cout << Divide(len, time) << endl;
	}
	catch (...) {
		cout << "delete[]" << array << endl;
		delete[] array;
		throw;
	}
	cout << "delete[]" << array << endl;
	delete array;
}
int main() {
	try {
		Func();
	}
	catch (const char* errmsg) {
		cout << errmsg << endl;
	}
	catch (const Exception& e) {
		cout << e.what() << endl;
	}
	catch (...) {
		cout << "Unkonwn Exception" << endl;
	}
	return 0;
}

1.7 异常规范

  • 对于用户和编译器而言,预先知道某个程序是否会抛异常大有裨益,直到某个函数是否会抛异常有助于简化调用函数的代码。

  • C++98中参数列表的后面接throw(),表示函数可能会抛异常;参数列表的后面接throw(类型1,类型2,...)表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。

  • C++98的这种方式过于复杂,实践中并不好用,比如func1调用func2,func2调用func3,那么func3可能会抛的异常在func1和func2也要注明,并且一旦发生修改,都得改,程序不可能一下子写好;C++11进行了简化,函数参数列表后面加noexcept表示不会抛出异常,什么都不加可能会抛异常。

  • noexcept(expression)还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会则返回false,不会就返回true

  • 编译器并不会在编译时严格检查noexcept,也就是说一个函数标注了noexcept,但是包含了throw语句或者调用的函数可能会抛异常,编译顺利通过或者报警告⚠️,但是标注noexcept的函数抛了异常,程序会调用treminate终止程序

cpp 复制代码
double Divide(int a, int b) noexcept{
	if (b == 0) {
		throw "Division by zero condition!";
		return -1;
	}
		
	else
		return (double)a / (double)b;
}
cpp 复制代码
//noexcept没有进行运行时检测
int main() {
	int i = 0;
	cout << noexcept(Divide(1, 2)) << endl;//1
	cout << noexcept(Divide(1, 0)) << endl;//1
	cout << noexcept(++i) << endl;//1
	return 0;
}

2.标准库的异常

C++标准库定义了一套自己的一套异常继承体系,基类是exception,日常写主程序,需要在主函数捕获exception,要或许异常信息,调用what函数,what是一个虚函数,派生类可以重写。

而在工作中,一般公司会定义一套自己的异常体系,或者使用第三方库的异常体系,再加上标准库,只需捕捉三种基类即可

相关推荐
HABuo2 小时前
【linux网络基础(二)】理解端口号&UDP、TCP协议&网络字节序
linux·服务器·c语言·网络·c++·ubuntu·centos
牢姐与蒯2 小时前
c++数据结构之二叉搜索树
数据结构·c++·搜索
Morwit3 小时前
【力扣hot100】 416. 分割等和子集
数据结构·c++·算法·leetcode·职场和发展
qeen873 小时前
【算法笔记】二分查找与二分答案
c语言·c++·笔记·学习·算法·二分
Sylvia-girl3 小时前
类与对象(下)
c++·友元函数·类与对象
Hello eveybody3 小时前
介绍最大公因数和最小公约数(C++)
java·开发语言·c++
宵时待雨3 小时前
优选算法专题3:二分查找
数据结构·c++·算法·leetcode·职场和发展
Byte不洛3 小时前
理解C++异常机制:栈展开、异常传播与异常安全
c++·异常处理·后端开发·编程基础·try catch
我头发多我先学3 小时前
C++ AVL 树:平衡原理到完整实现(自平衡二叉搜索树)
开发语言·数据结构·c++·算法