异常
一、异常的概述
- 程序有时会遇到运行阶段错误,导致程序无法正常的运行下去。
- 例如:程序可能试图打开一个不可用的文件,请求过多的内存,或者遭到不能容忍的值。
- 通常,程序员都会试图预防这种意外情况。所以c++出现了异常的功能。
1.基本方法
讨论异常之前,了解可使用的一些基本方法,例:计算两个的调和平均数的函数为例。两个数的调和平均数的定义是:这两个数字倒数的平均值的倒数。表达式:2.0xy/(x+y)
如果y是x的负值,则上述公式将导致被零除:这是不允许的运算。对于被零的情况,编译器将生成一个表示无穷大的特殊浮点值,cout将这种值显示为Inf,inf,INF或类似的东西。程序也可能会崩溃。
2.调用abort()
- 对于上面的情况,处理方法之一,若参数是另一个参数的负值,调用abort()函数。
- 函数头文件
cstdlib(stdlib.h)
,实现是向标准错误流(即cerr使用的错误流)发送信息: 程序异常终止,返回随实现而异的值,处理失败。 abotr()
是否刷新文件缓冲区取决于实现。也可以使用exit()
,该函数刷新缓冲区,不显示信息。
实例:
#include <iostream>
#include<cstdlib>
using namespace std;
double hmean(double a,double b);
int main()
{
double x,y,z;
cout<<"Enter two numbers:";
while(cin>>x>>y)
{
z=hmean(x,y);
cout<<"Harmonic mean of"<<x<<"and"<<y
<<"is"<<z<<end;
cout<<"Enter next set of number <q to quit>:";
}
cout<<"Bye!\n";
return 0;
}
double hmean(double a, double b)
{
if(a==-b)
{
cout<<"unenable arguments to hmean()\n";
abort();//调用将直接终止程序,不是返回到main()
}
return 2.0*a*b/(a+b);
}
为了避免异常终止,程序应在调用hmean()
函数之前检查x和y
的值。
二、返回错误码
- 一种比异常终止更灵活的方法:使用函数的返回值来指出问题。
- 如:
ostream类的get(void)
成员返回下一个输入字符的ASCII码,但到达文件尾时,将返回特殊值EOF。 - 对于
hmean()
不管用,任何数值都是有效的返回值,因此不存在可用于指出问题的特殊值。 - 这时,可以使用指针参数或引用参数来将值返回给调用程序,并使用函数的返回值来指出成功还是失败。
istream
族重载>>
运算符使用了这种技术的变体。使得程序采取除异常终止程序之外的其他措施。
例:将hmean()
的返回值重新定义为bool
,让返回值指出成功了还是失败。例:
#include <iostream>
#include<cfloat>
using namespace std;
bool hmean(double a,double b, double * ans);
int main()
{
double x,y,z;
cout<<"Enter two numbers:";
while(cin>>x>>y)
{
if(hmean(x,y,&z))
cout<<"Harmonic mean of"<<x<<"and"<<y
<<"is"<<z<<endl;
else
cout<<"one value should not be the negative"
<<"of theother-try again'\n";
cout<<"Enter next set of numbers <q to quit>:";
}
cout<<"Bye!\n";
return 0;
}
bool hmean(double a, double b,double * ans)
{
if(a==-b)
{
*ans=DBL_MAX;
return false;
}
else
{
return 2.0*a*b/(a+b);
return true;
}
}
三、异常机制
异常的组成部分:
- 引发异常;
- 使用处理程序捕获异常;
- 使用try块;
程序在出现问题时将引发异常。
例如:可以修改hmean()
,而不是调用abort()
函数,throw语句实际上是跳转,即命令程序跳到另一条语句。
throw关键字表示引发异常,紧随后的值(例如字符串或对象)指出了异常的特征。
- 程序使用异常处理程序来捕获异常,异常处理程序位于要处理问题的程序中,catch关键字表示捕获异常。
- 处理程序以关键字catch开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型;
- 花括号括起的代码块,指出要采取的措施。
- catch关键字和异常类型用作标签,指出当异常被引发时,程序应调到这个位置执行。异常处理程序也被称catch块。
- try标识其中特定的异常可能被激活的代码块,try的后面是一个由花括号括起来的代码块,表明需要注意这些代码引发的异常。
例:
#include <iostream>
using namespace std;
double hmean(double a,double b);
int main()
{
double x,y,z;
cout<<"Enter two numbers:";
while(cin>>x>>y)
{
//捕获开始
try{
z=hmean(x,y);
}//捕获结束
catch(const char * s)//异常处理开始
{
cout<<s<<endl;
cout<<"Enter a new pair of numbers:";
continue;
}//异常处理结束
cout<<"Harmonic mean of"<<x<<"and"<<y
<<"is"<<z<<endl;
cout<<"Enter next set of numbers <q to quit>:";
}
cout<<"Bye!\n";
return 0;
}
double hmean(double a, double b)
{
if(a==-b)
throw "bad hmean() arguments:a=-b not allowed";//抛出异常,throw将程序控制权返回给main()。
return 2.0*a*b/(a+b);
}
-
1-10传递给函数,if语句导致
hmean()
引发异常。这将终止hmean()
的执行。 -
hmean()
函数是从main()
中的try
块中调用的,因此查找与异常类型匹配的catch
块。 -
唯一的一个catch块的参数为
char*
,因此它与引发异常匹配。程序将字符串"bad hmean() arguments:a=-b not allowed"
赋给变量s
,然后执行处理程序的代码。
-
其中被引发的异常是字符串
bad hmean() arguments:a=-b not allowed
。异常类型可以是字符串或其他c++类型;通常为类类型。 -
如果函数引发异常,而没有try块或没有匹配的处理程序时,在默认·的·情况下。程序最终会调用
abort()
函数。但可以修改这种行为。
四、将对象用作异常类型
-
引发异常的函数将传递一个对象,优点之一是:使不同的异常类型来区分不同的函数在不同情况下引发的异常。
-
对象可以携带信息,方便确定引发异常的原因。
-
catch块可以根据这些信息来决定采取什么措施。
例:下面是针对函数hmean()
引发的异常而提供的一种设计:
class bad_hmean
{
private:
double v1;
double v2;
public:
bad_hmean(int a=0,int b=0):v1(a),v2(b){}
void mesg();
};
inline void bad_hmean::mesg()
{
cout<<"hmean("<<v1<<","<<v2<<"):"
<<"invalid arguments:a=-b/n";
}
可以将一个bad_hmean
对象初始化为传递给函数hmean的值,而方法mesg()
可用于报告问题(包括传递给函数hmean()的值)。函数hmean()可使用下面的代码:
if(a==-b)
throw bad_hmean(a,b);
上述代码调用构造函数bad_hmean()
,以初始化对象,使其存储参数值。
下面的两个程序添加了另一个异常类 bad_gmean
以及另一个名为gmean()
的函数,该函数引发bad_gmean
异常。
该函数gmean计算两个数的几何平均值,即乘积的平方根,要求两个参数都不为负,为负则引发异常。
头文件:
#ifndef EXC_MEAN_H
#define EXC_MEAN_H
#include<iostream>
using namespace std;
class bad_hmean
{
private:
double v1;
double v2;
public:
bad_hmean(double a=0,double b=0):v1(a),v2(b){}
void mesg();
};
inline void bad_hmean::mesg()
{
cout<<"hmean("<<v1<<", "<<v2<<"):"
<<"invalid arguments:a=-b\n";
}
class bad_gmean
{
public:
double v1;
double v2;
bad_gmean(double a=0,double b=0):v1(a),v2(b){}
const char*mesg();
};
inline const char * bad_gmean::mesg()
{
return "gmean() arguments should be >=0\n";
}
#endif // EXC_MEAN_H
源文件:
#include <iostream>
#include<cmath>
#include"exc_mean.h"
using namespace std;
double hmean(double a,double b);
double gmean(double a,double b);
int main()
{
double x,y,z;
cout<<"Enter two numbers:";
while(cin>>x>>y)
{
try{
z=hmean(x,y);
cout<<"Harmonic mean of"<<x<<"and"<<y
<<"is"<<z<<endl;
cout<<"Geometric mean of"<<x<<"and"<<y
<<"is"<<gmean(x,y)<<endl;
cout<<"Enter next set of numbers <q to quit>:";
}
catch(bad_hmean & bg)//bad_hmean异常捕获
{
bg.mesg();
cout<<"Try again.\n";
continue;
}
catch (bad_gmean &hg)//bad_gmean异常捕获
{
cout<<hg.mesg();
cout<<"Values used:"<<hg.v1<<","
<<hg.v2<<endl;
cout<<"Sorry,you don't get to play any more.\n";
break;
}
}
cout<<"Bye!\n";
return 0;
}
double hmean(double a, double b)
{
if(a==-b)
{
throw bad_hmean(a,b);
return 2.0*a*b/(a+b);
}
}
double gmean(double a, double b)
{
if(a<0 || b<0)
throw bad_gmean(a,b);
return sqrt(a*b);
}
bad_hmean异常处理程序使用continue,bad_gmean 异常处理程序使用break。给函数hmean() 提供的参数不正确,跳出,进入下一次。给函数gmean()提供参数不正确将结束循环。
五、异常规范和c++11
一种理念看似有前途,但实际的使用效果并不好。一个这样的例子是异常规范,这是c++98新增的一项功能,但c++11却抛弃了。意味着c++11仍然处于标准之中,但以后可能从标准中剔除,因此不建议使用它。
忽视异常规范前,应该知道它是什么样的:
double harm(double a)throw(bad_thing);//可能会抛出异常
double marm(double) throw();//不会抛出异常
异常规范的作用:
- 告诉用户可能需要使用try块。
- 让编译器添加执行运行阶段检查的代码,检查是否违反了异常规范。很难检查。
c++建议忽略异常规范。然而,c++11却支持一种特殊的异常规范:可使用新增的关键字noexcept指出函数不会引发异常:
double marm()noexcept;//marm()不会抛出异常
六、栈解退
异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被 自动析构。析构的顺序与构造的顺序相反,这一过程称为栈的解旋。
实例:
//头文件:
#ifndef EXC_MEAN_H
#define EXC_MEAN_H
#include<iostream>
using namespace std;
class bad_hmean
{
private:
double v1;
double v2;
public:
bad_hmean(double a=0,double b=0):v1(a),v2(b){}
void mesg();
};
inline void bad_hmean::mesg()
{
cout<<"hmean("<<v1<<", "<<v2<<"):"
<<"invalid arguments:a=-b\n";
}
class bad_gmean
{
public:
double v1;
double v2;
bad_gmean(double a=0,double b=0):v1(a),v2(b){}
const char*mesg();
};
inline const char * bad_gmean::mesg()
{
return "gmean() arguments should be >=0\n";
}
#endif // EXC_MEAN_H
//源文件:
#include<cmath>
#include<string>
#include"exc_mean.h"
using namespace std;
class demo
{
private:
string word;
public:
demo(const string & str)
{
word=str;
cout<<"deom"<<word<<"created\n";
}
~demo()
{
cout<<"deom"<<word<<"destroyed\n";
}
void show()const
{
cout<<"deom"<<word<<"lives!\n";
}
};
double hmean(double a,double b);
double gmean(double a,double b);
double means(double a,double b);
int main()
{
double x,y,z;
{
demo d1("found in block in main()");
cout<<"Enter two numbers:";
while(cin>>x>>y)
{
try{
z=means(x,y);
cout<<"The mean mean of"<<x<<"and"<<y
<<"is"<<z<<endl;
cout<<"Enter next pair:";
}
catch(bad_hmean &bg)
{
bg.mesg();
cout<<"Try again.\n";
continue;
}
catch(bad_gmean &hg)
{
cout<<hg.mesg();
cout<<"Values used:"<<hg.v1<<". "
<<hg.v2<<endl;
cout<<"Sorry,you don't get to paly any more.\n";
break;
}
d1.show();
}
cout<<"Bye!\n";
cin.get();
cin.get();
return 0;
}
}
double hmean(double a,double b)
{
if(a==-b)
throw bad_hmean(a,b);
return 2.0*a*b/(a+b);
sqrt(a*b);
}
double gmean(double a, double b)
{
if(a<0||b<0)
throw bad_gmean(a,b);
return sqrt(a*b);
}
double means(double a, double b)
{
double am,hm,gm;
demo d2("found in means()");
am=(a+b)/2.0;
try
{
hm=hmean(a,b);
gm=gmean(a,b);
}
catch(bad_hmean &bg)
{
bg.mesg();
cout<<"Caught in means()\n";
throw;
}
d2.show();
return (am+hm+gm)/3.0;
}
程序说明:
-
demo类的构造函数指出,在main()函数中创建一个deom的对象,调用函数means(),它创建了另一个demo对象。
-
函数means()使用
6和2
调用函数hmean()和gmean()
,将结果返回means(),计算一个结果将其返回。 -
返回结果前,means()调用了
d2.show()
;返回后,函数means()
指向完毕。
因此自动调用析构函数:
demo found in means() lives;
demo found in means() destroyed
输入循环将值6,-6
发给means(),然后means()创建一个新的deom对象,并将值传递给hmean()函数,hmean()引发bad_hmean异常,该异常被means()中的catch块捕获,下面输出指出:
hmean(6,-6):invalid arguments:a=-b
Caught in means()
该catch块中的throw语句导致函数means()终于执行,并将异常传递给main()函数。语句d2.show()没有被执行表明means()函数被提前终止。但需要指出的是,还是为d2调用析构函数:
deom found in means() destroyed
程序进行栈解退以后到能够捕获异常的地方时,将释放栈中的自动存储型变量。如果变量是类对象,将为该对象调用析构函数。
第三次输入循环:
-
6,-8
被发给函数means()
,means()创建一个新的demo对象,然后将6,-8
传递给hmean(),后者处理没问题。 -
means()
将6和-8传递给gmean()
,后者引发bad_gmean
异常。 -
由于
means()
不能捕获bad_gmean
异常,不能指行means()
的其代码。
程序进行栈解退时,释放局部的动态变量,因此为d2
调用了析构函数:
demo found in means() destroyed
最后,bad_gmean
异常处理程序捕获了该异常,循环结束终止,显示信息自动为d1
调用析构函数。
如果catch块使用的是exit而不是break,程序将立刻终止。
七、其他异常特性
虽然throw-catch
机制类似于函数参数和函数返回机制,但还有不同之处:
- 函数fun()的返回语句将控制权返回到调用fun()的函数,但throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合。
- 如上面程序,函数hmeans()引发异常时,控制权传递给函数means();当gmean()引发异常时,控制权传递到main()。
- 引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用。例:
class problem{...};
...
void super()throw(problem)
{
...
if(oh_no)
{
problem oops;
throw oops;
...
}
...
try{
super();
}
catch(problem & p)
{
}
//p将指向oops的副本而不是oops本身。
- 既然throw语句将生成副本,为何代码中使用引用呢?因为,引用还有个重要特征:基类引用可以执行派生类对象。
- 假设有继承关联的异常类型,在异常规范中需列出基类引用,它将与任何派生类对象匹配。
- 假设有异常类层次结构,并分别处理不同的异常类型,则使用基类引用将能够捕获任何异常对象,而使用派生类对象只能捕获它所属类和派生来的类的对象。
引发的异常对象被第一个与之匹配的catch块匹配,意味着catch块的排列顺序应该与派生顺序相反:
class bad_1{...};
class bad_2:public bad_1{...};
class bad_3:public bad_2{...};
...
void duper()
{
...
if(oh_no)
throw bad_!();
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语句放在最后面。
- 有时不知道会发生那些异常。例如:编写一个调用另一个函数的函数,却不知道调用的函数可能引发那些异常。但下面的方法仍能捕获异常,即使不知道异常的类型。
方法:使用省略号来表示异常类型,从而捕获任何异常:
catch(...){}
若知道一些可能会引发的异常,可以将上述捕获所有异常的catch块放在最后面,有点类似于switch语句中的default:
try{
duper();
}
catch(bad_3 &be)
{}
catch(bad_2 &be)
{}
catch(bad_1 &be)
{}
catch(bad_hmean &h)
{}
catch(...)
{}
可以创建捕获对象而不是引用的处理程序。在语句中使用基类对象时,将捕获所有的派生类对象,但派生特性将被剥去,因此将使用虚方法的基类版本。
八、exception类
-
较新的c++编译器将异常合并到语言中,例如:支持该语言,exception 头文件定义了exception类,c++可以把它用作其他异常类的基类。
-
代码可以引发exception异常,也可以将exception类用作基类。
若有个名为what()的虚拟成员函数,它返回一个字符串,该字符串的特征随实现而异。然而,由于这是个虚方法,因此可以在从exception派生而来的类中重新定义它:
#include<exception>
class bad_hmean:public exception
{
public:
const char * what(){return "bad arguments to hmean()";}
...
};
class bad_gmean:public exception
{
public:
const char * what(){return "bad arguments to gmean()";}
...
};
如果不想以不同的方式处理这些派生而来的异常,可以在同一个基类处理程序中捕获它们:
try
{
...
}
catch(exception & e)
{
cout<<e.what()<<endl;
...
}
否则,可以分别捕获它们。c++库定义了很多基于exception的异常类型。
九、stdexcept异常类
头文件stdexcept
定义了其他几个异常类。首先,该文件定义了logic_error
和runtime_error
类,它们都是以公有方式从exception派生而来:
class logic_error:public exception
{
public:
explicit logic_error(const string & what_arg);
...
};
class domain_error:public logic_error
{
public:
explicit domain_error(const string & what_arg);
...
};
注:这些类的构造函数接受一个string对象作为参数,该参数提供了方法what()以c-风格字符串方式返回的字符数据。
这两个新类被用作两派生类系列的基类。异常类系列logic_error描述了典型的逻辑错误。每个类的名称指出了它用于报告的错误类型:
domain_error
:让函数在参数不在定义域-1到1之间时引发异常invalid_argument
:指出给函数传递了一个意料外的值。引发异常length_error
:用于指出没有足够的空间来执行所需的操作。引发异常out_of_bounds
:用于指出索引错误。
每个类独有一个类似于logic_error
的构造函数,让你能够提供一个方法what()
返回的字符串。
runtime_error
异常系列描述了可能在运行期间发生但难以预计和防范的错误。每个类的名称指出了它用于报告的错误类型:
- range_error;
- overflow_error;
- underflow_error;
下溢错误在浮点数中,存在浮点类型可以表示的最小非零值,结果比这个值还小时将导致下溢错误。
整型和浮点型都可能发生上溢错误,当计算结果超过了某种类型能够表示的最大数量级时,将发生上溢错误。
结果不在函数允许的范围之内,但没有发生上溢或下溢错误,可以使用range_error
错误。
每个类独有一个类似与runtime_error
的构造函数,让您能够提供一个供方法what()返回的字符串。
总结:
logic_error
系列异常表明存在可以通过编程修复的问题,而runtime_error
系列异常表明存在无法避免的问题。- 但是这些错误类有相同的常规特征,主要区别:1.不同的类名让您能够分别处理每种异常。2.继承关系让您能够一起处理它们。
例:下面的代码首先单独捕获out_of_bounds
异常,然后统一捕获其他logic_error
系列异常,最后统一捕获exception
异常,runtime_error
系列异常以及其他从exception
派生而来的异常:
try{
...
}
catch(out_of_bounds &oe)
{...}
catch(logic_error &oe)
{...}
catch(excepion &oe)
{...}
注:如果上述库类不能满足您的需求,应该从logic_error或runtime_error派生一个异常类,以确保异常类可归入同一个继承层次结构中。
十、bad_alloc和new
- 对于使用new导致的内存分配问题,c++的最新处理方式是让new引发
bad_alloc
异常。 - 头文件new包含bad_alloc类的声明,它是从exception类公有派生而来的。但在以前,当无法分配请求的内存的内存量时,new返回一个空指针。
例:捕获异常显示继承的what()方法返回的消息,然后终止。
#include <iostream>
#include<new>
#include<cstdlib>
using namespace std;
struct Big
{
double stuff[20000];
};
int main()
{
Big * pb;
try{
cout<<"Trying to get big block of memory:\n";
pb=new Big[10000];
cout<<"Got past the new request:\n";
}
catch(bad_alloc &ba)
{
cout<<"Caught the exception!\n";
cout<<ba.what()<<endl;//返回字符串
exit(EXIT_FAILURE);
}
cout<<"Memory successfulluy allocated\n";
pb[0].stuff[0]=4;
cout<<pb[0].stuff[0]<<endl;
delete [] pb;
return 0;
}
如果程序在您的系统上运行时没有出现内存分配问题,可尝试提供请求的内存量。
十一、空指针和new
很多代码都是在new在失败时返回空指针时编写的。
为处理new的变化,有些编译器提供了一个标记,让用户选择所需的行为。
当前,c++标准提供了一种在失败时返回空指针的new,其用法如下:
int * pi=new (nothrow)int;
int *pa=new (nowthrow)int [500];
使用这种new,可将上面的核心代码改为:
Big * pb;
pb=new (nothrow)Big[10000];
if(pb==0)
{
cout<<"Could not allocate memory Bye.\n";
exit(EXIT_FAILURE);
}