异常
异常是对程序运行过程中发生的异常情况的一种响应。
异常的处理流程
1 引发异常
2 使用处理程序捕获异常
3 使用try块
程序在出现问题时将引发异常,一般在可能出现异常的情况下定义一个throw语句,throw语句后边接的类型一般为字符串或对象,当引发异常时,throw语句将导致程序沿函数调用序列后退,直到找到包含try块的函数。
try块包含可能会引发异常的代码块,他后面跟一个或多个catch块。
catch块用来捕获异常,catch块的标签与throw块的标签相同时将触发catch块。
如果函数引发了异常,没有try块或没有匹配的处理程序时,程序最终会调用about函数
#include <iostream>
doubel hmean(double a,doule b);
int main()
{
double x,y,z;
std::cout<<"enter two numbers";
while(std::cin>>x>>y)
{
try{
z=hmean(x,y);
}
catch(const char*s)
{
std::cout<<s<<std::endl;
std::cout<<"enter a new pair of numbers";
continue;
}
std::cout<<"harmonic mean of"<<x<<"and"<<y<<"is"<<z<<std::endl;
std::cout<<"enter next set of numbers<q to quit>: ";
}
std::cout<<"bye";
return 0;
}
double hmean(double a,double b)
{
if(a==-b)
throw"bad arguments";
return 2.0*a*b/(a+b);
}
将对象用作异常类型
有两个异常类
class bad_g
{
private:
....
public:
void mesg();
....
}
class bad_h
{
private:
.....
public:
void mesg();
.....
}
try
{
//引发异常的块
}
catch(bad_g & g)
{
g.mesg();
}
catch(bad_h & h)
{
h.mesg();
}
......
void funname(type a,type b)
{
if(...)
throw bad_g(a,b);
...
}
void funname2(type a,type b)
{
if(...)
throw bad_h(a,b);
...
}
栈解退
假设try块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序流程将从引发异常的函数跳到包含try块和处理程序的函数,这涉及到栈解退。
假设函数由于出现异常而终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句,这称为栈解退。引发机制的一个重要的特性是,和函数返回一样,对于栈中的自动类对象,类的析构函数将被调用。然而,函数返回仅仅处理该函数放在栈中的对象,而throw语句则处理try块和throw之间整个函数调用序列放在栈中的对象。如果没有栈解退这种特性,则引发异常后,对于中间函数调用放在栈中的自动类对象,其析构函数将不会被调用。
异常的其他特性
引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用。
假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将与任何派生类对象匹配,使用基类引用将能够捕获任何异常对象,而使用派生类对象只能捕获它所从属类及从这个类派生而来的类的对象。
如果有一个异常类继承层次结构,应这样排列catch块,将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面。
exception类
异常使得在程序设计中包含错误处理功能更容易,以免事后采取一些严格的错误处理方式。
exception头文件定义了exception类,c++可以把它用作其他异常类的基类,代码可以引发exception异常,也可以将exception类用作基类。
exception类中有一个what()的虚拟成员函数,它返回一个字符串,该字符串的特征随实现而异。what()函数是一个虚方法,可以在exception派生而来的类中重新定义它。
c++库定义了很多基于exception类的异常类
stdexcept异常类
头文件stdexcept定义了logic_error类和runtime_error类,它们都是以公有方式从exception派生而来的类。
logic_error
class logic_error:public exception{
public:
explicit logic_error(const string& what_arg);
...
}
logic_error又派生出了不同的类
domain_error//数学函数通常提供的有值域或定义域,不在值域或定义域之间将引发异常
invalid_argument//当给函数传递一个意外的值时将引发异常,例如函数接受字符串‘1’或‘0’当传递字符串中包含其他字符时,将引发invalid_argument异常
length_error//没有足够的空间来执行操作
out_of_bounds//将用于指示索引错误
每一种类都有一个类似于logic_error的构造函数,可以使用what()返回的字符串。
runtime_error
runtime_error 用于描述可能在运行期间发生但难以预计和防范的错误。
range_error //计算结果不在函数允许的范围内
overflow_error//某种类型超过了最大数量级,发生上溢错误
underflow_error//发生下溢错误,一般浮点类型可以表示的最小非零值,比这个值还小将导致下溢。
try
{
}
catch(out_of_bounds &oe)
{
cout<<oe.what()<<endl;
eixt(EXIT_FAILURE);
}//不需要throw
bad_alloc异常和new
对于使用new导致的内存分配问题,c++一般让new引发bad_alloc异常,头文件new中包含了bad_alloc类的声明,它是从exception类公有派生而来的。
#include<new>
#include<cstdlib>//exit()
try
{
//内存分配出问题
}
catch(bad_alloc &ba)
{
cout<<ba.what()<<endl;
exit(EXIT_FALLURE);
}
很多代码都是在new在失败的时候返回空指针编写的。有些编译器提供了一个标记,让用户选择所需的行为。
int *pi=new (std::nothrow)int;
int *pa=new (std::nowthrow)int[500];
Big* pb;
pb=new (std::nothrow)Big[10000];
if(pb==0)
{
cout<<"coule not allocate memory";
exit(EXIT_FALLURE);
}
异常、类和继承
异常、类和继承以三种方式相互关联
1 从一个异常类派生出另一个
2 可以在类定义中嵌套异常类声明来组合异常
3 这种嵌套声明本身可被继承,还可用作基类
class A
{
prublic:
class bad_a:public std::logic_error
{
prvate:
...
public:
....
virtual ~bad_a()throw(){}
};
....
};
class B:public A
{
public:
class bad_b:public A:bad_a
{
private:
....
public:
....
virtual ~bad_b()throw(){}
};
...
};
bad_a被嵌套在A的公有声明中,这使得客户类的catch块可以使用这个类作为类型。
在外部使用这个类型时,要使用A::bad_a来标识。
bad_a和bad_b类都使用了异常规范throw(),这是因为它们都是从基类exception派生而来的,而exception的虚构造函数使用了异常规范throw()。在c++11中,exception的构造函数没有使用异常规范。
异常何时会迷失方向
异常被引发后,在两种情况下,会导致问题。首先,如果它是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配,如果异常不是在函数中引发的,则必须捕获它。如果没被捕获,则异常被称为未捕获异常。未捕获异常不会导致程序立刻异常终止,程序首先将调用terminate()。默认情况下,terminate()调用about()函数。可以指定terminate()应调用的函数来修改terminate()的这种行为。
可调用set_terminate()函数。set_terminate()和terminate()都是在头文件exception中声明的。
//terminate()函数和set_terminate()函数的声明
typedef void(*terminate_handler)();
terminate_handler set_terminate(terminate_handle f)throw();//c++98
terminate_handler set_terminate(terminate_handle f)noexcept;//c++11
void terminate();//c++98
void terminate()noexcept;//c++11
RTTI
RTTI是运行阶段类型识别
RTTI为了为程序在运行阶段确定对象的类型提供的一种标准方法。
c++提供了3个支持RTTI的元素
1 dynamic_cast运算符
2 typeid运算符
3 type_info 结构
RTTI的用途
因为一个基类可以指向它派生出来的类的任何一个类对象,假设我们有一个函数,函数中创建了一个类对象,函数返回这个类对象的地址,并将这个地址赋给基类指针。那么我们如何知道这个指针指向的是哪个对象呢?如果我们只是用这个指针去调用虚函数,并不需要知道具体的对象,但是如果想要调用这个对象具体的方法,那就需要知道这个对象的类型。
RTTI只能用于包含虚函数的类。
dynamic_cast
dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针。
否则该运算符返回0。
dynamic_cast 运算符不能回答指针指向的是哪类对象,但能回答是否可以安全地将对象的地址赋给特定类型的指针。
//dynamic_cast的用处
#include<iostream>
#include<cstdlib>
#include<ctime>
using namespace std;
class Grand
{
private:
int hold;
public:
Grand(int h=0):hold(h){}
virtual void speak()const{cout<<"grand"<<endl;}
virtual int value()const{return hold;}
};
class Superb:public Grand
{
public:
Superb(int h=0):Grand(h){}
void speak()const {cout<<"superb"<<endl;}
virtual void say()const{cout<<"superb value"<<value()<<"\n";}
};
class Magnificent :public Superb
{
private:
char ch;
public:
Magnificent(int h=0,char c='A'):Superb(h),ch(c){}
void speak() const{cout<<"magnificent\n";}
void say() const{cout<<"hold character"<<ch<<"and the integer"<<value()<<"\n";}
};
Grand * GetOne();
int main()
{
srand(time(0));
Grand* pg;
Superb *ps;
for(int i=0;i<5;i++)
{
pg=GetOne();
pg->speak();
if(ps=dynamic_cast<Superb *>(pg))
ps->say();
}
return 0;
}
Grand* GetOne()
{
Grand* p;
switch(rand()%3)
{
case 0:p=new Grand(rand()%100);
break;
case 1:p=new Superb(rand()%100);
break;
case 2:p=new Magnificent(rand()%100,'A'+rand()%26);
break;
}
return p;
}
这个程序定义了三个类 Grand,Superb,Magnificent
三个类都定义了方法speak,但只有superb和magnificent类定义了方法say
使用一个函数将派生类对象地址赋给基类指针,则这个指针可以调用speak函数,但只有superb对象和magnificent对象能调用say()函数,所以使用dynamic_cast来测试pg指针是否能赋给ps指针。
dynamic_cast的语法
Superb *pm=dynamic_cast