传统的错误处理方式
如果程序发生了严重的错误,在C语言中,一般的处理方式是:返回错误码,严重时直接终止程序。对于错误码,很多系统调用和库调用的接口都是把错误码放到errno变量中,使用者需要通过错误码查找对应错误信息,而直接终止程序的做法未免有些暴力,对于程序发生错误的处理方式,C++使用抛异常来解决。
何为异常?
异常作为一种错误的处理方式,在程序发生无法处理的错误时,程序会抛出异常,让使用者解决这个错误。
throw:程序出现错误时,用throw抛出错误
catch:catch用来接收throw抛出的错误
try:try内抛出的异常才会被catch捕获,try后面必须跟一个或多个catch块
double Division(int num1, int num2)
{
if (num2 == 0)
throw "Division by zero condition!";
else
return num1 * 1.0 / num2;
}
int main()
{
try
{
Division(1, 0);
}
catch (const char* str)
{
cout << str << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
cout << "after catch" << endl;
return 0;
}
比如上面的代码,try块中调用了Division函数,当被除数为0时,Division函数会抛出异常对象"Division by zero condition!",很显然这是一个const char*对象,catch的参数列表接收并打印这个对象。 至于最后的catch,参数列表中的三个点则表示接收任意对象,当异常对象没有被catch捕获时,程序才会走到参数为三个点的catch块中。
在异常被捕获后,程序会执行catch块的代码,接着跳过其他catch块,然后运行剩下的代码,所以程序会打印after catch这句话。
异常的使用
异常的语法点
异常由抛出对象引发,抛出对象的类型决定了要调用哪个catch块
被选中的处理代码(catch块)是与抛出对象距离最近并且类型对应的那个
抛出对象会被临时拷贝,产生临时对象,再传给catch块,类似于函数的传值返回
catch(…)可以接收任意类型的异常对象,但主要的问题是不知道发生的错误是什么
可以用父类对象接收类型为子类对象的异常,这个过程发生切片,但在异常的捕获中经常使用
函数调用链中异常的栈展开匹配原则
一个异常被抛出,先检查异常是否在try块中,如果不在则退出当前函数,到调用它的函数块中,检查异常是否在该函数栈中的try块中被抛出,如果在则寻找对应的catch块执行,如果找不到对应的catch块,再推出当前函数栈,重复上面的步骤,直到main函数栈中找不到catch块,程序直接终止。如果执行完catch块,程序会执行后面的代码。
以上面的代码为例,Division抛出了异常,但Division中没有try块,程序退出当前函数栈,到了main函数栈,发现异常在try块中,接着寻找与抛出对象类型相同的catch块,找到了运行其代码:打印出错误信息。
double Division(int num1, int num2)
{
try
{
if (num2 == 0)
throw "Division by zero condition!";
else
return num1 * 1.0 / num2;
}
catch (const char* str)
{
cout << str << endl;
}
}
int main()
{
try
{
Division(1, 0);
}
catch (const char* str)
{
cout << str << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
cout << "after catch" << endl;
return 0;
}
如果改写之前的代码,Division中抛出异常,throw在当前函数栈的try块中,则在当前函数栈中寻找catch块,成功找到后,打印其错误信息。
try块的嵌套使用
当异常抛出,需要对异常进行特殊处理的时候,可以嵌套使用try块,比如发送微信信息失败,可能是网络错误,也可能是没有对方好友的权限,当捕获网络错误的异常时,需要重新发送信息几次,而捕获权限不足的异常时,不需要重新发送消息,只要显示“不是对方好友”的提示即可。
下面用代码模拟这个过程。
namespace test
{
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
,_id(id)
{}
virtual string what()
{
return _errmsg;
}
int errid()
{
return _id;
}
protected:
int _id;
string _errmsg;
};
class HttpServeException : public Exception
{
public:
HttpServeException(const string& errmsg, int id)
:Exception(errmsg, id)
{}
string what()
{
string str = "HttpServeException:";
str += _errmsg;
return str;
}
};
void SeedMsg(const string& str)
{
if (rand() < RAND_MAX - 10000)
{
throw HttpServeException("网络错误", 1);
}
else if (rand() < RAND_MAX / 2)
{
throw HttpServeException("权限不足,你已不是对方好友", 2);
}
else
{
cout << "信息发送成功" << endl;
}
}
}
int main()
{
while (1)
{
try
{
for (int i = 0; i < 10; i++)
{
try
{
Sleep(500);
test::SeedMsg("hello");
break;
}
catch (test::HttpServeException& exception)
{
if (exception.errid() == 1)
{
cout << "网络错误,重新发送消息次数为" << i << endl;
}
else
{
throw exception;
}
}
}
}
catch (test::Exception& exception)
{
cout << exception.what() << endl;
}
}
return 0;
}
先说明代码逻辑,我定义了一个基类Exception,用来表示异常的基本信息,再定义了一个派生类HttpServeException,其重写了基类Exception的what函数,打印返回的异常信息时,可以得知异常的类型。
然后模拟发送消息的过程,用for循环十次SeedMsg函数,该函数通过产生随机数,通过随机数的范围判断是否需要抛出异常(模拟程序遇到错误的情况),for循环在外面的try块中,for循环调用SeedMsg函数在内try块中,如果SeedMsg函数没有抛出异常,break退出for循环。如果发生了异常,在内try块下捕获异常,判断异常的错误码(是否是网络错误),如果是网络错误,打印信息提示(实际这串信息没必要打印,这里是方便观察现象),如果不是网络错误则再次抛出异常(即退出了for循环,不重复发送消息),程序走到外try块的catch中,由其捕获再次抛出的异常。
最后将程序放到死循环中,一次次的模拟发送消息的过程
可以看到,出现网络错误,程序会重复发送消息,但重复次数有限。使用try的嵌套后,该程序做到了重复发送消息,并对发送过程中产生的特殊异常进行处理,其他异常重新抛出的功能。
异常涉及的资源管理问题(异常安全)
如果一段程序申请了资源,但在资源释放之前抛出异常,则资源不会释放,因为程序跳转到catch块中执行,略过了资源释放语句,结果是造成了内存泄漏。
void Func()
{
int* p = nullptr;
p = new int[1024 * 1024];
if (rand() < RAND_MAX / 2)
{
throw test::Exception("异常发生", 1);
}
delete[] p;
cout << __LINE__ << "delete[] p" << endl;
}
int main()
{
while (1)
{
try
{
Func();
}
catch (test::Exception& e)
{
cout << e.what() << endl;
}
}
return 0;
}
Func函数申请完内存后,发生了异常,程序跳转到main函数中的catch块,略过了释放资源的语句,造成了内存泄漏。
所以申请资源后,如果异常可能发生,就需要在当前函数栈中特地捕获这个异常进行资源释放
void Func()
{
int* p = nullptr;
p = new int[1024 * 1024];
// 捕获异常并完成资源释放
try
{
if (rand() < RAND_MAX / 2)
{
throw test::Exception("异常发生", 1);
}
}
catch (test::Exception& e)
{
delete[] p;
throw;
}
delete[] p;
cout << __LINE__ << "delete[] p" << endl;
}
异常规范
C++98的异常规范是:需要在每个函数后声明该函数是否会抛出异常,并且说明抛出什么异常。
void Func1() throw() // 不会抛异常
void Func2() throw(A, B) // 会抛出A,B类型
void Func3() throw(std::bad_alloc) // 抛出bad_alloc异常
每个异常的类型都要写出,实在有些麻烦,C++11简化了这个规则,当函数不抛出异常时,只要在函数后声明noexcept
void Func4() noexcept
异常的优缺点
异常的优点:
1.比起错误码,异常能返回直观详细的错误消息,可以更好地定位bug
2.比起错误码,调用函数出现错误时,错误码只能通过return语句层层返回,而异常能够直接跳转到catch中返回错误
3.对于一些函数,如构造,析构,使用异常能更好的处理错误,因为它们没有返回值,不能返回错误码,只能在函数中抛出异常
异常的缺点:
1.异常的跳转类似C语言中的goto语句,在大型的工程中跳转使得程序执行流非常混乱,在调用分析程序时,异常的存在使其变得困难
2.异常没有垃圾回收机制,要处理资源只能使用智能指针,增加了学习成本
3.标准库中的异常定义的不太好,导致大家定义自己的异常体系,没有一个统一的标准使得异常非常混乱