目录
条款9:利用destrucors(析构函数)避免资源泄漏
- 当异常抛出时,如果没有异常处理机制,调用抛出异常代码的位置之后的代码将不会被执行。一旦这些不被执行的代码的作用是做堆内存上的清理工作,那么就极有可能发生资源泄漏。在C++中可以采用try…catch…语句捕获和处理异常:
void processAdoptions(istream& dataSource)
{
while (dataSource) {
ALA *pa = readALA(dataSource);
try {
pa->processAdoption();
}
catch (...) { // 捕获所有异常
delete pa; // 避免内存泄漏
// 当异常抛出时
throw; // 传送异常给调用者
}
delete pa; // 避免资源泄漏
} // 当没有异常抛出时
}
- 为了避免资源泄漏问题,可以采用智能指针管理资源对象,这样就无需用手动释放内存。一旦抛出异常,智能指针作为局部变量会被销毁,它所管理的资源对象也随之销毁。如果希望智能指针应用在数组上,更好的选择是用vector代替数组。
void processAdoptions(istream& dataSource)
{
while (dataSource) {
auto_ptr<ALA> pa(readALA(dataSource));
pa->processAdoption();
}
}
- 总结出的规则:将 “ 资源 ” 封装在 “ 资源管理类对象 ” 内,可以利用资源管理类对象的析构函数对资源进行释放,通常可以避免异常出现造成的资源泄漏,但是避免不了在获取资源过程中抛出的异常。
条款10:在constructors内阻止资源泄漏
- C++保证删除null指针是安全的。
- 在构造函数中,可能在对象还未完全构造的时候抛出异常,此时该对象并未构造成功,析构函数也不会被调用。如果在此之前,在堆上构造完的其他成员变量可能就会被泄漏。如果在构造函数之外使用try…catch…语句及时捕获异常,但是仍然可能会造成资源泄漏,如下例:
void testBookEntryClass()
{
BookEntry *pb = 0;
try {
pb = new BookEntry("Addison-Wesley Publishing Company",
"One Jacob Way, Reading, MA 01867");//构造对象
...
}
catch (...) { // 捕获所有异常
delete pb; // 删除pb,当抛出异常时,new操作并未成功,赋值失败,pb是null
//因此delete pb并未解决资源泄漏
throw; // 传递异常给调用者
}
delete pb; // 正常删除pb
}
- 因为当对象在构造中抛出异常后C++不负责清除对象,所以你必须重新设计你的构造函数以让它们自己清除。
- 方法1:在构造函数内使用try…catch…及时捕获并处理异常
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
: theName(name), theAddress(address),
theImage(0), theAudioClip(0)//堆上资源
{
try { // 这try block是新加入的
if (imageFileName != "") {
theImage = new Image(imageFileName);
}
if (audioClipFileName != "") {
theAudioClip = new AudioClip(audioClipFileName);//当它发生异常时,theImage也能被清理
}
}
catch (...) { // 捕获所有异常
delete theImage; // 完成必要的清除代码
delete theAudioClip;
throw; // 继续传递异常
}
}
如果theImage和theAudioClip是常量指针,只能在构造函数中的初始化列表里面初始化(只能为表达式),无法在初始化列表里面使用try…catch…语句,导致无法捕获异常。可以用一个辅助函数初始化这些成员变量,在该函数中使用try…catch…语句捕获异常,将函数调用写在初始化列表中。缺点在于构造函数完成的动作分散在其他函数之中,后期维护困难。
class BookEntry {
...
Image * initImage(const string& imageFileName);
AudioClip * initAudioClip(const string&
audioClipFileName);
};
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
: theName(name), theAddress(address),
theImage(initImage(imageFileName)),
theAudioClip(initAudioClip(audioClipFileName))
{}
// theImage 被首先初始化,所以即使这个初始化失败也
// 不用担心资源泄漏,这个函数不用进行异常处理。
Image * BookEntry::initImage(const string& imageFileName)
{
if (imageFileName != "") return new Image(imageFileName);
else return 0;
}
// theAudioClip被第二个初始化, 所以如果在theAudioClip
// 初始化过程中抛出异常,它必须确保theImage的资源被释放。
// 因此这个函数使用try...catch 。
AudioClip * BookEntry::initAudioClip(const string&
audioClipFileName)
{
try {
if (audioClipFileName != "") {
return new AudioClip(audioClipFileName);
}
else return 0;
}
catch (...) {
delete theImage;
throw;
}
}
- 方法2:智能指针类对象管理在堆上创建的资源,析构函数甚至啥也不用干了。
class BookEntry {
public:
... // 同上
private:
...
const auto_ptr<Image> theImage; // 它们现在是
const auto_ptr<AudioClip> theAudioClip; // auto_ptr对象
};
//这样做使得BookEntry的构造函数即使在存在异常的情况下也能做到不泄漏资源
//而且让我们能够使用成员初始化列表来初始化theImage 和 theAudioClip,如下所示:
BookEntry::BookEntry(const string& name,
const string& address,
const string& imageFileName,
const string& audioClipFileName)
: theName(name), theAddress(address),
theImage(imageFileName != ""
? new Image(imageFileName)
: 0),
theAudioClip(audioClipFileName != ""
? new AudioClip(audioClipFileName)
: 0)
{}
条款11:禁止异常流出destructors之外
- 如果在一个异常被激活的同时,析构函数也抛出异常,并导致程序控制权转移到析构函数外,C++将调用terminate函数:它终止你程序的运行,而且是立即终止,甚至连局部对象都没有被释放。
- 禁止异常传递到析构函数外有两个原因,第一能够在异常转递的堆栈辗转开解(stack-unwinding)的过程中,防止terminate被调用。第二它能帮助确保析构函数总能完成它应该完成的事情。
- 在析构函数内使用try…catch…及时捕获并处理异常,如果处理异常的过程也有可能发生异常,那么最好直接忽略处理这个任务,否则就会陷入到处理异常的循环之中。
Session::~Session()
{
try {
logDestruction(this);
}
catch (...) { }
}
条款12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
-
抛出异常、传递参数、调用虚函数之间的区别主要有三点:
(1)异常对象在传递时(从throw传递到catch)总被进行拷贝(包括by-value、by-reference,by-pointer),生成一个临时对象;当通过传值(by-value)方式捕获时,异常对象被拷贝了两次(第一次是异常传递时生成的一个临时对象,第二次是临时对象给catch的形参赋值)。然而对象做为参数传递给函数时不一定需要被拷贝。
(2)对象做为异常被抛出与做为参数传递给函数相比,前者类型转换比后者要少(前者只有两种转换形式)。
(3)catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序第一个类型匹配成功的catch将被用来执行。当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里,即使该类不是在源代码的最前头。 -
千万不要抛出一个指向局部变量的指针。
-
异常对象抛出时,一般是严格匹配(int只能匹配int,不能匹配double等等隐式转换),只能有两种类型转换:
(1)catch声明接收基类对象,异常可以抛出对应的基类类型和派生类类型对象;
(2)catch声明接收const void *指针,异常可以抛出所有类型的指针; -
不要把处理基类异常的catch子句放在处理派生类异常的catch子句的前面,因为异常catch匹配是顺序匹配(最先匹配的catch),不是最佳匹配。
-
函数调用过程中不允许将一个临时对象传递给一个non-const-reference参数,因为函数参数要求传入引用说明想修改引用的变量,结果变量是临时对象,即使修改了也是做无用功,所以编译器不允许;但是异常里面可以,因为异常里面参数传递都是副本,传引用也不是为引用异常抛出前的原始对象,而是相较于by-value减少一次副本拷贝。
条款13:以by reference方式捕捉exception
- 用by pointer指针的方式捕捉异常的缺点:必须保证抛出的指针指向的东西在离开原函数作用域后依然存在,例如堆上的对象、全局对象、静态对象,局部对象不可以。
- 用by value传值的方式捕捉异常的缺点:(1)by value需要复制对象两次;(2)传递继承体系中的对象时,可能会产生切割问题。例如抛出派生类对象,catch中当作基类类型接收,就会造成派生类对象中的派生成员丢失,调用虚函数也只会调用基类的虚函数而非派生类。
class exception { // 如上,这是
public: // 一个标准异常类
virtual const char * what() throw();
// 返回异常的简短描述.
... // (在函数声明的结尾处
// 的"throw()",
}; //有关它的信息
// 参见条款14)
class runtime_error: //也来自标准C++异常类
public exception { ... };
class Validation_error: // 客户自己加入个类
public runtime_error {
public:
virtual const char * what() throw();
// 重新定义在异常类中
... //虚拟函数
}; //
void someFunction() // 抛出一个 validation
{ // 异常
...
if (a validation 测试失败) {
throw Validation_error();
}
...
}
void doSomething()
{
try {
someFunction(); // 抛出 validation
} //异常
catch (exception ex) { //捕获所有标准异常类
// 或它的派生类
cerr << ex.what(); // 调用 exception::what(),
... // 而不是Validation_error::what()
}
}
- 用by reference引用的方式不会造成多次复制、切割问题、对象被提前删除的问题。
void someFunction() //这个函数没有改变
{
...
if (a validation 测试失败) {
throw Validation_error();
}
...
}
void doSomething()
{
try {
someFunction(); // 没有改变
}
catch (exception& ex) { // 这里,我们通过引用捕获异常
// 以替代原来的通过值捕获
cerr << ex.what(); // 现在调用的是
// Validation_error::what(),
... // 而不是 exception::what()
}
}
条款14:明智运用exception specifications
- 当A函数调用B函数,但因为B函数可能抛出一个不在A函数异常规格之内(抛出异常类型不匹配,无法处理)的异常,所以这个函数调用就违反了A函数的异常规格(exception specifications)。如果违反了异常规格,程序就会调用unexpected函数,调用terminate->默认行为调用abort函数,直接终止程序。
- 避免异常规格的方法1:避免在带有类型参数的模板内使用异常规格。因为模板总是采用不同的方法使用类型参数。解决方法只能是模板和异常规格不要混合使用。
// 这个模板包含的异常规格表示模板生成的函数不能抛出异常。
//但是事实可能不会这样,因为opertor&能被一些类型对象重载。
//如果被重载的话,当调用从operator==函数内部调用opertor&时,
//opertor&可能会抛出一个异常,这样就违反了我们的异常规格,使得程序控制跳转到unexpected。
template<class T>
bool operator==(const T& lhs, const T& rhs) throw()
{
return &lhs == &rhs;
}
- 避免异常规格的方法2:如果A函数内调用B函数,而B函数无异常规格,那么A函数本身也不要设定异常规格。注意有可能通过函数指针调用的函数无异常规格,比较隐晦。
- 避免异常规格的方法3:当阻止非预期异常是件不切实际的事情,那就放弃预防,直接处理异常。C++允许以不同类型的exception取代非预期的exception。如果被违反的异常规格包含UnexpectedException异常,那么异常传递将继续下去,好像异常规格总是得到满足。如下希望所有的unexpected异常都被替换为UnexpectedException对象:
class UnexpectedException {}; // 所有的unexpected异常对象被
//替换为这种类型对象
void convertUnexpected() // 如果一个unexpected异常被
{ // 抛出,这个函数被调用
throw UnexpectedException();
}
//通过用convertUnexpected函数替换缺省的unexpected函数,来使上述代码开始运行。:
set_unexpected(convertUnexpected);
另一种把unexpected异常转变成已知类型的方法是替换unexpected函数,让其重新抛出当前异常,这样异常将被替换为bad_exception。
void convertUnexpected() // 如果一个unexpected异常被
{ //抛出,这个函数被调用
throw; // 它只是重新抛出当前
} // 异常
set_unexpected(convertUnexpected); // 安装 convertUnexpected
// 做为unexpected的替代品
条款15:了解异常处理的成本
- 异常处理成本:在内存空间上需要额外放置某些数据结构记录哪些对象已经被完全构造;在时间上需要保持哪些数据结构的正确性;使用try…catch…会造成代码体积膨胀;编译器会为有异常处理的程序花费更多的编译时间。
- 为了使你的异常开销最小化,只要可能就尽量采用不支持异常的方法编译程序,把使用try块和异常规格限制在你确实需要它们的地方,并且只有在确为异常的情况下才抛出异常。