目录
前言
异常的概念
异常的抛出与捕获
捕获过程
重新抛出
规范
异常体系
自定义
标准库
异常的优缺点
后记
前言
对于程序运行时发生的错误,比如内存错误、除0错误等类型,你会如何处理?是使用assert终止程序或是使用exit返回错误码等等,这些都是C语言阶段使用的处理错误的方法了。c++提出了一种处理错误的方式叫做异常,通过抛出和捕获异常来处理一个错误,涉及到的知识点不是很多,主要包括抛出与捕获原则、异常体系及优缺点,下面我们一一说明。
异常的概念
异常(Exception)是程序在运行过程中发生异常情况时的一种处理机制,能够在异常发生时跳出当前的程序流程,转而执行异常处理代码,从而保证程序的稳定性和安全性。在C++中,异常通常由程序运行时检测到,比如空指针异常、除零异常、数组越界异常、文件读取异常等等。当程序检测到异常时,可使用try-catch语句来捕获并处理异常。语法如下:
try {
// 可能会发生异常的代码块
}
catch (ExceptionType& e1) {
// e1类型异常处理代码
}
catch (ExceptionType& e2) {
// e2类型异常处理代码
}
catch (ExceptionType& e3) {
// e3类型异常处理代码
}
其中,try块中的代码可能会抛出异常,使用关键字throw抛出异常,然后catch块则会捕获并处理异常。ExceptionType代表抛出的异常类型,&e则是指向异常对象的引用。在catch块中,可以根据具体情况编写处理异常的代码,如输出错误信息、修复异常问题等。
异常的抛出与捕获
-
捕获过程
1.如果某一个栈帧中throw了一个异常,则看这个异常在此栈帧中有无在一个try块中,如果有,则在此try块下面查找类型匹配的catch语句再进入此atch块进行处理异常;
2.如果没有,则退出此栈帧回到上一个栈帧看这个异常有无在一个try块中;
3.如果直到main函数的栈帧中都没有在一个try块中,则程序会被中止,不过正常情况下知道会抛异常,我们会将此代码放入try块进行检查,而且会在最后加一个catch(...)捕获任意类型的异常,目的就是无论什么异常都捕获,不让程序中止;
4.过程中异常被某个catch块捕获并处理以后,会跳过其他catch块继续执行后面的语句。
注意:
①异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码;
②选中的catch块是调用链中与该对象类型匹配且离抛出异常位置最近的那一个,其中调用链是抛异常所在栈帧回溯到try块所在栈帧的所经过的所有栈帧组合;
③catch(...)可以捕获任意类型的异常。
eg:
main()中执行到func函数,进入func函数中执行Div函数,Div函数中设置如果除数是0就抛异常(抛一个字符串),在调用链(Div->func->main)中寻找类型匹配的catch块并处理异常,这里是打印抛出的字符串以提醒用户。
int Div(int x, int y)
{
if (y == 0)
throw "除0异常";
return x / y;
}
void func()
{
int a = 1;
int b = 0;
int c = Div(a,b);
}
int main()
{
try
{
func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (...)
{
cout << "error unknown" << endl;
}
return 0;
}
运行:
-
重新抛出
如果说一个异常在一个catch块中处理的不充分,需要到调用链的下一个栈帧中继续处理,或者当前栈帧还有任务没有完成就被跨过去了,该怎么办呢?语法支持重新抛出,当在一个catch块中处理了以后,可以重新抛出此异常到调用链的下一个try块寻找匹配的catch块继续处理。比如说,在上面的例子中,func函数的开始去new了一块资源,在函数最后需要释放,但中间调用了Div函数导致抛出异常直接到了main函数那里,导致new的资源无法释放造成内存泄漏,这时我们可以在func函数中放一个try块拦截一下将资源释放了,再重新抛出异常到main函数中继续处理,具体看如下代码块:
int Div(int x, int y)
{
if (y == 0)
throw "除0异常";
return x / y;
}
void func()
{
int* arr = new int[5];
cout << "new int[5]" << endl;
try
{
int a = 1;
int b = 0;
int c = Div(a, b);
}
catch (...)
{
delete[] arr;
cout << "delete[]" << endl;
throw; //重新抛出
}
}
int main()
{
try
{
func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (...)
{
cout << "error unknown" << endl;
}
return 0;
}
运行:
-
规范
1.在一个函数名后面加上一个throw(类型),括号中填入函数中可能抛出的所有异常类型,目的在于让函数使用者知道并在合适的地方捕获对应异常,若没有填入类型说明没有异常,若无throw(),说明此函数可能抛任何类型的异常。
eg:
void fun() throw(int,string,Date) //这个函数可能会会抛出int\string\Date这三种类型的异常
2.关键字noexcept
noexcept表示一个函数不会抛出任何异常。当使用noexcept关键字声明一个函数时,编译器会优化代码,因为它知道这个函数不会抛出异常。noexcept关键字有两种形式:noexcept和noexcept(expression)。noexcept表示函数不会抛出异常,noexcept(expression)表示函数在表达式为真时不会抛出异常。expression可以是任何表达式,包括函数调用和运算符。
注意:这两种规范只是c++编写人员希望或者说建议的一种规范异常的方法,用户可以选择遵守也可以选择不遵守,对于企业来说,都会一套自己的规范体系,具体还是要看每个企业的情况。
3.建议不要在构造函数、析构函数中抛出异常,否则可能导致对象不完整或没有完全初始化、可能导致资源泄漏等。
异常体系
-
自定义
实际上企业都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家 随意抛异常,那么外层的调用者就没办法接收,所以会定义一套继承的规范体系,如此大家抛出的都是继承的派生类对象,外层的调用者只要捕获一个基类就可以了。比如:
eg:
class Exception
{
public:
Exception(const string& msg, int id)
:_msg(msg)
, _id(id)
{}
virtual const string what() const
{
return _msg;
}
int getid() const
{
return _id;
}
protected:
string _msg;
int _id;
};
class HttpServerException : public Exception
{
public:
HttpServerException(const string& msg, int id, const string& type)
:Exception(msg, id)
, _type(type)
{}
virtual const string what() const
{
string str = "HttpServerException";
str += ":";
str += _type;
str += ":";
str += _msg;
return str;
}
private:
string _type;
};
void SeedMgr(const string& msg)
{
srand(time(0));
if (rand() % 5 == 0)
{
throw HttpServerException("网络错误", 100, "get");
}
else if (rand() % 6 == 0)
{
throw HttpServerException("http权限不足", 101, "post");
}
cout << "发送成功" << endl;
}
void HttpServer()
{
//实现网络请求遇到错误重新发送(三次)
string msg = "你在干什么";
int n = 3;
while (n--)
{
try
{
SeedMgr(msg);
break;
}
catch(const Exception& e)
{
if (e.getid() == 100 && n > 0)
{
continue;
}
else
{
throw e;
}
}
catch (...)
{
cout << "unknown error" << endl;
}
}
}
int main()
{
while (1)
{
Sleep(1000); //睡眠1秒
try
{
HttpServer();
}
catch (const Exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "unknown error" << endl;
}
}
return 0;
}
运行:
-
标准库
C++ 提供了一系列标准的异常,定义在std中,它们是以父子类层次结构组织起来的,如下图所示。
实际中我们可以去继承exception类实现自己的异常类,但是实际中很多公司像上面一 样自己定义一套异常继承体系,因为C++标准库设计的不够好用。
说明:
异常的优缺点
优点:
①相比错误码的方式可以清晰准确的展示出错误的各种信息,这样可以帮助更好的定位程序的bug;
②能够将异常跳到最外层处理,不用层层跳;
③很多的的第三方库都包含异常,比如boost、gtest、gmock等;
④部分函数使用异常更好处理,比如 T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理。
缺点:
①运行时出错抛异常就会乱跳。这会 导致我们跟踪调试时以及分析程序时,比较困难;
②C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。
总之,异常是利大于弊的,大部分语言都是使用异常处理错误。
后记
对于不同的企业,异常处理的规范都是不同的,但是都大差不差,具体都是定义一个基类,下级抛出的都是继承此基类的派生类对象,在catch块中捕获基类即可,细节还是要等大家到了企业中再具体去了解自己部门的一个规范,但是涉及到的相关关键字的知识点必须完全掌握,这些是基本能力,异常相关介绍并不难,知识点也不多,详细去看一下就能明白,拜拜。