【C++:异常】C++ 异常处理完全指南:从理论到实践,深入理解栈展开与最佳实践


🎬 个人主页艾莉丝努力练剑
专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录
Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬 艾莉丝的简介:


🎬 艾莉丝的C++专栏简介:


文章目录

  • C++学习阶段的三个参考文档
  • [1 ~> 异常的概念](#1 ~> 异常的概念)
  • [2 ~> 异常的使用层](#2 ~> 异常的使用层)
    • [2.1 异常的抛出和捕获](#2.1 异常的抛出和捕获)
    • [2.2 栈展开](#2.2 栈展开)
      • [2.2.1 理论](#2.2.1 理论)
      • [2.2.2 最佳实践](#2.2.2 最佳实践)
    • [2.3 查找匹配的处理代码](#2.3 查找匹配的处理代码)
      • [2.3.1 抛出对象和catch一般是类型完全匹配的](#2.3.1 抛出对象和catch一般是类型完全匹配的)
      • [2.3.2 最佳实践](#2.3.2 最佳实践)
    • [2.4 异常重新抛出](#2.4 异常重新抛出)
      • [2.4.1 概念](#2.4.1 概念)
      • [2.4.2 最佳实践](#2.4.2 最佳实践)
    • [2.5 异常安全问题](#2.5 异常安全问题)
      • [2.5.1 注意事项](#2.5.1 注意事项)
      • [2.5.2 最佳实践](#2.5.2 最佳实践)
      • [2.6 异常规范](#2.6 异常规范)
      • [2.6.1 理论](#2.6.1 理论)
      • [2.6.2 最佳实践](#2.6.2 最佳实践)
  • [3 ~> 标准库的异常](#3 ~> 标准库的异常)
  • C++11完整代码示例与实践演示
  • 结尾

C++学习阶段的三个参考文档

看库文件(非官方文档): Cplusplus.com

这个文档在C++98、C++11时候还行,之后就完全没法用了......

准官方文档(同步更新) ------还 可以看语法C++准官方参考文档

这个行,包括C++26都同步了,我们以后主要会看这个。

官方文档(类似论坛): Standard C++

这个网站上面会有很多大佬,类似于论坛。



1 ~> 异常的概念

在C语言里面,异常的处理机制------通过错误码的形式处理错误,比较麻烦,如下图所示------

异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后解决问题的任务传递给程序的另一部分,检测环节无须知道问题的处理模块的所有细节。

C语言主要通过错误码的形式处理错误,错误码本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息,比较麻烦。异常时抛出一个对象(可以抛出任何类型的异常),这个对象可以函数更全面的各种信息(包含各种各样的信息)。


2 ~> 异常的使用层

2.1 异常的抛出和捕获

程序出现问题时,我们通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前的调用链决定了应该由哪个catch的处理代码来处理该异常。

被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误。

当throw执行时,throw后面的语句将不再被执行。程序的执行从throw位置跳到与之匹配的catch模块,catch可能是同一函数中的一个局部的catch,也可能是调用链中另一个函数中的catch,控制权从throw位置转移到了catch位置。

抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝的对象会在catch子句后销毁(这里的处理类似于函数的传值返回)。


2.2 栈展开

2.2.1 理论

抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理。

如果当前函数中没有try/catch子句,或者有try/catch子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch过程被称为栈展开。

如果到达main函数,依旧没有找到匹配的catch子句,程序会调用标准库的terminate函数终止程序。

如果找到匹配的catch子句处理后,catch子句后面的代码会继续执行。

2.2.2 最佳实践

注意: e.what(); 返回包含异常的字符串。

上图中,[ LINE ] 的作用:获取你是哪个位置的宏

2.3 查找匹配的处理代码

2.3.1 抛出对象和catch一般是类型完全匹配的

一般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离它位置更近的那个(类型匹配时遵循"就近原则")。

但是也有一些例外,允许从非常量向常量的类型转换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派生类向基类类型的转换,这个点非常实用,实际中继承体系基本都是用这个方式设计的。

如果到main函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以一般main函数中最后都会使用catch(...),它可以捕获任意类型的异常,但是我们不知道异常错误是什么。

2.3.2 最佳实践

2.4 异常重新抛出

2.4.1 概念

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

2.4.2 最佳实践

2.5 异常安全问题

2.5.1 注意事项

1、异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。中间我们需要捕获异常,释放资源后面再重新抛出,当然后面智能指针章节讲的RAII方式解决这种问题是更好的。

2、其次析构函数中,如果抛出异常也要谨慎处理,比如析构函数要释放10个资源,释放到第5个时抛出异常,则也需要捕获处理,否则后面的5个资源就没释放,也资源泄漏了。《EffctiveC++》中的第8个条款也专门讲了这个问题,别让异常逃离析构函数。

2.5.2 最佳实践

2.6 异常规范

2.6.1 理论

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

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

C++98的方式这种方式过于复杂,实践中并不好用,C++11中进行了简化,函数参数列表后面加noexcept表示不会抛出异常,啥都不加表示可能会抛出异常。

编译器并不会在编译时检查noekcept,也就是说如果一个函数用noexcept修饰了,但是同时又包含了throw语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是一个声明了noexcept的函数抛出了异常,程序会调用terminate终止程序。

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

2.6.2 最佳实践


3 ~> 标准库的异常

标准库链接:std::exception

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


C++11完整代码示例与实践演示

Test.cpp:

cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
using namespace std;
#include<exception>

//double Divide(int a, int b)
//{
//	// 当b == 0时抛出异常 
//	if (b == 0)
//	{
//		//string s("Divide by zero condition!");
//		//throw s;
//
//		throw exception("Divide by zero condition!");
//	}
//	else
//	{
//		return ((double)a / (double)b);
//	}
//}
//
//void Func()
//{
//	try
//	{
//		int len, time;
//		cin >> len >> time;
//		cout << Divide(len, time) << endl;
//	}
//	catch (const exception& e)
//	{
//		cout << e.what() << endl;
//	}
//
//	cout << "Func():" << __LINE__ << endl;
//}
//
//int main()
//{
//	while (1)
//	{
//		try
//		{
//			Func();
//		}
//		catch (const string& s)
//		{
//			cout << s << endl;
//		}
//
//		catch (const exception& e)
//		{
//			cout << e.what() << endl;
//		}
//
//		catch (...)		// 任意类型的对象
//		{
//			cout << "未知异常" << endl;
//		}
//
//		cout << "main():" << __LINE__ << endl;
//	}
//
//	return 0;
//}

#include<thread>	// 延时功能

// 一般大型项目才会使用异常,下面我们模拟设计一个服务的几个模块
// 每个模块的继承都是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 = "HttpException:";
		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 HttpSever()
{
	if (rand() % 3 == 0)
	{
		throw HttpException("申请资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpException("权限不足", 101, "post");
	}
	else
	{
		cout << "HttpSever 调用成功" << endl;
	}
	CacheMgr();
}

//int main()
//{
//	srand(time(0));
//	while (1)
//	{
//		this_thread::sleep_for(chrono::seconds(1));		// 延迟1秒
//
//		try
//		{
//			HttpSever();
//		}
//		catch (const Exception& e)	// 这里捕获基类,基类对象和派生类对象都可以被捕获
//		{
//			// 多态调用
//			cout << e.what() << endl;
//		}
//
//		catch (...)		// 任意类型的对象
//		{
//			cout << "Unknown Exception" << endl;
//		}
//	}
//
//	return 0;
//}

// 输出结果:
// HttpSever 调用成功
// CacheMgr 调用成功
// SQLMgr 调用成功
// HttpSever 调用成功
// CacheException:权限不足
// HttpSever 调用成功
// CacheException:数据不存在
// HttpSever 调用成功
// CacheMgr 调用成功
// SQLMgr 调用成功
// HttpException:get : 申请资源不存在
// HttpException:post : 权限不足
// HttpException:get : 申请资源不存在
// HttpException:post : 权限不足
// HttpSever 调用成功
// CacheException:权限不足
// HttpSever 调用成功
// CacheException:数据不存在
// HttpException:get : 申请资源不存在
// HttpSever 调用成功
// CacheMgr 调用成功
// SQLMgr 调用成功
// HttpSever 调用成功
// CacheMgr 调用成功
// SQLMgr 调用成功
// HttpException:get : 申请资源不存在
// HttpException:get : 申请资源不存在
// HttpException:get : 申请资源不存在
// HttpException:get : 申请资源不存在
// HttpException:post : 权限不足
// HttpException:post : 权限不足
// HttpSever 调用成功
// CacheMgr 调用成功
// SQLMgr 调用成功
// HttpSever 调用成功
// CacheMgr 调用成功
// SQLMgr 调用成功
// HttpSever 调用成功
// CacheException:权限不足
// HttpException:get : 申请资源不存在
// HttpSever 调用成功
// CacheMgr 调用成功
// SQLMgr 调用成功
// HttpException:post : 权限不足
// HttpSever 调用成功
// CacheMgr 调用成功
// SQLMgr 调用成功
// HttpSever 调用成功
// CacheMgr 调用成功
// SQLMgr 调用成功
// HttpSever 调用成功
// CacheMgr 调用成功
// SqlException:权限不足->select* from name = 张三
// HttpException:post : 权限不足
// HttpSever 调用成功
// CacheMgr 调用成功
// SQLMgr 调用成功
// . . . . . . .

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

// 网络不稳定,要求重试3次,均失败
void SendMsg(const string& s)
{
	for (size_t i = 0; i < 4; i++)
	{
		try
		{
			_Sending(s);

			// 走到这里代表成功了,跳出循环
			break;
		}
		catch (const Exception& e)
		{
			if (e.getid() == 102)
			{
				if (i == 3)
					throw;

				cout << "开始第" << i + 1 << "重试" << endl;
			}
			else
			{
				// 重新抛出异常
				//throw e;
				throw;
			}
		}
	}
}

//int main()
//{
//	srand(time(0));
//	string str;
//	while (cin>>str)
//	{
//		try
//		{
//			SendMsg(str);
//		}
//		catch (const Exception& e)	// 这里捕获基类,基类对象和派生类对象都可以被捕获
//		{
//			// 多态调用
//			cout << e.what() << endl << endl;
//		}
//
//		catch (...)		// 任意类型的对象
//		{
//			cout << "Unknown Exception" << endl;
//		}
//	}
//
//	return 0;
//}

//double Divide(int a, int b) noexcept;
//double Divide(int a, int b) throw(const char*);
double Divide(int a, int b)
{
	// 当b == 0时抛出异常 
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	return (double)a / (double)b;
}

void Func()
{
	// 这里可以看出如果发生除0错误抛出异常,另外下面的array没有得到释放
	// 所以这里捕获异常后并不处理异常,异常还是交给外层处理,这里捕获了再重新抛出去
	int* array = new int[10];

	int len, time;
	//try
	//{
	//	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 (...)
	//{
	//	cout << "Unknown Exception" << endl;
	//}

	int i = 0;
	cout << noexcept(Divide(1, 2)) << endl;
	cout<<noexcept(Divide(1, 0)) << endl;
	cout << noexcept(Func()) << endl;
	cout << noexcept(++i) << endl;

	return 0;
}

// 输出结果:
// 0
// 0
// 0
// 1

结尾

uu们,本文的内容到这里就全部结束了,艾莉丝再次感谢您的阅读!

结语:希望对学习C++相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!

往期回顾

【C++:C++11收尾】解构C++可调用对象:从入门到精通,掌握function包装器与bind适配器包装器详解

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა

相关推荐
金士镧(厦门)新材料有限公司34 分钟前
稀土抑烟剂在PVC材料中的应用:提升安全与环保
科技·安全·全文检索
武子康35 分钟前
Java-184 缓存实战:本地缓存 vs 分布式缓存(含 Guava/Redis 7.2)
java·redis·分布式·缓存·微服务·guava·本地缓存
小马爱打代码6 小时前
Spring Boot:模块化实战 - 保持清晰架构
java·spring boot·架构
快乐zbc7 小时前
【C++ 基础】:给定一个指针 p,你能判断它是否指向合法的对象吗?
c++
岁忧7 小时前
GoLang五种字符串拼接方式详解
开发语言·爬虫·golang
tyatyatya7 小时前
MATLAB基础数据类型教程:数值型/字符型/逻辑型/结构体/元胞数组全解析
开发语言·matlab
小坏讲微服务7 小时前
SpringBoot4.0整合knife4j 在线文档完整使用
java·spring cloud·在线文档·knife4j·文档·接口文档·swagger-ui
8***Z897 小时前
springboot 异步操作
java·spring boot·mybatis
i***13247 小时前
Spring BOOT 启动参数
java·spring boot·后端