异常的处理思路一般是根据一些条件(如超出定义域、值域等),引发相应异常(可以是值,也可以是类),后再处理(如catch块)。异常处理是程序的重要组成部分,可以有效防止程序崩溃,也能方便去除程序错误,另一方面异常也增加了代码量。
目录
几种异常处理的常用方法
可以使用以下方法处理异常:
#使用stdlib中的abort()方法直接结束程序,能避免程序崩溃但程序直接被关闭
#使用返回值(即错误码)来检测异常,方便检测但不适用于任意值均可以作为输入的情况
#使用对象处理异常,可自定义且方便检测但不完善
#使用异常机制(即try-catch-throw)处理异常,体系完善但还是增加了代码量
虽然异常处理的几种方法都有缺点,但异常仍然是保证程序正常进行所不可缺少的。
使用abort()与exit()处理异常
abort()和exit()方法用于在异常的情况直接结束程序。exit()会刷新文件缓冲区,但不报告信息;abort()会返回值(但具体值取决于各实现)告诉系统处理失败,是否刷新文件缓冲区则取决于实现)。
abort()和exit()方法均位于标准库中,使用前记得包含头文件cstdlib。使用代码如下:
#include<iostream>
#include<cstdlib>
using std::abort;
using std::cout;
using std::endl;
using std::cin;
double hmean(const double &a,const double &b);
int main()
{
double res=hmean(0,1);
cin.get();
return 0;
}
double hmean(const double &a,const double &b)
{
if(a==0 || b==0)
{
cout<<"The divider can't be zero. Please enter another divider."<<endl;
abort();
}
else
return 2.0*a*b/(a+b);
}
-----输出结果-----
The divider can't be zero. Please enter another divider.
(并弹出窗口提示终止程序)
若使用exit()则需要输入一个int类型值,且最后不会看到消息,程序很快就退出了。
返回错误码处理异常
可以借鉴cin.get()方法到达文件结尾时返回EOF的思路,使函数遇到异常时返回一个异常的值。例如可以把上文中的hmean的返回值改成bool类型,但此时需要一个新的参数作为引用传入,以保存答案。代码如下:
#include<iostream>
#include<cstdlib>
using std::abort;
using std::cout;
using std::endl;
using std::cin;
bool hmeanB(const double &a,const double &b,double &res);
int main()
{
//double res=hmean(0,1);
double resB;
hmeanB(1.0,2.0,resB);
cout<<resB;
cin.get();
return 0;
}
bool hmeanB(const double &a,const double &b,double &res)
{
if(a==0 || b==0)
return false;
else
{
res=2.0*a*b/(a+b);
return true;
}
}
-----输出结果-----
The divider can't be zero. Please enter another divider.
异常机制
C++的异常机制提供了将控制权从程序的一个部分传递到另一个部分的途径。
- 异常处理机制的组成——try-catch-throw机制
异常处理机制由三个部分组成:
①引发异常——throw语句;
②使用异常处理程序(exception handler)捕获异常——catch语句;
③使用try块(实际上假设程序正常运行)——try语句
throw语句表示引发异常,语句中的值(比如字符串或者对象)表明异常类型;catch语句则根据异常类型确定要采取的措施,某类异常被引发以后就执行相应标签的代码;try语句则代表异常可能被激活的代码块,表明应该这些代码可能会引发异常(因此也可以在try块中写throw,只需要在catch块中处理好相应类型即可)。
异常处理机制的声明、定义、调用代码如下:
try语句后可以跟多个catch块,以对应不同异常引发。/*声明 try { ...//可能激活异常的语句 } catch(throw的值,例如字符串,或者类对象)//catch块1 { ... } catch(throw的值,例如字符串,或者类对象)//catch块2 { ... } ... 返回类型 函数名(参数列表) { ... throw 字符串或者类;//引发异常 } */ #include<iostream> #include<cstdlib> #include<exception> using std::cout; using std::endl; using std::cin; double hsumE(const double &a,const double &b); int main() { try//激活异常 { cout<<"Here is the answer of 1/0+1/1:"<<endl; double ans=hsumE(0,1); } catch(const char * errStr)//处理异常 { cout<<errStr<<endl; } cin.get(); return 0; } double hsumE(const double &a,const double &b) { if(a==0 || b==0) throw "The number can't be zero.";//引发异常 else return 2.0*a*b/(a+b); }
程序首先执行try块,若没有异常则跳过catch块,执行其后的代码;若有异常则执行相应catch块代码。
- 使用对象处理异常
如上文示例代码所示,还可以创建相应类型或者直接使用C++自带的exception类处理异常。对于前者而言,需要定义一个专门类。例如上面的调和和程序可以改成如下代码://badHmean.h #include<iostream> using std::cout; using std::endl; class badHmean { public: badHmean(double val_a,double val_b):a(val_a),b(val_b){} ~badHmean(){} void sendErrMsg(); private: double a; double b; }; void badHmean::sendErrMsg() { cout<<"The numbers can't be zero or contrast number."<<endl; } //user.cpp #include<iostream> #include<cstdlib> #include"badHmean.h" using std::cout; using std::endl; using std::cin; double hsumEO(const double &a,const double &b); int main() { try { cout<<"Here is the answer of 1/0+1/1:"<<endl; double ans=hsumEO(0,1); } catch(badHmean &obj_badHmean) { obj_badHmean.sendErrMsg(); cout<<"Bye"; } cin.get(); return 0; } -----输出----- Here is the answer of 1/0+1/1: The numbers can't be zero or contrast number. Bye
- throw语句的功能、栈解退
· throw与return的比较
throw语句有点像return语句,因为它也能终止函数的执行,也能返回一个值,但是throw将沿着函数调用的序列(即栈)后退,将控制权交给上一个能处理异常的函数(即含有try块和catch块的函数),同时并将throw和try语句之间的调用序列的自动内存释放。
return也返回一个值,但其将控制权交给主调函数或者调用该函数的函数。同时,每个return只负责它所在的函数调用。
· 栈解退
throw语句将沿着函数调用的序列(即栈)后退,将控制权交给上一个能处理异常的函数(即含有try块和catch块的函数),这个过程即栈解退(unwinding the stack)。在这个过程中,栈中的自动变量也会被释放。
· 二次抛异常
当可能引发异常的函数抛出异常后,也可以继续在catch块中继续抛异常,以让下一个能处理异常的程序处理异常。只需在catch块中再写一个throw即可://badHmean.h #include<iostream> using std::cout; using std::endl; class badHmean { public: badHmean(double val_a,double val_b):a(val_a),b(val_b){} ~badHmean(){} void sendErrMsg(); private: double a; double b; }; void badHmean::sendErrMsg() { cout<<"The numbers can't be zero or contrast number."<<endl; } //user.cpp #include<iostream> #include<cstdlib> #include<cmath> #include"badHmean.h" #include"badGmean.h" using std::cout; using std::endl; using std::cin; double hsumEO(const double &a,const double &b); void testErr(); int main() { try { testErr(); } catch(badHmean &obj_badHmean) { cout<<"badHmean happened."<<endl; } cin.get(); return 0; } double hsumEO(const double &a,const double &b) { if(a==0 || b==0 || a==-b) throw badHmean(a,b); else return 2.0*a*b/(a+b); } void testErr() { try { hsumEO(0,1); } catch(badHmean & obj_badHmean) { obj_badHmean.sendErrMsg(); throw; } } -----输出----- The numbers can't be zero or contrast number. badHmean happened.
- 其他特性
异常还具有以下特点,在写的时候需要注意,在此做一些记录:
①即使catch块中写的是异常类的引用,实际在编译时还是会创建一个异常类对象副本(如badHmean),因为这样的对象再函数执行完以后就不存在了。
②使用异常类的引用的好处是,当异常类有继承的关系时,基类引用可以执行派生类对象,但需要注意的是,此时catch块的顺序应与派生的顺序相反,否则就不能与派生异常类匹配。
③可以在catch块中使用省略号,表示未知异常,以处理已知异常外的其他异常(有点像switch语句)。这点很有用,因为如果发生了未知的异常,那么程序会直接终止。示例代码如下:... try { testErr(); } catch(badHmean &obj_badHmean) { cout<<"badHmean happened."<<endl; } catch(...) { ...//其他语句 } ...
C++中的异常类——exception、stdexcep、bad_alloc
C++中提供了exception类,其中包含了许多的异常类型,其位于exception头文件中,使用时记得包含它。以下是对它的一些介绍。
- exception类
exception类可以作为自定义的异常类型(如前文提到的badHmean)的基类。其中有一个what()虚方法,其返回一个字符串(具体值随实现而异),可以在派生时重写。
- stdexcep中的logic_error、runtime_error
头文件stdexcep定义了logic_error、runtime_error两大类及它们各自的派生类,它们也是从exception类派生出来的。这些异常类的构造函数均以string对象为参数,并且这些参数将作为what()方法的返回值。
logic_error表示可以通过修正语句来修复的编程错误,runtime_error则表示无法避免的问题(解决它需要考虑是否触碰到了限值)以下是各类的简介:
①logic_error类
logic_error表示逻辑错误,以下各类均是从logic_error派生出来的:
· domain_error
即定义域异常,在使用参数超出定义范围后引发。
· invalid_argument
即无效参数,当使用参数不是指定的类型或者值时引发。
· length_error
空间不足以执行操作时引发的异常,如string类的append()方法在合并得到的字符串长度超限以后即引发该异常。
· out_of_bounds
即索引异常,当使用数组的索引值无效时引发。
以下是一些调用代码示例:
②runtime_error类//user.cpp #include<iostream> #include<cstdlib> #include<cmath> #include<stdexcept> using namespace std; double hsumStdexc(const double &a,const double &b); int main() { try { cout<<"Here is the answer of 1/0+1/1:"<<endl; double ans=hsumStdexc(0,1); } catch(domain_error & objErr) { cout<<objErr.what(); } cin.get(); return 0; } double hsumStdexc(const double &a,const double &b) { if(a==0 || b==0 || a==-b) throw domain_error("The numbers can't be zero or contrast number."); else return 2.0*a*b/(a+b); } -----输出----- Here is the answer of 1/0+1/1: domain error: The numbers can't be zero or contrast number.
runtime_error类主要描述了可能在运行期间发生的难以预料的错误。以下各类均是从runtime_error派生出来的:
· range_error
即值域异常,当计算出的结果超过函数的值域时引发
· overflow_error
即上溢,当计算出的整数或者浮点数大于最大允许值时引发。
· underflow_error
即下溢,当计算出的结果小于最小非零值(是一个浮点数)时引发。
- bad_alloc与new
当使用动态内存时,若分配内存失败,new将返回空指针(这是C++的老做法),或者引发bad_alloc异常,这两种行为引发哪个取决于编译器。
bad_alloc类在头文件new中,其也是从exception类派生出来的,其使用方法和前文所述的其他异常类类似,但其what()的返回值取决于具体实现(例如vs2012中bad_alloc类的what()的返回值为"bad allocation")。
异常与类——异常类的嵌套、继承
异常类也是类,因此也可以嵌套在类中,也可以被继承,只要满足正常类的嵌套、继承规则即可。由于相关过程与普通类的嵌套、继承相似,此处仅作记录。
异常与动态内存分配
和正常的动态内存管理一样,如果使用了new(或者new []),那么一定要配套使用delete(或者delete [])。当delete或者delete []语句在throw语句之后,那么分配的动态内存将会无法释放,导致内存泄漏,此时需要在catch块中写上delete或者delete []语句。
例如:
void testFunc1()//随便定义的一个函数
{
int *pint=new int;
if(...)//引发异常条件
throw ...;//异常值
}
void testFunc2()//随便定义的一个函数
{
int *pint=new int;
try
{
//位置1:int *pint=new int;
if(...)//引发异常条件
throw ...;//异常值
}
catch(...)
{
delete pint;
...//处理语句
}
}
在testFunc1()中,当引发异常时,控制权将交给调用程序,此时pint的内存就没法释放,且后续的catch块也无法释放,造成了内存泄漏。在testFunc2()中,try-catch块都在函数域内,此时new与delete搭配着使用,所以动态内存就可以被释放。