C++:异常
一、异常介绍
在C中,传统的异常处理机制有两种:
- assert断言。比如程序发送段错误时,会触发assert,导致程序直接退出。
- 错误码。程序异常返回错误码,虽然每种错误码背后对于一类错误,但程序员还是无法明确错误原因,以及错误位置,需要程序员自身去查找!
所有C++引入了异常机制。当一个函数发生无法处理的错误后,会抛出异常,让函数的直接或间接调用者对该异常进行捕获处理!!C++通过throw抛出异常,当一段段程序可能抛出错误,捕获异常的方式采用try和catch关键字。其中try中存放可能抛出异常的代码,而try中的代码也被称为保护代码;而catch则是对异常机进行捕获,并进行处理!!
try
{
// 保护的标识代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}
二、异常的使用
1)、异常抛出和匹配规则
异常是通过throw抛出对象引发的,该对象的类型决定了激活那个catch块的代码。但在调用链中可能存在多个对同一类型的catch模块。所以被选择的处理代码为调用链中与该对象匹配并且距离抛出异常位置最近的那个!被抛出对象,不是直接抛出对象本身,而是抛出该对象的拷贝副本(原因在于抛出的可能是一个临时对象)
catch捕获异常时,捕获的类型和抛出对象的类型要相同。但也有例外,即我们可以通过catch一个父类,对子类抛出对象进行捕获!通过C++中提供了catch(…)用于捕获任意异常,但该方式无法得知异常错误是啥!!
2)、函数调用链中异常栈的展开匹配规则
抛出异常后,编译器首先会检查throw本身是否在try内部,如果在编译器会查找符合要求的catch语句,然后直接跳到目标catch语句执行catch中的代码!
如果当前函数栈中不存在,此时直接退出当前函数的栈,编译器对当前栈进行销毁。然后在调用函数的栈中继续查找匹配合适的catch语句。如果到达main函数的栈中还没有查找到合适的catch语句,此时编译器会直接终止程序!
一旦编译器查找到合适的catch语句后,编译器会直接从抛出异常的位置直接调转到catch语句处,执行catch语句中的内容。这也意味着该过程可能会导致一些资源释放错误,导致一系列问题。
3)、异常的重新抛出
有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。
下面有这样一段代码:我们用于计算用户输入的两个值相除结果。当除数为0时,抛出异常。但博主在Func()开始时,先申请一段空间。此时一旦抛出异常,会导致空间资源无法正常释放,导致内存泄漏!!
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void Func()
{
int* array = new int[10];
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
return 0;
}
重新抛出:
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void Func()
{
int* array = new int[10];
try {
int len, time;
cin >> len >> time;
cout << Division(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;
}
return 0;
}
4)异常安全
构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题。
5)、 异常规范
异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。函数的后面接throw(),表示函数不抛异常。若无异常接口声明,则此函数可以抛掷任何类型的异常。
但该异常规范仅仅是一些建议性行为。即使列出了可能抛出的异常,但程序最后抛出其他异常,程序还是正常运行的!
所在在c++11中增加noexcept,表示不会抛出任何异常。此时如果程序抛出异常也不会对异常进行捕获!
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;
三、C++标准库的异常体系
C++ 提供了一系列标准的异常,定义在std中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:
四、异常的优缺点
4.1)、C++异常的优点
异常相对于错误码的方式可以清晰准确的展示各自错误信息,甚至可以包含堆栈调用的信息,帮助定位bug。 并且在调用链比较深的函数中,错误码需要层层往上反,而异常会直接跳转到外边!除此在外,很多第三方库都是以异常机制!
部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误。
4.1)、C++异常的缺点
在运行时,异常会导致执行流乱跳,并且非常混乱,导致调试困难!
C++没有垃圾回收机制,资源需要自己管理。异常的产生,可能会导致资源泄漏、死锁等异常安全问题。一般通过RAII进行处理。
C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
异常尽量规范使用,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func() throw();的方式规范化。