异常
异常是指在程序运行阶段的错误,例如被除数为0。在C语言中,我们处理异常的方法是:调用abort()
或者exit()
函数,或者采用返回错误码。C++提供了更好的方法处理异常.
1.传统C方法
1.1 调用abort()
函数
abort()
或exit()
函数在头文件cstdlib
中,它门两会向标准错误流发送信息,然后终止程序。
#include<iostream>
#include<cstdlib>
double foo(double a,double b);
int main()
{
using namespace std;
double a,b;
cout<<"Enter two values: ";
while(cin>>a>>b)
{
double result=foo(a,b);
cout<<"the result: "<<result<<endl;
cout<<"Enter two values: ";
}
}
double foo(double a,double b)
{
if(b==0)
{
std::cout<<"Error! b=0 not allowed!\n";
std::abort();
}
return a/b;
}
如果b=0的话,程序会直接终止,而不会返回main函数。而我们希望的是,即使运行出现异常,程序也不会终止。
1.2 返回错误码
#include<iostream>
#include<cstdlib>
bool foo(double a,double b,double & result);
int main()
{
using namespace std;
double a,b;
cout<<"Enter two values: ";
while(cin>>a>>b)
{
double result;
if(foo(a,b,result))
{
cout<<"the result: "<<result<<endl;
}
cout<<"Enter two values: ";
}
}
bool foo(double a,double b,double & result)
{
if(b==0)
{
std::cout<<"Error! b=0 not allowed!\n";
result=0;
return false;
}
result=a/b;
return true;
}
稍微对上面那个程序做了修改,使得即使出现异常,也会返回main函数。
2. C++ throw-catch
机制
2.1 try块的使用
C++异常机制有3个组成部分:
- 引发异常
- 使用处理程序捕获异常
- 使用
try
块
下面介绍3个关键词
throw
这个相当于
return
,它会引发异常,但是不会终止程序。而且throw
和return
一样会返回信息,一般是字符串或者对象。
catch
这是用来捕获异常的,
throw
返回的信息,会被catch
捕获。
try
try
块,就是在代码块前面加个try
。一般try
块里面就是可能引发异常的语句,在try
块后面会跟一个或者多个catch
来捕获它的异常信息。
#include<iostream>
double foo(double a,double b);
int main()
{
using namespace std;
double a,b;
cout<<"Enter two values: ";
while(cin>>a>>b)
{
double result;
try{
result=foo(a,b);
}
catch(const char* s)
{
cout<<s<<endl;
cout<<"Enter two values: ";
continue;
}
cout<<"the result: "<<result<<endl;
cout<<"Enter two values: ";
}
}
double foo(double a,double b)
{
if(b==0)
throw "Error! b=0 not allowed!";
return a/b;
}
try
块中如果某个语句引发异常,就会被紧随其后的catch
捕获到,并执行。
throw
关键词和return
特别像,它终止函数的执行,但是 throw
不是把控制器交给调用程序,而是沿函数调用序列后退,直到找到包含try
块的函数。
catch
块类似于函数定义,但他不是函数定义,他是一个异常处理程序,而char* s
是表明该处理程序和字符串异常相匹配。异常引发字符串被赋给s,然后执行catch
块代码。
noexcept
关键词
这是一种特殊的异常规范,它出现在函数原型或者定义中,例如:void foo() noexcept{...}
它告诉编译器,这个函数保证不会出现异常。
2.2 将对象用作异常类型
我们推荐将对象用作异常类型,因为对象里面包含的信息很多,而且类具有很多很好的性质。
看看下面这段代码:
#include<iostream>
#include<cmath>
double average(double a,double b);
double foo(double a,double b);
double fun(double a,double b,double c);
class Bad_foo
{
private:
double a;
double b;
public:
Bad_foo(double x1=0,double x2=0):a(x1),b(x2){}
void mesg() const
{
std::cout<<"foo("<<a<<","<<b<<") error!\n";
std::cout<<"foo(a,b) arguments: b=0 not allowed!\n";
}
};
class Bad_ave
{
private:
double a;
double b;
public:
Bad_ave(double x1=0,double x2=0):a(x1),b(x2){}
void mesg() const
{
std::cout<<"average("<<a<<","<<b<<") error!\n";
std::cout<<"sqrt(double a) arguments: a<0 not allowed!\n";
}
};
int main()
{
using namespace std;
double a,b,c;
cout<<"Enter three values: ";
while(cin>>a>>b>>c)
{
double result1,result2,result3;
try{
result1=foo(a,b);//b!=0
cout<<"result1: "<<result1<<endl;
result2=average(a,b);//a*b>=0
cout<<"result2: "<<result2<<endl;
result3=fun(a,b,c);//b*c>0
cout<<"result3: "<<result3<<endl;
cout<<"Enter three values: ";
}
catch(Bad_foo & bad)
{
bad.mesg();
cout<<"Enter three values: ";
continue;
}
catch(Bad_ave & bad)
{
bad.mesg();
cout<<"Enter three values: ";
continue;
}
}
}
double foo(double a,double b)
{
if(b==0)
throw Bad_foo(a,b);
return a/b;
}
double average(double a,double b)
{
using std::sqrt;
if(a*b<0)
throw Bad_ave(a,b);
return sqrt(a*b);
}
double fun(double a,double b,double c)
{
double ret=foo(a,average(b,c));
return ret;
}
Enter three values: 1 2 3
result1: 0.5
result2: 1.41421
result3: 0.408248
Enter three values: 1 0 3
foo(1,0) error!
foo(a,b) arguments: b=0 not allowed!
Enter three values: 1 -2 3
result1: -0.5
average(1,-2) error!
sqrt(double a) arguments: a<0 not allowed!
Enter three values: 1 2 -3
result1: 0.5
result2: 1.41421
average(2,-3) error!
sqrt(double a) arguments: a<0 not allowed!
Enter three values: 1 2 0
result1: 0.5
result2: 1.41421
foo(1,0) error!
foo(a,b) arguments: b=0 not allowed!
Enter three values: q
上面代码反映了两个事实:
try
块中代码,如果出现异常,那么会立即将控制权转到块尾的异常处理程序,例如上例中,如果result1
出错,那么后面就不会执行result2
和result3
。- 栈解退
在计算result3
时,我们调用了函数fun()
,而fun()
中使用了可能出现异常的foo()
和average()
,如果出现异常,那么程序控制权不会返回给调用函数,而是返回给main()
函数中try
块尾的异常处理情况。
假设函数由于出现异常而终止,而程序也会释放栈中的内存,但不会在释放栈的第一个返回地址(即调用函数地址)后停止,而是继续释放栈,直到找到一个位于try
块中的返回地址,随后,控制权将转到块尾的异常处理程序,这个过程叫做栈解退。
下面时return
和throw
的区别:
2.3 其他特性
throw-catch
机制类似于函数参数和函数返回机制,但是又两点不同,一点是栈解退,另一点是引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch
块中指定的是引用。
class problem{...};
void super()
{
if(oh_no)
{
problem oops;
throw oops;
}
}
...
try{
super();
}
catch(problem & p)
{
...
}
上面这段代码中,p指向的是oops
的副本而不是本身。这样设计的原因是:在super()
函数发生异常时,oops
将不复存在(即被退栈了),此外,使用对象引用的好处是可以引用所有派生类对象。
例如:
class bad_1{...};
class bad_2:public bad_1{...};
class bad_3:public bad_2{...};
...
void duper()
{
if(oh_no)
throw bad_1();
if(rats)
throw bad_2();
if(drat)
throw bad_3();
}
...
try{
duper();
}
catch(bad_3 &be){...}
catch(bad_2 &be){...}
catch(bad_1 &be){...}
如果将bad_1 &
处理程序放到最前面,他将捕获bad_1
,bad_2
,bad_3
;通过按相反的顺序排列,bad_3
异常将被bad_3 &
处理程序捕获。
总之,通过正确地排列catch块的顺序,让您可以在如何处理异常方面有选择的余地。
有时,我们并不知道被调用的函数可能引发哪些异常,那么我们可以使用下面这样的catch
程序:
catch(...){//statement}
,用...
代替catch
的参数列表。
这样子,我们可以类似于switch
语句:
try{
duper();
}
catch(bad_3 &be){/*statemnet*/}
catch(bad_2 &be){/*statement*/}
catch(bad_1 &be){/*statement*/}
catch(...)
{
//statement
}
catch(...){//statement}
类似于switch
中的default
语句。
3. exception
类
在头文件exception
中定义了exception
类,这个类就是处理异常的,C++把它作为其他异常类的基类。在我们自己写的代码中,我们可以引发exception
异常.也可以将它作为基类。这个类中有一个what()
的虚拟成员函数,它返回一个字符串。由于这是一个虚方法,因此在派生类中我们可以重新定义它。
#include<exception>
class Bad_hmean:public std::exception
{
public:
const char* what(){return "bad arguments to hmean()";}
...
};
class Bad_gmean:public std::exception
{
public:
const char* what(){return "bad arguments to gmean()";}
...
};
我们也可以直接使用现成的exception
类来处理异常:
try{
...
}
catch(std::exception & e)
{
cout<<e.what()<<endl;
}
3.1 stdexcept
异常类
头文件stdexcept
中定义了logic_error
和runtime_error
类,它们都是从exception
类中公有派生出来的
class logic_error :public exception
{
public:
explicit logic_error(const string& what_arg);
...
};
注意,这些派生类都接受一个string
对象作为参数,该参数提供了方法what()
以C风格字符串方式返回的字符串。
logic_error
和runtime_error
类又被做成两个派生类系列的基类。
logic_error
异常类系列:描述了典型的逻辑错误,逻辑错误可以通过合理编程来避免。
这些类是:
domain_error
将参数传给函数时,参数不在其定义域中,这样会引发定义域异常invalid_argument
传给函数一个意料之外的值,会引发无效参数异常length_error
没有足够空间来执行所需操作,会引发长度异常,例如string::append()方法合并字符串时长度超过最大允许长度时out_of_bounds
索引错误,例如operator[]
中使用的索引无效。
runtime_error
异常类系列:描述了可能在运行期间发生但难以预计和防范的错误。
这些类是:
overflow_error
上溢错误underflow_error
下溢错误range_error
结果不在允许范围内,但是没有溢出的错误
总之,目前我们介绍了C++自带的7个异常类,和3个基类。
你可以这样使用它们:
try{
...
}
catch(out_of_bounds & oe)
{...}
catch(logic_error & oe)
{...}
catch(exception &oe)
{...}
上面这段代码,首先捕获
out_of_bounds
异常,然后捕获其他logic_error
异常,最后统一捕获exception
及其派生类的一切异常.
如果你觉得这些异常,并不能满足需求,你可以从logic_error
或者runtime_error
派生出一个异常类。
3.2 bad_alloc
异常和new
头文件new
中包含一个bad_alloc
异常类的声明,这个类是从exception
类公有派生类的。bad_alloc
在无法分配请求的内存时,会引发该异常。
例如:
#include<iostream>
#include<new>
struct Big
{
double stuff[2000000];
};
int main()
{
using std::cout;
using std::endl;
using std::bad_alloc;
Big *pb;
try{
cout<<"Trying to get a big block of memory:\n";
pb=new Big[10000];
}
catch(bad_alloc& ba)
{
cout<<"Caught the exception!\n";
cout<<ba.what()<<endl;
}
delete [] pb;
}
Trying to get a big block of memory:
Caught the exception!
std::bad_alloc
在这里,方法
what()
返回字符串std::bad_alloc
- 空指针和
new
很多代码都是在new
失败时,返回空指针时编写的。为处理这种情形,C++标准提供了一种在失败时返回空指针的new
,其用法如下:
pb = new(std::nothrow) Big[10000];
if(pb==0)
{
cout<<"can't allocate memory.\n";
}
3.3 异常、类和继承
//异常6.h
#include<stdexcept>
#include<string>
class Sales
{
public:
enum{MONTHS=12};
private:
double gross[MONTHS];
int year;
public:
class bad_index:public std::logic_error
{
private:
int bi;//bad index value
public:
explicit bad_index(int ix,const std::string &s="Index error in Sales object\n")
:std::logic_error(s),bi(ix){}
int bi_val() const{return bi;}
virtual ~bad_index(){}
};
explicit Sales(int yy=0):year(yy)
{
for(int i=0;i<MONTHS;i++)
gross[i]=0;
}
Sales(int yy,const double *gr,int n)
{
year=yy;
int lim=(n<MONTHS)?n:MONTHS;
int i;
for(i=0;i<lim;i++)
gross[i]=gr[i];
for(;i<MONTHS;i++)
gross[i]=0;
}
virtual ~Sales(){}
int Year() const {return year;}
virtual double operator[](int i) const;
virtual double & operator[](int i);
};
class LabeledSales:public Sales
{
private:
std::string label;
public:
class nbad_index:public Sales::bad_index
{
private:
std::string lbl;
public:
nbad_index(const std::string & lb,int ix,
const std::string &s="Index error in LabeledSales object\n")
:Sales::bad_index(ix,s),lbl(lb){}
const std::string & label_val() const{return lbl;}
virtual ~nbad_index(){}
};
explicit LabeledSales(const std::string &lb="none",int yy=0):Sales(yy),label(lb){}
LabeledSales(const std::string &lb,int yy,const double *gr,int n):Sales(yy,gr,n),label(lb){}
virtual ~LabeledSales(){}
const std::string & Label() const{return label;}
virtual double operator[](int i) const;
virtual double & operator[](int i);
};
double Sales::operator[](int i) const
{
if(i<0||i>=MONTHS)
throw bad_index(i);
return gross[i];
}
double & Sales::operator[](int i)
{
if(i<0||i>=MONTHS)
throw bad_index(i);
return gross[i];
}
double LabeledSales::operator[](int i) const
{
if(i<0||i>=MONTHS)
throw nbad_index(label,i);
return Sales::operator[](i);
}
double& LabeledSales::operator[](int i)
{
if(i<0||i>=MONTHS)
throw nbad_index(label,i);
return Sales::operator[](i);
}
//异常6.cpp
#include<iostream>
#include"异常6.h"
int main()
{
using std::cout;
using std::cin;
using std::endl;
double vals1[12]=
{
1220,1100,1122,2212,1232,2344,
2884,2393,3302,2922,3002,3544
};
double vals2[12]=
{
12,11,22,21,32,34,
28,29,33,29,32,35
};
Sales sales1(2011,vals1,12);
LabeledSales sales2("Blogstar",2012,vals2,12);
cout<<"First try block:\n";
try
{
int i;
cout<<"Year = "<<sales1.Year()<<endl;
for(i=0;i<12;i++)
{
cout<<sales1[i]<<" ";
if(i%6==5)
cout<<endl;
}
cout<<"Year = "<<sales2.Year()<<endl;
cout<<"Label = "<<sales2.Label()<<endl;
for(i=0;i<12;i++)
{
cout<<sales2[i]<<" ";
if(i%6==5)
cout<<endl;
}
cout<<"End of try block 1.\n";
}
catch(LabeledSales::nbad_index &bad)
{
cout<<bad.what();
cout<<"Company: "<<bad.label_val()<<endl;
cout<<"bad index: "<<bad.bi_val()<<endl;
}
catch(Sales::bad_index &bad)
{
cout<<bad.what();
cout<<"bad index: "<<bad.bi_val()<<endl;
}
cout<<"\nNext try block:\n";
try
{
sales2[2]=37.5;
sales1[20]=23345;
cout<<"End of try block 2.\n";
}
catch(LabeledSales::nbad_index &bad)
{
cout<<bad.what();
cout<<"Company: "<<bad.label_val()<<endl;
cout<<"bad index: "<<bad.bi_val()<<endl;
}
catch(Sales::bad_index &bad)
{
cout<<bad.what();
cout<<"bad index: "<<bad.bi_val()<<endl;
}
cout<<"done\n";
}
First try block:
Year = 2011
1220 1100 1122 2212 1232 2344
2884 2393 3302 2922 3002 3544
Year = 2012
Label = Blogstar
12 11 22 21 32 34
28 29 33 29 32 35
End of try block 1.
Next try block:
Index error in Sales object
bad index: 20
done
对上面程序的说明:
我们将异常类作为公有嵌套类放到我们设计的类中。
当从logic_error
类中派生出自定义异常类时,我们不需要重新写what()
方法,我们在写构造函数的时候,创建logic_error
子对象时,会自动修改what()
方法。
4. 未捕获异常和意外异常
4.1 未捕获异常
异常在引发后,可能没有try
块或者没有匹配的catch
块,导致异常无法被处理,这种异常叫做未捕获异常。
在默认情况下,未捕获异常的出现会直接终止程序,但是我们可以修改这种行为。
默认情况下,未捕获异常的出现会调用terminate()
函数,然后terminate()
会调用abort()
函数来终止程序。现在,我们可以使用set_terminate()
函数来修改terminate()
的调用函数。
set_terminate()
的原型:
typedef void (*terminate_handler)();
terminate_handler set_terminate(terminate_handler f) noexcept;
void terminate() noexcept;
typedef
将terminate_handler
变成函数指针的别名,然后set_terminate()
函数的返回值和参数都是函数地址(函数指针)。并且set_terminate()
和terminate()
函数都是不允许出现异常的。
set_terminate()
和terminate()
都是在头文件exception
中声明的。
#include<cstdlib>
#include<iostream>
#include<exception>
using namespace std;
void myQuit()
{
cout<<"Terminating due to uncaught exception\n";
exit(5);
}
int main()
{
set_terminate(myQuit);
try
{
throw exception();
}
catch(double a){}
}
上面这段代码就会引发未捕获异常,但是我们修改了terminate()
的调用函数,如果未捕获异常出现,terminate()
函数就会调用myQuit()
函数处理未捕获的异常。
4.2 意外异常
C++98采用了异常规范,目前这一功能受到很多人诟病,但是我们仍然允许使用异常规范。
我们不推荐使用规范异常机制,因此这部分知识可以不看。
异常规范是指:我们可以在函数定义或者原型中,规定这个函数只可能出现哪些异常。
void foo1() throw();
void foo2() throw(logic_error,const char*);
void foo3() noexcept;
上面这个
foo1
和foo3
的原型指出函数中不允许出现异常,foo2
的原型指出,函数中只允许出现logic_error
和const char*
异常.
意外异常是指在存在异常规范的情况下,规范中不容许的异常。
注意:noexcept
关键字不属于规范异常,throw()
规范的函数中出现的异常是意外异常,而noexcept
修饰的函数中出现的异常是未捕获异常。
如果发生意外异常,那么程序会调用unexpected()
,然后unexcepted()
会调用terminate()
,后者在默认情况下,调用abort()
来终止程序。我们可以使用set_unexpected()
函数来修改unexpected()
中的调用函数,也可以使用set_terminate()
函数来修改terminate()
的调用函数。
typedef void (*unexpected_handler)();
unexpected_handler set_unexpected(unexpected_handler f) noexcept;
void unexpected() noexcept;
意外异常比未捕获异常复杂且危险,这也是我们抛弃异常规范的原因。
相较于使用set_terminate()
,使用set_unexpected()
会复杂得多:
我们通常提供给set_unexpected()
一个函数地址,这个函数会引发异常。
- 如果新引发的异常和异常规范匹配,我们想象一下,当意外异常出现,程序调用
unexpected()
函数,然后unexpected()
函数引发一个新的异常,而这个异常和异常规范匹配,那么就会正常进行异常处理。总之,**使用预期异常取代意外异常。 - 如果新引发的异常和异常规范不匹配,且异常规范里面没有
std::bad_exception
类型,那么就会调用terminate()
函数,(std::bad_exception
是从exception
中公有派生的,专门处理意外异常的类) - 如果新引发的异常和异常规范不匹配,但是异常规范里面有
std::bad_exception
类型,则不匹配的异常会被std::bad_exception
类型接受,然后正常进行异常处理。
总之,你如果想要自己处理意外异常,你就在使用规范异常的时候,在容许异常类型中添加一个std::bad_exception
类型,然后使用set_unexpexted()
函数,给他传入一个函数地址,这个函数会引发异常,(随便异常都行,引发新异常注意是为了栈解退将程序返回给try块)
#include<exception>
#include<iostream>
using namespace std;
void myUnexpected()
{
cout<<"myUnexpected()"<<endl;
throw;
}
void foo() throw(const char*,bad_exception)
{
int a=2;
throw a;
}
int main()
{
set_unexpected(myUnexpected);
try
{
foo();
}
catch(const char* s){}
catch(bad_exception &e)
{
cout<<"Caught in main()"<<endl;
}
}
myUnexpected()
Caught in main()
上面那个
foo()
函数引发了意外异常,然后我们修改了unexpected()
函数的行为,使之引发新异常,然后就被bad_exception
类型接受,然后程序控制权返回在main
函数中被try
块,然后被catch
到。
5.关于异常的注意事项
我们应该在设计程序的时候就加入异常处理功能,而不是以后再添加。
使用异常会增加程序代码,降低程序运行速度。
异常规范不适用于模板,因为模板函数引发的异常可能随着特定具体化而异
动态内存分配和异常。
void test1(int n)
{
string mesg("I'm trapped in an endless loop");
...
if(oh_no)
throw exception();
...
return;
}
string
类采用动态内存分配.通常,当函数结束时,将为mesg
调用string
的析构函数。肃然throw
语句过早的终止了函数,但他仍然使得析构函数被调用,这要归功于栈解退。因此上面这段代码时没有问题的。
void test2(int n)
{
double *ar= new double[n];
...
if(oh_no)
throw exception();
...
delete[] ar;
return;
}
上面这段代码有问题,在栈解退的时候,将删除栈中的变量
ar
,但是由于程序过早引发异常,导致delete[]
语句被忽略,这就导致内存泄漏(即指针消失,内存没被释放)。
这种泄漏是可以避免的:
void test2(int n)
{
double *ar= new double[n];
...
try
{
if(oh_no)
throw exception();
}
catch(exception &e)
{
delete[] ar;
throw;//throw后面不加参数的意思是,重新返回原来异常,即exception
}
...
delete[] ar;
return;
}
实际上,异常处理是非常困难的事情,你必须了解库文档和源代码中的异常和错误的理解细节。