C++11之异常

前言:在前面笔者分享了关于c++11的新的语法特性,今天分享的这个内容也是c++11新增的语法特性,它就是异常,相信各位老铁或多或少都听过异常两个字,像java语言对错误的处理就会抛异常啥的,那么在c++11,c++官方也引入了异常。

1.C语言传统处理错误的方式\

我们都知道c语言是没有异常的,那么c语言是如何对错误进行处理的呢???

c语言处理错误的方式有两种,一种是直接终止程序,另一种是返回对应的错误码。

(1)直接终止程序:C语言是使用什么来直接终止程序呢?C语言是直接使用assert进行断言,但是这个使用着太难受了,一旦发生断言错误,我的代码直接就崩掉了,这在实际中使用太难受了。

我们来看看个例子

cpp 复制代码
#include <iostream>
#include <assert.h>
using namespace std;

int divs(int x, int y)
{
	assert(y!=0);
	return x / y;
}

int main()
{
	int a, b;
	cin >> a >> b;
	cout << divs(a, b) << endl;
	return 0;
}

假设我们除数为0,那么看看程序是否直接会崩掉。

代码直接就崩掉了,那么这个是不是很不合理,明明只是一个除0的错误,却导致我的整个代码崩掉了,可能有的老铁会觉得,我的代码就只有几十行,所有除0错误算是很严重了,所有程序直接崩掉也正常,但是如果是在项目工程里面呢?一个工程项目有几十万行或者几百亡行代码,只是因为你的某个计算功能出现除0错误导致整个项目崩掉,那是不是很过分。所以在实际项目运用中很少敢用assert,除非你非常明确这个错误是不会发生的,那么你就可以使用assert进行断言判断。

(2)返回错误码:在我们写c语言程序时,是不是会在main函数中写一个return 0;语句,其实这个0就是一个程序运行成功的状态码。下面笔者写一段代码来带大家看看错误码,使用打开文件来演示,除0错误不会返回错误码,而是返回特定的信号。

cpp 复制代码
#include <iostream>
#include <string.h>
#include <errno.h>
using namespace std;
int main()
{
	FILE* file = fopen("test.txt", "r");
	if (file == nullptr)
	{
		cout << "错误码: " << errno << "\n错误码转错误描述:" << strerror(errno) << endl;
	}

	fclose(file);
	return 0;
}

我以读的方式打开这个test.txt文件,但是这个文件并不存在,那么我们看看是不是会生成错误码,我们的错误码是不是可以转成错误描述。

的确返回了错误码,但是错误码是数字,数字对机器来说很友好,但是对使用机器的用户来说就很难受了,非专业的人可能根本看不懂,那么老铁就会抬杠说,错误码不是可以转错误描述吗?的确可以转成错误描述,但是在一个项目工程中有几百万行代码,万一开发者忘记了检查错误码,发生错误了就不知道了,到时候项目快要上线了,但是发生错误,没有写检查错误码的代码,导致项目上线时间推迟,那么今年奖金没有了,还可能被炒鱿鱼!!!

由于C语言对错误的处理机制有着各种各样的缺陷,那么C++就引入了异常。

2.异常:异常简单来说就是一个函数发现自己无法处理的错误时,就可以抛出一个异常,让函数的直接或者间接调用者来处理这个错误。

那么如何抛出一个异常呢?

(1)使用throw关键字来完成。

(2)try:try用于定义一段需要异常监控的代码

(3)catch:用于捕获异常

到这里我们就懂得了异常了定义了,也懂得了为什么c++需要有异常,那么接下来就该是如何使用异常了。

先来简单使用一下异常

自动抛出异常

cpp 复制代码
#include <iostream>
#include <string.h>
#include <errno.h>
#include <vector>
using namespace std;
int main()
{
	try
	{
		vector<int> v = { 1,2,3,4,5 };
		for (int i = 0; i <= v.size(); i++)
		{
			cout << v.at(i) << " ";//at表示安全访问数组中元素并输出
		}
	}
	catch (exception& e)//捕获异常
	{
		cout << e.what() << endl;//打印异常错误信息
	}
	return 0;
}


我们就可以看到由于发生了数组越界访问,那么自动抛出了异常,catch块就捕获了异常,最后调用what方法打印出了异常信息,这样是不是很简单,不管什么错误,只要出现了错误我们就可以抛出对应的异常,当然异常不是随便抛出的,因为抛异常会导致程序运行发生跳转,在后面我们会详细讲解。

有自动抛出的异常,那么肯定也有对应的手动抛出异常,那么如何手动抛出异常呢?

手动抛出异常

cpp 复制代码
namespace ljy
{
	int div(int n, int m)
	{
		if (m == 0)
		{
			//throw可以抛出任意类型的异常对象
			throw string("发生除0错误");//抛出的是该对象的拷贝,该对象是局部对象,出了作用域就销毁了
		}
		return n / m;
	}
}

int main()
{
	try
	{
		int n, m;
		cin >> n >> m;
		cout << ljy::div(n, m) << endl;
	}

	//异常列表可以有多个
	//只有发生异常,才会跳转到对应的catch语句,不发生异常,不会执行catch语句
	catch (int err)
	{
		cout << err << endl;
	}
	
	catch (const string& err)
	{
		cout << err << endl;
	}

	//可以捕获没有匹配的任意类型的异常,避免异常没捕获时程序直接终止了
	catch (...)
	{
		cout << "未知错误" << endl;
	}
	return 0;
}

由于CSDN上上传不了视频,各位老铁可以copy我的代码自己打断点进行调试看看抛异常时程序执行的步骤,再看看不抛异常时程序是如何执行的。

这里笔者直接给出结论
抛异常时会直接跳到对应类型的catch块中执行代码,如何没有定义对应类型的catch块,会跳到catch(...)捕捉任意类型异常的代码块中执行,一般是就近原则。
如果没有抛异常,那么也不会执行catch块的代码。

那么我们懂得如何进行抛异常了,那么接下来笔者来解释一下抛异常的原理,来帮助各位老铁理解抛异常。

3.抛异常和捕获异常的原理

那么我们举个例子来验证一下吧。

cpp 复制代码
void f2()
{
	throw string("我是一个异常");//先在这里查找throw是否在try块内部,
								//如果在try内部且存在匹配catch块,则进行处理
								//不存在退出当前函数栈
}

void f1()
{
	f2();//不存在对应的catch块,退出当前函数栈
}

int main()
{
	try
	{
		f1();
	}
	catch(string e)	//捕捉到同类型的异常
	{
		cout << e<< endl;
	}
}

抛异常还有一个规则非常实用,那就是通过抛派生类对象,使用基类对象进行捕获异常 ,这个在实际中非常实用的,那么有老铁就会疑惑了,不是说需要抛同类型的异常对象,才可以进行捕获吗,那现在为什么基类可以捕获派生类对象?那是因为派生类继承了基类的所有的成员,所有我们可以把派生类对象看成一个基类对象,所有基类对象可以捕获所有派生类对象抛出的异常。,这个笔者会在后面详细讲解。

4.异常重新抛出

当单个catch不能完全处理一个异常时,在进行一些力所能及的校正处理后,catch则通过重新抛出异常传递给上一层函数进行处理。

可能对于上面讲解有老铁理解不了,没关系,笔者再举个生活中例子进行说明。

假设现在公司项目工程中有隐藏多个BUG,这些BUG有容易排查的,也有难以排查的,那么你一看那些BUG你就知道以你的能力不能完全处理掉,那么你就把简单的BUG给处理掉了,那些难以排查和处理的你就报告给你的上级了。大喊:"老大,这个项目的BUG太多了,隐藏太深了,我处理不了",你的上司回应你说"公司花那么多钱招你进来,连BUG都找不到,直接回家吧,明天别来了(狗头保命!!!)",最后没方法上司就自己亲自出马来解决这个项目工程中的所有BUG了。

cpp 复制代码
int Divs(int a, int b)
{
	if (b == 0)
	{
		throw("除0错误");
	}

	return a / b;
}

void Func()
{
	int* array = new int[10];

	//我们可以看到Divs函数抛出了异常,Func函数需要抛出异常
	//但是Func函数主要任务是先释放掉内存空间,所以就将捕获到的异常重新抛出了
	try
	{
		int a, b;
		cin >> a >> b;
		cout << Divs(a, b) << endl;
	}

	catch (...)
	{
		cout << "delete[]" <<array<< endl;
		delete[] array;
		throw;
	}
}

int main()
{
	try
	{
		Func();
	}

	catch (const char* s)
	{
		cout << s << endl;//cout对const char*类型有重载处理,所以直接输出了
	}

	return 0;
}

5.异常安全

(1)最好不要在构造函数中进行抛异常,可能会导致对象不完整或者初始化不完全。

cpp 复制代码
class A
{
public:
	A(int a=1, int b=2) :_a(a), _b(b)
	{
		throw string("我抛异常了,导致_array没有开辟空间");
		cout << "new int[]" << endl;
		_array = new int[10];
	}
public:
	int _a;
	int _b;
	int* _array; 
};

int main()
{
	try
	{
		A aa;
	}
	catch (string& s)
	{
		cout << s << endl;
	}
	return 0;
}

(2)最好不要在析构函数中进行抛异常,可能导致资源泄漏。(例如在new了空间,但是在delete中抛出了异常,那么就会跳过delete语句,导致内存没有释放)(例如在lock和unlock之间进行抛异常,导致死锁),对于这些问题我们在后面博客中再进行讲解如何解决,先留个悬念。

cpp 复制代码
class A
{
public:
	A(int a=1, int b=2) :_a(a), _b(b)
	{
		cout << "new int[]" << endl;
		_array = new int[10];
	}
	~A()
	{
		throw string("我抛异常了,导致没有释放_array的空间");
		delete[] _array;
	}
public:
	int _a;
	int _b;
	int* _array; 
};

int main()
{
	try
	{
		A aa;
	}
	catch (string& s)
	{
		cout << s << endl;
	}
	return 0;
}

6.异常使用规范

异常接口声明

cpp 复制代码
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
voidfun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void*operatornew (std::size_tsize) throw (std::bad_alloc);// 这里表示这个函数不会抛出异常
void*operatordelete (std::size_tsize, void*ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常thread() noexcept;
thread (thread&&x) noexcept;

若无异常接口声明,那么该函数可以抛出任意类型异常。

7.C++标准库实现异常

我们先来看看C++STL库里面实现的异常。

STL库中是通过父子类层次来组织起来的。

我们来看看异常的父类是有哪些字段。

这个就是异常机制的父类,它通过定义虚函数what来让继承它的子类来重写what方法,它给成员函数禁掉抛异常,只能在派生类中抛异常,通过基类对象来捕捉异常。

再看看STL库里面的异常类

8.自定义异常体系

在企业中,很多开发团队都认为STL库实现的异常体系不好用,一般都会在自己内部创建自定义的异常体系,既然企业用到了,那么我们也要学习一下如何自定义异常体系,这里笔者通过举例服务器开发中通常使用的自定义异常体系。

自定义的异常父类

cpp 复制代码
class Exception
{
public:
	Exception(int id,const string& errmsg):_id(id),_errmsg(errmsg){}
	virtual string what() const 
	{
		return _errmsg;
	}
protected:
	int _id;
	string _errmsg;
};

自定义异常子列SqlException类

cpp 复制代码
class SqlException : public Exception
{
public:
	SqlException(int id,const string& errmsg,const string& sql)
			:Exception(id,errmsg),_sql(sql)
	{}

	virtual string what() const
	{
		string str = "SqlException";
		str += _errmsg;
		str += "->";
		str += _sql;

		return str;
	}
protected:
	const string _sql;
};

自定义异常子类CacheException类

cpp 复制代码
class CacheException : public Exception
{
public:
	CacheException(int id,const string& errmsg)
		:Exception(id,errmsg)
	{}

	virtual string what() const
	{
		string str = "CacheException";
		str += _errmsg;

		return str;
	}
};

自定义异常子类HttpServeException

cpp 复制代码
class HttpServerException : public Exception
{
public:
	HttpServerException(int id,const string& errmmsg,const string& type)
		:Exception(id,errmmsg)
		,_type(type)
	{}

	virtual string what() const
	{
		string str = "HttpServerException";
		str += _type;
		str += ":";
		str += _errmsg;

		return str;
	}
protected:
	const string _type;
};

到这里服务器常用的自定义异常体系就完成了,那么接下来我们进行测试一下,看看能不能抛出对应的异常错误。(使用单个线程来执行不同的方法进行抛异常)

cpp 复制代码
#include <thread>
class Exception
{
public:
	Exception(int id,const string& errmsg):_id(id),_errmsg(errmsg){}
	virtual string what() const 
	{
		return _errmsg;
	}
protected:
	int _id;
	string _errmsg;
};

class CacheException : public Exception
{
public:
	CacheException(int id,const string& errmsg)
		:Exception(id,errmsg)
	{}

	virtual string what() const
	{
		string str = "CacheException";
		str += _errmsg;

		return str;
	}
};

class SqlException : public Exception
{
public:
	SqlException(int id,const string& errmsg,const string& sql)
			:Exception(id,errmsg)
			,_sql(sql)
	{}

	virtual string what() const
	{
		string str = "SqlException";
		str += _errmsg;
		str += "->";
		str += _sql;

		return str;
	}
protected:
	const string _sql;
};

class HttpServerException : public Exception
{
public:
	HttpServerException(int id,const string& errmmsg,const string& type)
		:Exception(id,errmmsg)
		,_type(type)
	{}

	virtual string what() const
	{
		string str = "HttpServerException";
		str += _type;
		str += ":";
		str += _errmsg;

		return str;
	}
protected:
	const string _type;

};


//抛出数据库异常
void SQLMgr()
{
	srand(time(0));
	if (rand() % 7 == 0)
	{
		//抛异常
		throw SqlException(100, "权限不足", "select * from name='张三'");
	}
}

//抛出cache异常
void CacheMgr()
{
	srand(time(0));
	if (rand() % 5 == 0)
	{
		throw CacheException(100,"权限不足");
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException(101, "数据不存在");
	}

	SQLMgr();
}

void HttpServer()
{
	srand(time(0));
	if (rand() % 3 == 0)
	{
		throw HttpServerException(100, "请求资源不存在", "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpServerException(101, "权限不足", "post");
	}

	CacheMgr();
}

int main()
{
	while (1)
	{
		this_thread::sleep_for(chrono::seconds(1));//让线程休眠1秒

		try
		{
			HttpServer();
		}
		catch (const Exception& e)
		{
			//构成多态(基类有虚函数,通过基类的指针或者引用去调用对应的虚函数)
			cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "unkown Exception" << endl;
		}
	}

	return 0;
}

9.C++异常优缺点

优点:

(1)异常相对于错误码的方式可以更清晰准确的展示出错误的个周末信息。

(2)很多第三方库都包含了异常(例如boost库,gtest库)

(3)返回错误码的方式,当深层函数返回错误码时,得需要层层返回错误,最外层才能拿到,而异常不需要。

(4)当需要通过返回值表示错误时,但是表示该错误值又需要使用时,那么久很难受了,异常就没有这个问题,还有数组下标越界只能使用抛异常或者终止程序处理,不能使用返回错误码得方式。

缺点

(1)异常会导致程序执行流各种跳转,不利于我们分享程序和跟踪调试。

(2)C++由于没有垃圾回收机制,所以抛异常会非常容易导致内存泄漏,死锁等异常安全问题(需要RAII来处理,后面博客会讲解)

(3)C++标准库定义异常体系不行,所以大家各自定义自己的异常体系,非常混乱。

相关推荐
木头左2 小时前
跨周期共振效应在ETF网格参数适配中的应用技巧
开发语言·python·算法
almighty272 小时前
C# WPF实现ComboBox实时搜索与数据绑定
开发语言·c#·wpf·combobox
菜鸟小九2 小时前
SSM(MybatisPlus)
java·开发语言·spring boot·后端
数据知道2 小时前
Go基础:常用数学函数处理(主要是math包rand包的处理)
开发语言·后端·golang·go语言
学习同学2 小时前
从0到1制作一个go语言服务器 (一) 配置
服务器·开发语言·golang
大飞pkz2 小时前
【设计模式】桥接模式
开发语言·设计模式·c#·桥接模式
数据知道3 小时前
Go基础:文件与文件夹操作详解
开发语言·后端·golang·go语言
珍宝商店3 小时前
原生 JavaScript 方法实战指南
开发语言·前端·javascript
神龙斗士2403 小时前
Java 数组的定义与使用
java·开发语言·数据结构·算法