本文分四种情况讨论了C++语言异常处理机制中,使用异常类对象传递异常信息时,异常类对象的构造以及析构的情况,以及使用异常重拋机制时异常类对象的构造与析构。
1 C++中的构造函数与析构函数
C++的构造函数在新建一个对象时被系统自动调用,用于为对象分配内存空间以及进行初始化工作。只有一个参数,并且是本类对象的引用的构造函数称为拷贝构造函数,是一种特殊的构造函数,这种构造函数在使用一个已经存在的对象初始化一个新对象时被调用。析构函数是在对象的生命周期结束时,完成清理内存,以及用户指定的工作,例如释放堆空间等。
2 C++中的异常类
异常处理机制是C++语言提供的用于处理运行时错误的机制,可以提高程序的健壮性,并且可以反馈一些信息帮助用户或者设计者解决问题。有时只利用C++的基本类型数据不能提供足够的信息,这时候就可以使用系统库中定义好的或者用户自定义的异常类来传递异常信息。
3 C++中异常类对象的构造与析构
首先定义一个异常类TestException:
class TestException{
public:
int a;
TestException(inta):a(a) {cout<<"对象"<<(int)this<<"被构造"<<endl;}
TestException(TestException&f){
a=f.a;
cout<<"对象"<<(int)this<<"被拷贝构造"<<endl;
}
~TestException(){
cout<<"a="<<a<<endl;
cout<<"对象"<<(int)this<<"被析构"<<endl;
}
voidshow() {cout<<"对象"<<(int)this<<"的a="<<a<<endl;}
};
分别定义构造函数、拷贝构造函数、析构函数。因为每个对象在内存中的地址都是唯一的,可以用地址来唯一标示每个对象,因此在构造函数、拷贝构造函数、析构函数中都将指针类型强制转换为整形输出,来清晰地展现每个对象的构造与析构过程。然后定义一个抛出异常的函数:
void throwException() {throw TestException(3);}
该函数被main函数调用抛出异常,由于异常逆着函数调用链传递直到被处理,每两个函数之间的异常传递是相同的,因此只用一次函数调用就可以模拟这个过程。因为如果catch语句使用类指针类型捕获异常,那么被抛出的不是异常类对象,而是这个对象的指针,因此对异常对象的构造与析构可以分为四种情况讨论。
3.1 catch语句中使用引用捕获异常对象,不使用异常重抛
main函数:
void main(){
try{
throwException();
}
catch(TestException&d/*引用类型*/){
d.show();
d.a++;
}
cout<<"endmain"<<endl;
}
运行输出结果为:
对象6093508被构造
对象6093508的a=3
a=4
对象6093508被析构
end main
可以看出在使用引用类型时,throw出的异常对象不经过拷贝构造,离开函数throwException的作用域之后没有被析构,而是在catch块执行完后被析构。这与普通对象的析构方式是不同的,普通的对象在定义它的函数结束之后就会被立即析构,此时的引用这个对象将会出错,而异常对象不是这样。
3.2 catch语句中使用引用捕获异常对象,使用异常重抛
修改throwException函数使之能够捕捉到异常并重新抛出:
void throwException(){
try{
throwTestException(3);
}
catch(TestException&t/*引用类型*/){
cout<<"throwException函数:"<<endl;
t.show();
throw;
}
}
main函数:
void main(){
try{
throwException();
}
catch(TestException&d/*引用类型*/){
cout<<"main函数:"<<endl;
d.show();
d.a++;
}
cout<<"endmain"<<endl;
}
输出结果为:
对象4782516被构造
throwException函数:
对象4782516的a=4
main函数:
对象4782516的a=4
a=5
对象4782516被析构
end main可以看出与第一种情况结果基本相同,被重新抛出的异常仍为最开始新建的异常,也是在catch块执行完之后被析构。
3.3 catch语句中使用类类型捕获异常对象,不使用异常重抛
throwException函数与第一种情况相同,main函数的改为:
void main(){
try{
throwException();
}
catch(TestExceptiond/*类类型*/){
d.show();
d.a++;
}
cout<<"endmain"<<endl;
}
运行结果为:
对象17103400被构造
对象17103820被拷贝构造
对象17103820的a=3
a=4
对象17103820被析构
a=3
对象17103400被析构
end main
可以看出对象17103400是最早被抛出的异常对象,在被main中的catch块捕获时经过拷贝构造生成对象17103820,然后对对象17103820的数据成员a进行自增操作后,catch块执行结束,两个对象被析构。因此在使用类类型捕获异常时,原始的异常对象仍然是直到catch块执行完成之后才被析构,且catch块中操作的对象是经过拷贝构造得到的新对象,不是原始异常对象。
3.4 catch语句中使用类类型捕获异常对象,使用异常重抛
throwException函数:
void throwException(){
try{
throwTestException(3);
}
catch(TestExceptiont/*类类型*/){
cout<<"throwException函数:"<<endl;
t.a++;
t.show();
throw;
}
}
main函数:
void main(){
try{
throwException();
}
catch(TestExceptiond/*类类型*/){
cout<<"main函数:"<<endl;
d.show();
d.a++;
}
cout<<"endmain"<<endl;
}
运行结果:
对象2030536被构造
对象2030740被拷贝构造
throwException函数:
对象2030740的a=4
a=4
对象2030740被析构
对象2030984被拷贝构造
main函数:
对象2030984的a=3
a=4
对象2030984被析构
a=3
对象2030536被析构
end main
可以看出最早被构造的对象2030536在被throwException函数的catch语句捕获时经过拷贝构造生成对象2030740,然后这个对象2030740的数值成员a在catch块中进行自增操作,然后进行重拋操作,但是从结果来看,最早生成的对象2030536最晚被析构,也就是说被throwException函数的catch语句重拋的对象依然是最早构造的对象2030536,而不是拷贝构造生成的对象2030740,并且由于拷贝构造生成新对象的原因,throwException函数的catch语句中的自增操作对原始对象没有任何影响。
从上面的四种情况的分析可以看出,最早被抛出的异常类对象只有在catch语句块执行完并且没有异常重抛时才会被析构;如果存在异常重拋,那么被重拋的对象一定是最早构造的对象。在catch块中使用类类型来捕捉异常对象时,会调用拷贝构造函数生成一个新对象,并且在catch块中操作的是新对象,而原始对象不受影响;在catch块中使用引用类型来捕捉异常对象时,不会调用拷贝构造函数生成新对象,并且在catch块中操作的是原始对象。
从得出的结论可以看出,使用异常类时候最好使用引用类型来捕捉异常对象,可以减少拷贝构造函数的调用、提高程序运行速度以及节省内存空间。另外,如果使用异常重抛机制,并且上一级catch块需要知道下一级catch块对异常对象的修改时,则需要使用引用类型来捕捉异常对象,否则做出的修改对原始异常对象没有作用。