如果在程序的代码中出现了异常情况——也就是说,通过当前语境无法获得足够的信息以决定应该采取什么样的措施——程序员可以创建一个包含错误信息的对象并把它抛出当前语境,通过这种方式将错误信息发送到更大范围的语境中去。这种方式被称为“抛出一个异常”。
try块
如果在一个函数内部抛出了异常(或者被这个函数所调用的其他函数抛出了异常),这个函数就会因为抛出异常而退出。如果不想因为一个throw而退出函数,可以在函数中试图解决实际产生程序设计问题的地方(和可能产生异常的地方)设置一个try块。这个块被称做try块的原因是程序需要在这里尝试调用各种函数。try块只是一个普通的程序块,由关键字try引导:
try{
//Code that may generate exceptions
}
异常处理器
异常处理器由多个catch函数块组成,catch函数块中的参数列表只能有一个参数,用于匹配由throw抛出的异常的类型,但是注意,最好是把catch函数块的参数写成引用传递,而不是值传递(因为这样可以避免将对象数据拆分和丢失的情况)。于是可以设置多个catch函数块,用于形成异常捕获网,捕获所以可能的异常类型。异常处理器紧接着try块,并且由关键字catch表示:
try{
//Code that may generate exceptions
}catch(type1 id1){
//Handle exceptions of type1
}catch(type2 id2){
//Handle exceptions of type2
}catch(type3 id3){
//Handle exceptions of type3
}
......
//Normal execution resumes heres...
异常处理器都必须紧跟在try块之后。一旦某个异常被抛出,异常处理机制将会按照我们书写的catch块的代码顺序依次寻找匹配的异常处理器,一旦找到一个相匹配的异常处理器,则系统就可以认为该异常已经得到处理了。
以下是举例说明try块和catch块的用法:
#include<iostream>
using namespace std;
class Rainbow
{
public:
Rainbow(){ cout<<"Rainbow()"<<endl;}
~Rainbow(){ cout<<"~Rainbow()"<<endl;}
};
void oz()
{
Rainbow rb;
for(int i =0;i<3;i++)
cout<<"There is no place like home./n";
throw 47;
}
int main(int argc, char ** argv)
{
try
{
cout<<"tornado, witch, munchkins ..."<<endl;
oz();
}
catch(int )
{
cout<<"Auntie Em! I had the strangest dream..."<<endl;
}
cout<<"over"<<endl;
return 0;
}
//当执行函数oz()中的throw语句时,程序的控制流程开始回溯,直到找到某个具有int型的catch子句为止。程序在这个catch子句的主体中恢复运行。
//应当注意的是,当一个函数抛出一个throw异常时,如果该函数里面有构造了对象的话,系统会先对该对象调用析构函数,当对象调用完了析构函数以后,才开始执行异常的抛出工作!!!!!!
下面再看一个例子:
#include<iostream>
using namespace std;
class Except1 {};
class Except2
{
public:
Except2(const Except1&) {} //以Except1来拷贝构造函数Except2。可以~
};
void f() { throw Except1();} //抛出Except1类型的异常,即使没有构造,但是可以使用系统自己生成 //的默认的构造函数。
int main()
{
try{
f();
}
catch(Except2&) //强调过了catch语句最好用的是引用传递而不是值传递。
{
cout<<"inside catch(Except2&)"<<endl;
}
catch(Except1&)
{
cout<<"inside catch(Except1&)"<<endl;
}
return 0;
}
//尽管读者可能会认为,通过使用转换构造函数将一个Except1对象转换成一个Except2对象,可以使得第一个异常处理器被匹配。但是,异常处理系统在处理异常的过程中不做这种这种转换,它只寻找和它最配的进行处理,所以异常将被第二个catch语句捕获。
对于有继承关系的类的异常的处理时,有这么一条原则:就是在异常的处理器catch捕获语句中,基类的类型总是可以包含本身甚至其派生类的类型,而派生类只能自己指明自己的类型,不能向上兼容,而基类则可以向下兼容。所以在对继承类的对象的捕获的过程中,为了避免基类垄断的局面,则将catch语句的排布从上到下为派生类到基类的顺序编写。这样可以保证派生类先找到属于自己的异常处理器,而最后再由基类找自己的类型。
举例:
#include<iostream>
using namespace std;
class X
{
public:
class Trouble {}; //注意:类中嵌套类的申明和定义,学习!!!
class small: public Trouble {};
class big:public Trouble {};//类中的继承!!!
void f() { throw big(); }
};
int main()
{
X x;
try{
x.f();
}
catch(X::Trouble &)
{
cout<<"caught Trouble"<<endl;
}
catch(X::small&)
{
cout<<"caught small"<<endl;
}
catch(X::big&)
{
cout<<"caught big"<<endl;
}
return 0;
}
//如果是这样的话,抛出的big()类异常则被Trouble类垄断,应该倒着写才可以实现顺序~
捕获所有异常:
有时候,程序员可能希望创建一个异常处理器,使其能够捕获所有的异常情况。用省略号代替异常处理器catch的参数列表就可以实现这一点:
catch(...)
{
cout<<"an exception was thrown"<<endl;
}
由于省略号异常处理器能够捕获任何类型的异常,所以最好将它放在异常处理器列表的最后,从而可以避免架空它后面的异常处理器。
省略号异常处理器不允许接受任何参数,所以无法得到任何有关异常的信息,也无法知道异常的类型。它是一个”全能捕获者”。这种catch语句经常用于清理资源并重新抛出所捕获的异常。
重新抛出异常的方法:
是在一个异常处理器内部,使用不带参数的throw语句可以重新抛出异常:
catch(...)
{
cout<<"an exception was thrown"<<endl;
//Deallocate your resource here, and then rethrow
throw; //该处的throw语句为重新抛出异常到更高一层的语境中去进行异常处理。
}
《不捕获异常》:
1。terminate()函数:
如果没有任何一个层次的异常处理器能够捕获某种异常,一个特殊的库函数 terminate()(在头文件
<exception>中定义)会被自动调用。默认情况下,terminate()函数会调用标准C库函数abort()使程序执行异常而退出。
terminate()函数为void无返回值类型,并且最后要有exit(0)函数表示程序中断而退出。
(!!!注意:决对不允许析构函数抛出异常,因为,如果当一个函数遇到抛出一个异常的时候,入彀改函数有执行构造函数的话,则应该在抛出异常之前先析构该构造的对象,则问题就产生了,于是该函数会返回2个异常,势必引起terminate()函数的调用!!!所以千万不能在析构函数中使用异常抛出!!!
2。set_terminate()函数
通过使用标准的set_terminate()函数,可以设置读者自己的terminate()函数,set_terminate()函数返回被替换的指向terminate()函数的指针(第一次调用set_terminate()函数时,返回函数库中默认的terminate()函数的指针),这样就可以在需要的时候恢复呀的terminate()函数。
自定义的terminate()函数不能有参数,而且其返回值的类型必须是void。另外它必须无条件以某个结束语句是程序终止逻辑。如果terminate()函数被调用,就以为着问题已经无法解决!!!~
以下举例说明terminate()和set_terminate()函数的使用:
#include<exception>
#include<iostream>
using namespace std;
void Myterminate()
{
cout<<"You have been killed"<<endl;
exit(0);
}
void (*old_terminate)()=set_terminate(Myterminate); //set_terminate()函数的使用,用函数指针来储存值。
class Botch
{
public:
class Fruit {};
void f()
{
cout<<"Botch::f()"<<endl;
throw Fruit();
}
~Botch()
{
//cout<<"sasa"<<endl; //当是这种情况时:发现类对象调用的成员函数如果抛出异常的话,则必要在处理该异常之前,连把该对象也要进行析构,即对象的成员函数的抛出异常也会连累到该对象本身,而调用析构函数结束自己,最后才开始处理异常!!!!!!
//throw 'c';//当是这种情况的时候,尽管异常处理器被申明为省略号的异常处理器catch(...),看起来应该能够捕获所有异常,但是结果是terminate()函数总是会被调用。
}
};
int main()
{
try
{
Botch b;
b.f();
}
catch(...)
{
cout<<"inside catch(...)"<<endl;
}
return 0;
}
《清理》
C++的异常处理必须确保当程序的执行流程离开一个作用域的时候,对于属于这个作用域的所有由构造函数建立起来的对象,他们的析构函数一定会被调用。
这里有一个例子,演示了当构造函数没有正常结束时不会调用相关联的析构函数。这个例子还显示了当在创建对象数组的过程中抛出异常时会发生什么情况:
#include<iostream>
using namespace std;
class Trace
{
static int counter;
int objid;
public:
Trace()
{
objid = counter ++;
cout<<"Constructing # "<<objid<<endl;
if(objid==3)
throw 3;
}
~Trace()
{
cout<<"Destructing # "<<objid<<endl;
}
};
int Trace::counter = 0;
int main()
{
try
{
Trace n1; //没有异常,完成构造
Trace array[5]; //第0,1号元素得到完全构造,但是之后的元素由于第2号元素构造失败,所以退出构造,第3,4号元素没有构造和析构。
Trace n2; //和上一句一样,由于抛出了异常,所以得不到构造,更加得不到析构。
}
catch(int i)
{
cout<<"caught "<<i<<endl;
}
return 0;
}
程序的输出结果为:
Constructing # 0
Constructing # 1
Constructing # 2
Constructing # 3
Destructing # 2
Destructing # 1
Destructing # 0
caught 3
程序的输出结果充分说明了如果构造函数没有得到正常的完成的话,其析构函数也将得不到调用,并且后面的一系列的对象也将不能继续构造函数。
《资源管理》
当我们在使用异常处理机制解决程序中的异常时,我们应当保证当资源由于抛出异常而没有被完全构造时能够调用其析构函数将资源回收,否则由于异常而不能调用析构函数的话,则浪费了构造函数中的一部分资源,造成资源泄露。
以下有一种方法可以解决上述问题,即把所有事物都抽象为对象,使得每次对对象的资源分配具有原子性
,即建立一个模板来使所以要分配资源的事物对象化。如:(其实就是建立了一个中间类当传媒)
#include<iostream>
#include<cstddef>
// size_t是什么东西呢?我在第一次看到这个动动的时候也是十分的困惑,毕竟以
前没有见过。size_t在<cstddef>中定义,是一种无符号整数类型(不一定是int),
用来保存对象的大小,这一用法是从C语言中借用过来的,现在你应该明白了吧
using namespace std;
template<class T,int sz =1>class PWard
{
T*ptr;
public:
class RangeError {};
PWard()
{
ptr = new T[sz];
cout<<"PWard constructor"<<endl;
}
~PWard()
{
delete []ptr;
cout<<"PWard destructor"<<endl;
}
T& operator[](int i) throw(RangeError) //重载operatpr[]
{
if(i>=0 && i<sz)
return ptr[i];
throw RangeError(); // 超出范围,异常。
}
};
class Cat
{
public:
Cat() { cout<<"Cat()"<<endl; }
~Cat() { cout<<"~Cat()"<<endl; }
void g() {}
};
class Dog
{
public:
void * operator new [](size_t)
{
cout<<"Allocating a Dog"<<endl;
throw 47;
}
void operator delete[](void *p)
{
cout<<"Deallocating a Dog"<<endl;
delete[](p);
}
};
class UseResource
{
PWard <Cat,3> cats;
PWard <Dog> dogs;
public:
UseResource()
{
cout<<"UseResource()"<<endl;
}
~UseResource()
{
cout<<"~UseResource()"<<endl;
}
void f()
{
cats[1].g();
}
};
int main()
{
try
{
UseResource ur;
}
catch(int )
{
cout<<"inside handler"<<endl;
}
catch(...)
{
cout<<"inside catch(...)"<<endl;
}
return 0;
}
//程序为Dog分配存储空间的时候再一次抛出异常,但是这一次Cat数组的对象被成功的清理了,没有出现内存泄露。
《auto_ptr类模板》
auto_ptr类模板是在头文件<memory>中定义的,它的构造函数接受一个指向类型属性的指针作为参数,它是一个用于封装指向分配的堆内存的指针。
例如:
#include<memory>
#include<iostream>
#include<cstddef>
using namespace std;
class TraceHeap
{
int i;
public:
static void * operator new(size_t siz)
{
void * p =::operator new (siz);
cout<<"yes,it's address is "<<p<<endl;
return p;
}
static void operator delete (void * p)
{
cout<<"No,it'll died in "<<p<<endl;
delete []p;
}
TraceHeap(int i):i(i) {};
int GetVal() const { return i;}
};
int main()
{
auto_ptr<TraceHeap> my(new TraceHeap(5));
cout<<my->GetVal()<<endl;
return 0;
}
//auto_ptr类模板可以很容易的用于指针数据成员。由于通过值引用的类对象总会被析构,所以当对象被析构的时候,这个对象的auto_ptr成员总是能释放它所封装的原指针。
//!!!!但是这个指针对由于抛出异常而没有完全构造的对象还是不能调用其析构函数.
//当涉及到有关异常抛出的继承类时,继承的可能抛出的错误类型或者大小只能减小或不变,但是不能增大。
<标准异常>
所有的标准异常类归根结底都是从exception类派生的,exception类的定义在头文件<exception>中。exception类的2个主派生类为logic_error和runtime_error,这2个类的定义在头文件<stdexcept>中。
logic_error用于描述程序中出现的逻辑错误,如传递无效的参数。runtime_error运行时错误是指那么无法预料的错误,例如硬件故障或者内存耗尽。logic_error和runtime_error都提供了一个参数类型为std::string的构造函数,这样就可以把消息保存到这2种类型的异常对象中,通过exception::what()函数,可以从对象中得到它所保存的信息。
举例如下:
#include<stdexcept>
#include<iostream>
using namespace std;
class MyError:public runtime_error
{
public:
MyError (const string & msg = "a"):runtime_error(msg) {}
};
int main()
{
try
{
throw MyError ("dawfsadf");
}
catch(MyError &x)
{
cout<<x.what()<<endl;
}
return 0;
}
〈异常规格说明〉
C++提供一种语法来告诉使用者函数所抛出的异常,这样他们就能很方便切=且正确的处理这些异常了。这就是可选的异常规格说明,它是函数申明的修饰符,写在参数列表的后面。
异常规格说明再次使用了关键字throw,函数可能抛出的所有可能异常的类型应该被写在throw之后的括号内。这里的函数申明如下所示:
void f() throw(toobig,toosmall,divzero);
在涉及异常的情况下,传统的函数申明:
void f();
意味着函数可能抛出任何类型的异常。
而下面这个函数:
void f() throw();
则表示不会抛出任何异常。
unexpected()函数(在<exception>头文件中定义)
如果函数所抛出的异常没有列在异常规格说明的异常集中,则系统将会自动调用函数unexpected(),默认的unexpected()函数会调用前面说讲过的terminate()函数。
set_expected()函数就像前面说的set_terminate()函数一样用。
下面这个例子可以分清这2个函数的使用:
#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() {}; //当是这个函数时,将不会报错,因为switch语句返回的2个异常都可以被捕获。
//void g() { throw 47; }//当是这个函数时,将会报错,因为,多返回了一个异常类型,而该异常类型
//却不在f()函数的异常列表范围内,所以引起调用umexpected()函数,报 //错。
void my_unexpected()
{
cout<<"unexpected exception thrown"<<endl;
exit(0);
}
int main()
{
void (*old_unexpected)()=set_unexpected(my_unexpected);
for(int i=1;i<=3;i++)
try{
f(i);
}catch(up &)
{
cout<<"Up caught"<<endl;
}catch(fit&)
{
cout<<"fit caught"<<endl;
}
return 0;
}
如果unexpected处理器所抛出的异常还是不符合函数的异常规格说明,下列2种情况之一讲会发生:
1。如果函数的异常规格说明中包括std::bad_exception(在<exception>中定义),unexpected处理器所抛出的异常会被替换成std::bad_exception对象,然后,程序恢复到这个函数被调用的位置重新开始异常匹配。
2。如果函数的异常规格说明中不包括std::bad_exception,程序则直接调用terminate()函数退出程序。