如今说到异常处理机制和try…catch这样的组合,大多数程序员第一反应往往是Java,的确Java的异常处理机制相当完善。今天翻了一下C++异常处理机制,感觉相比Java的机制而言,有更多的细节需要自己来处理和维护,确实没有Java的机制来的完善,简单地总结了一下目前的收获,希望能够起到抛砖引玉的作用。
一、抛出异常和try…catch块
C++的try…catch块与java的很类似。与Java不同的是,C++允许用户在throw中抛出任何数据类型,包括整数、字符串、以及自定义的结构体和对象。例如下面的这个例子就抛出了一个整数:
#include <cstdlib>
#include <iostream>
using std::cout;
using std::endl;
double divide(double dividend, double divisor)
{
if (divisor == 0) throw 1.0;
return dividend / divisor;
}
int main()
{
try
{
cout << divide(5.0, 0) << endl;
}
catch (double ex)
{
cout << "发生异常:浮点数" << ex << endl;
}
catch (...)
{
cout << "发生异常" << endl;
}
system("pause");
return 0;
}
需要指出的是,对于原子类型的数据,这里的catch过程中,不会进行类型转换,而需要严格地进行一一对应。例如如果之前的例子中抛出的异常是个整数(throw 1),那么这个异常将会被catch(…)捕获而不是catch(double ex)捕获。
当然大多数情况下,抛出一个整数或者一个字符串并不能有效地反应出异常的类型和信息。更多的时候我们会和Java一样抛出一些类对象用以表示异常信息,这个时候就需要异常类。
一个典型的异常类可以是这样的
class ArithmeticException
{
public:
ArithmeticException(string message) : message_(message) {}
~ArithmeticException() {}
string getMessage()
{
return message_;
}
private:
string message_;
};
在使用的时候可以和抛出其他异常的方式是一样的
double divide(double dividend, double divisor)
{
if (divisor == 0) throw ArithmeticException("除数不能为0");
return dividend / divisor;
}
我们也可以通过catch(ArithmeticException ex)这样的方法来捕获ArithmeticException的子类。
二、捕获实体对象和捕获引用或对象指针的区别
但是在选择捕获方式的时候,我们实际上有三种选择:
1. 捕获实体,如catch (ArithmeticException ex)
2. 捕获实体的引用,如catch (ArithmeticException &ex)
3. 捕获实体的指针,如catch (ArithmeticException *ex)
三种捕获方式中的Exception对象的生成和销毁过程各有不同,下面我们通过一个例子来对其进行分析。
我们给ArithmeticException的构造函数,析构函数和拷贝构造函数中添加一些打印语句,方便观察其调用过程
class ArithmeticException
{
public:
ArithmeticException(string message) : message_(message)
{
cout << "异常\"" << this->message_ << "\"的构造函数被调用" << endl;
}
ArithmeticException(const ArithmeticException &exception) : message_(exception.message_)
{
cout << "异常\"" << this->message_ << "\"的拷贝构造函数被调用" << endl;
}
~ArithmeticException()
{
cout << "异常\"" << this->message_ << "\"的析构函数被调用" << endl;
}
string getMessage()
{
return message_;
}
private:
string message_;
};
在g++编译后的运行结果是如下:
根据调试的情况来看,这几次函数的调用时机分别是在这里
显示信息 | 说明 |
---|---|
异常”除数不能为0”的构造函数被调用 | throw被调用时声称新的ArithmeticException对象 |
异常”除数不能为0”的拷贝构造函数被调用 | 在catch时调用拷贝构造函数,生成一个新的ArithmeticException对象 |
发生异常:除数不能为0 | 异常处理语句 |
异常”除数不能为0”的析构函数被调用 | 在catch块的执行完后优先析构最初throw出来的对象 |
异常”除数不能为0”的析构函数被调用 | 在整个catch快完成后析构拷贝得到的ArithmeticException对象 |
如果将捕获异常的catch语句修改成引用
catch (ArithmeticException &ex)
编译后运行的结果是这样的:
可以看到,中间少了catch时的拷贝构造函数和析构函数的调用,这说明,catch语句中的Exception参数很类似于函数调用中的值传递和引用传递,如果我们catch一个实体对象的话,就相当于值传递,系统将会调用拷贝构造函数生成一个新的对象。
另外,如果我们在throw前就已经生成了Exception对象,然后在throw时在抛出这个对象的话,拷贝构造函数还会多调用一次。
double divide(double dividend, double divisor)
{
if (divisor == 0)
{
ArithmeticException ex("除数不能为0");
throw ex;
}
return dividend / divisor;
}
结果如下:
其中多出来的一次拷贝构造和一次析构函数调用是在throw的过程中调用的,程序首先调用拷贝构造函数生成一个新对象用以装在对象ex,然后将ex析构,将拷贝所得的对象交给catch捕获。也就是说,使用throw的时候和catch时类似,也相当于调用了一个函数,除非使用对象引用(如ArithmeticException &ex),否则系统以值方式进行传递对象,于是系统就会额外生成一个新的对象。
很明显,对于异常处理而言,这些对象基本上只会用到一次,这样反复地复制新对象是没有意义的,无形之间造成了大量额外开销。所以编译器会对throw时直接new生成的Exception对象进行优化,自动将其作为对象引用传输。
使用引用来捕获异常还有一个特点,就是如果我们调用的是该异常类子类的一个对象是,使用引用就可以将其动态绑定到子类的对象和方法中。具体来说,加入我们有一个DivideException是ArithmeticException的子类,其定义如下:
class ArithmeticException
{
public:
ArithmeticException(string message) : message_(message) {}
ArithmeticException(const ArithmeticException &exception) : message_(exception.message_) {}
~ArithmeticException() {}
virtual string getMessage()
{
return message_;
}
protected:
string message_;
};
class DivideException : public ArithmeticException
{
public:
DivideException(string message) : ArithmeticException(message) {}
DivideException(const DivideException &exception) : ArithmeticException(exception) {}
~DivideException() {}
virtual string getMessage()
{
return message_ + " in DivideException";
}
};
抛出异常时我们直接抛出DivideException,并捕获ArithmeticException
double divide(double dividend, double divisor)
{
if (divisor == 0) throw DivideException("除数不能为0");
return dividend / divisor;
}
int main()
{
try
{
cout << divide(5.0, 0) << endl;
}
catch (ArithmeticException ex)
{
cout << "发生异常:" << ex.getMessage() << endl;
}
catch (...)
{
cout << "发生异常" << endl;
}
system("pause");
return 0;
}
如果我们捕获对象实体,并调用其getMessage方法后我们将会得到这样的结果:
很明显我们调用的实际上是基类ArithmeticException的getMessage方法。
但如果使用引用的话,我们将会调用子类DivideException的getMessage方法。
结合之前对于捕获异常时对于异常对象的创建、拷贝和销毁的过程进行分析我们不难发现,造成这种情况的原因正是由于在抛出异常和捕获异常是类似于函数调用的传递方式,捕获实体就相当于传值,系统会调用形参上的类型,使用拷贝构造函数来获取一个新对象,如果原对象是其子类,虽然能够成功执行拷贝构造函数,但子类的特性也会随之消失。而捕获引用就相当于传引用,捕获后获取的对象引用可以进行动态绑定。
捕获异常对象指针的行为和捕获引用的行为很类似,但需要注意的是,捕获的引用对象会在catch块完成后自动释放,但捕获对象指针并不会自动释放,需要手动释放。例如:
double divide(double dividend, double divisor)
{
if (divisor == 0) throw new ArithmeticException("除数不能为0");
return dividend / divisor;
}
int main()
{
try
{
cout << divide(5.0, 0) << endl;
}
catch (ArithmeticException *ex)
{
cout << "发生异常:" << ex->getMessage() << endl;
}
catch (...)
{
cout << "发生异常" << endl;
}
system("pause");
return 0;
}
其执行结果如下:
我们必须在catch块内手工释放这个异常对象的指针
catch (ArithmeticException *ex)
{
cout << "发生异常:" << ex->getMessage() << endl;
}
注意这个操作必须在catch块中完成,因为ex这个指针在离开catch块后就会被释放,如果不及时释放这个异常对象,就会导致内存泄露发生。