异常处理
异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理。异常使得我们能够将问题的检测与解决过程分离开来。
异常的抛出和捕获
在C++中,我们通过抛出一条表达式来引发一个异常。被抛出的表达式的类型以及当前的调用链共同决定了哪段代码将被用来处理该异常。被选中的处理代码是在调用链中与抛出对象类型匹配的最近的代码。
当执行一个throw时,跟在throw后面的语句将不再执行(有点类似于return语句)。相反,程序的控制权从throw转移到与之匹配的catch模块,catch是位于同一个函数的局部catch,也可能位于直接或间接调用了发生了异常的函数的另一个函数中。
控制权从一处转移到另一处包含以下两点含义:
*沿着调用链的函数可能会提早退出。
*一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁。
栈展开
栈展开过程沿着嵌套函数的调用链不断查找,直到找到与异常匹配的catch子句为止,如果找到main函数也没有找到,则退出主函数后查找过程终止。假设找到了一个匹配的catch子句,则程序进入该子句并执行其中的代码。当执行完catch子句后,找到与try块关联的最后一个catch子句之后的点,并从这里继续向后执行。如果没有找到匹配的catch,程序将调用标准库函数terminate终止程序的执行过程。
下面看一段代码
#include<iostream>
using namespace std;
void fun1()
{
try{
throw string("fun1 false");
cout << "fun1()" << endl;
}
catch (int &e)
{
}
catch (string &e)
{
cout << "string false" << endl;
throw;//异常的重新抛出
}
}
void fun2()
{
try{
fun1();
}
catch (string &e)
{
cout << "string false" << endl;
}
catch (...)//可捕获任意类型的异常,如果有直接匹配的catch则不选它,如果没有,则选择它
{
cout << "未知错误" << endl;
}
cout << "fun2()" << endl;
}
int main()
{
fun2();
system("pause");
return 0;
}
fun1( )中抛出异常,现在fun1( )中找相匹配的catch,但是没有找到,所以返回fun2中找,找到了,然后进入catch继续执行,执行完catch后,直接跳到cout << "fun2()" << endl这里执行。
异常对象的生命周期:异常对象的生命周期是不确定的,只有找到相匹配的catch并交给catch执行,异常的生命周期才结束。
异常捕获的匹配规则
异常对象的类型与catch说明符的类型必须完全一致。
只有以下几种情况类外:
1.允许从非const对象到const的转换。
2.允许从派生类型到基类类型的转换。
3.将数组转换为指向数组类型的指针,将函数转换为指向函数类型的指针。
异常的重新抛出
有时一个单独的catch语句不能完整的处理某个异常。在执行了某些校正操作之后,当前的catch可能会决定由调用链更上一层的函数接着处理异常 。或者是catch处理后,需要将这些异常显示给用户看。这时就需要通过重新抛出(throw 不包含任何表达式)的操作将异常传递给另一个catch语句。
异常构造函数和析构函数
构造函数完成对象的构造和初始化,需要保证不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
析构函数主要完成资源的清理工作,需要保证不要在析构函数中抛出异常,否则可能导致资源泄漏(内存泄漏等)。
C++标准库定义的exception类
exception是C++定义的一个标准的异常类,通常我们通过继承exception类定义合适的异常类。
◆ 1、exception类的接口如下:
namespace std //注意在名字空间域std中
{
class exception
{
public:
exception() throw() ; //默认构造函数
exception(const exception &) throw() ; //复制构造函数
exception &operator=(const exception&) throw() ;
//复制赋值操作符
virtual ~exception() throw() ; //析构函数
virtual const char* what() const throw() ;
//返回一个C风格的字符串,目的是为抛出的异常提供文本描述
};
}
◆ 2、C++标准库提供的逻辑异常:
invalid_argument异常,接收到一个无效的实参,抛出该异常。
out_of_range异常,收到一个不在预期范围中的实参,则抛出。
length_error异常,报告企图产生“长度值超出最大允许值”的对象
domain_error异常,用以报告域错误(domain error)。
◆ 3、C++标准库提供的运行时异常:
range_error异常,报告内部计算中的范围错误。
overflow_error异常,报告算术溢出错误。
underflow_error异常,报告算术下溢错误。
以上三个异常是由runtime_error类派生的。bad_alloc异常,由基类exception派生,当new()操作符不能分配所要求的存储区时,会抛出该异常。
#include<iostream>
using namespace std;
#include<string>
class Exception1:public exception
{
public:
Exception1(int errId = 0, const char* errMsg = "")
:_errId(errId), _errMsg(errMsg)
{}
virtual const char* what() const
{
cout << "errId:" << _errId << endl;
cout << "errMsg:" << _errMsg << endl;
return _errMsg.c_str();
}
private:
int _errId; // 错误码
string _errMsg; // 错误消息
};
void Func1()
{
try
{
throw Exception1(1, "Exception1");
throw Exception1(2, "Exception2");
}
catch (exception & e)
{
e.what();
}
}
void Func()
{
try{
int* p = new int[0x7fffffff / 4];
}
catch (bad_alloc&ba)
{
cout << "bad_alloc caught: " << ba.what() << '\n' << endl;
}
catch (exception & e)
{
cout << "exception<-bad_alloc caught: " << e.what() <<'\n';
}
}
int main()
{
Func1();
Func();
system("pause");
return 0;
}
异常和返回错误码相比优缺点是什么
优缺点:
1.异常捕获后能清晰的知道发生了什么错误,错误码只是整型,不能描述错误原因。
2.异常会打断执行流,影响调试分析代码。
3.异常有异常安全问题,需要尽量使用RAII配合。
4.很多第三方库使用异常。
5.很多测试框架使用异常gtest/gmock–vt。