标准异常:
使用标准库定义的异常要比先前的自己创建异常类方便得多,当然,也可以从标准异常类中派生出自己的类。
所有的标准异常都是从exception类派生的,该类定义在<exception>中,主要的两个派生类为logic_error和runtime_error,这两个类定义在<stdexcept>中(这个头文件包含exception)。logic_error描述程序中出现的逻辑错误,如传递无效的参数。runtime_error描述无法预料的事造成的错误,无硬件错误或内存耗尽。这两类错误提供了std::string的构造参数,可以将信息保存在异常对象中,通过exception::what( )函数,可以从对象中得到保存的信息:
#include <stdexcept>
#include <iostream>
using namespace std;
class MyError: public runtime_error{
public:
MyError(const string& msg=""):runtime_error(msg){}
};
int main()
{
try{
throw MyError("my message");
}catch(MyError& x){
cout<<x.what()<<endl;
}
}
尽管runtime_error的构造函数把消息保存在std::exception在对象中,但std::exception并没有提供一个参数类型为std::string的构造函数,所以还是最好是派生自己的异常类。
输入输出流异常ios::failure也是由exception派生的,但它没有子类。
异常规格说明
有时不要求程序提供资料告诉函数的使用者在函数使用时会抛出什么异常,但这样的话,函数的使用者无法确定如何编码捕获可能出现的异常。C++提供一种语法告诉函数的使用者所抛出的异常,称为异常规格说明,它是函数声明的修饰符,放在参数列表后面。
函数可能抛出的异常在throw后面的括号中
void f() throw(toobig,toosmall,divzero);
不添加该修饰符则表明可能抛出任何异常。
void f() throw()表示不会抛出任何异常
1.unexcepted()函数
如果函数抛出的异常没有在异常规格说明中,那么unexcepted()函数就会被调用,默认该函数会调用之前的terminate()函数。
2.set_unexcepted()函数
可以设置自己的unexcepted()函数,例如:
#include <exception>
#include <iostream>
using namespace std;
class Up{};
class Fit{};
void g();
void f(int i) throw(Up,Fit){
switch(i){
case 1:throw Up();
case 2:throw Fit();
}
g();
}
//void g(){} //version1
void g(){throw 47;} //version2
void my_unexcepter(){
cout<<"unexcepted exception thrown"<<endl;
}
int main()
{
set_unexpected(my_unexcepter);
for(int i=1;i<=3;i++)
{
try{
f(i);
}catch(Up){
cout<<"up caught"<<endl;
}catch(Fit){
cout<<"Fit caught"<<endl;
}
}
}
典型的unexcepted处理器会将错误记录日志,然后调用exit终止程序。它也可以抛出另外一个异常或调用abort()。如果它抛出的异常类型不在违反触发unexcepted的函数的异常规格说明,那么程序将会恢复到被调用的位置重新开始异常匹配。
如果仍旧不符合异常规范,下面两种情况之一将会发生:
1.如果函数的异常规格说明中包括std::bad_exception,那么抛出的异常将会被换成这个对象,然后重新匹配
2.如果不包含std::bad_exception,那么直接调用terminate()函数。
#include <exception>
#include <iostream>
#include <cstdio>
#include <stdlib.h>
using namespace std;
class A{};
class B{};
void my_thandler(){
cout<<"terminate called"<<endl;
exit(0);
}
void my_unhandler1(){throw A();}
void my_unhandler2(){throw;}
void t(){throw B();}
void f() throw(A) {t();}
void g() throw(A,bad_exception){t();}
int main()
{
set_terminate(my_thandler);
set_unexpected(my_unhandler1);
try{
f();
}catch(A&){
cout<<"cathc an A from t"<<endl;
}
set_unexpected(my_unhandler2);
try{
g();
}catch(bad_exception&){
cout<<"catch a bad_exception from g"<<endl;
}
try{
f();
}catch(...){
cout<<"this will never be print"<<endl;
}
}
异常规格说明和继承
先看一个例子:
#include <iostream>
using namespace std;
class Base{
public:
class BaseException{};
class DerivedException: public BaseException{};
virtual void f() throw(DerivedException){
throw BaseException();
}
};
class Derived:public Base{
public:
void f() throw(BaseException){
throw BaseException();
}
virtual void g() throw(DerivedException){
throw DerivedException();
}
};
由于Derived::f()违反了Base::f()的规定,所以编译会报错。
异常安全
在栈容器中,pop()的成员函数声明如下:
void pop();
该函数仅仅只是删除了栈顶的元素,而为了获得栈顶元素,需要在pop()在前调用top()。这么做的很重要的原因就是栈必须保证异常安全。
假设使用动态数组实现一个栈(data数组名),pop具有返回值:
template<class T>T stack<T>::pop(){
if(count==0)
throw logic_error("stack underflow");
else
return data[--count];
}
如果为了得到返回值而调用拷贝构造函数,函数却在最后一行抛出一个异常,函数没有将应该退栈的元素返回,但是count的值已经减1,所以,要遵守内聚设计原则,每个函数只做一件事
什么时候避免使用异常:
1、不要在异步事件中使用
2、不要在处理简单错误的时候使用异常
3、不要将异常用于程序的流程控制
4、不要强迫自己使用异常
5、新异常,老代码