分享

More Effective C++ (M9 -to-M15)

 My Room 2012 2012-03-20
M9:使用析构函数防止资源泄漏
你需要对用来操纵局部资源(local resources)的指针说再见。
隐藏在auto_ptr后的思想是:用一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源.
资源应该被封装在一个对象里,遵循这个规则,你通常就能避免在存在异常环境里发生资源泄漏。
M10:在构造函数中防止资源泄漏
C++仅仅能删除被完全构造的对象(fully contructed objects, 只有一个对象的构造函数完全运行完毕,这个对象才被完全地构造。如果一个对象做为局部对象建立,并且在构造对象的过程中,一个异常被抛出,对象的析构函数不会被调用。
如果你用对应的auto_ptr对象替代指针成员变量,就可以防止构造函数在存在异常时发生资源泄漏,你也不用手工在析构函数中释放资源,并且你还能象以前使用非const指针一样使用const指针,给其赋值。
M11:禁止异常信息(exceptions)传递到析构函数外
如果在析构函数中抛出异常,并且该析构函数的调用源自于其他异常的抛出,那么terminate函数将被自动调用,彻底终止你的程序。
如果一个异常被析构函数抛出而没有在函数内部捕获住,那么析构函数就不会完全运行(它会停在抛出异常的那个地方上)。如果析构函数不完全运行,它就无法完成希望它做的所有事情。
综上所述,我们知道禁止异常传递到析构函数外有两个原因,第一能够在异常转递的堆栈辗转开解(stack-unwinding)的过程中,防止terminate被调用。第二它能帮助确保析构函数总能完成我们希望它做的所有事情。
M12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
1.调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。
2.C++规范要求被做为异常抛出的对象必须被复制。当通过传值方式捕获时,异常对象被拷贝了两次,对象做为参数传递给函数时不一定需要被拷贝,所以抛出异常运行速度比参数传递要慢。当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。
一般来说,你应该用throw来重新抛出当前的异常,因为这样不会改变被传递出去的异常类型,而且更有效率,因为不用生成一个新拷贝。
catch (Widget& w)                 // 捕获Widget异常
{
  ...                             // 处理异常
  throw;                          // 重新抛出异常,让它
}                                 // 继续传递
catch (Widget& w)                 // 捕获Widget异常
{
  ...                             // 处理异常
  throw w;                        // 传递被捕获异常的
}                                 // 拷贝
当抛出一个异常时,系统构造的(以后会析构掉)被抛出对象的拷贝数比以相同对象做为参数传递给函数时构造的拷贝数要多一个。
通过指针抛出异常:你不能抛出的指针是一个指向局部对象的指针,必须是全局的或堆中的对象。
3.函数参数传递会进行隐式类型转换,但是异常不会。在catch子句中进行异常匹配时可以进行两种类型转换:
第一种是继承类与基类间的转换。一个用来捕获基类的catch子句也可以处理派生类类型的异常,这种派生类与基类(inheritance_based)间的异常类型转换可以作用于数值、引用以及指针上。
第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常。
4.catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的catch将被用来执行。当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里,即使该类不是在源代码的最前头。
M13:通过引用(reference)捕获异常
指针:1通过指针捕获异常,将遇到一个哈姆雷特式的难题:是删除还是不删除?这是一个难以回答的问题,因为你不知道它是在全局或静态对象地址还是堆中建立的异常对象的地址,所以你最好避开它。
     2通过指针捕获异常也不符合C++语言本身的规范。四个标准的异常――bad_alloc(当operator new(参见条款M8)不能分配足够的内存时,被抛出),bad_cast(当dynamic_cast针对一个引用(reference)操作失败时,被抛出),bad_typeid(当dynamic_cast对空指针进行操作时,被抛出)和bad_exception(用于unexpected异常;参见条款M14)――都不是指向对象的指针,所以你必须通过值或引用来捕获它们。
值: 1系统将对异常对象拷贝两次.2而且它会产生slicing problem,即派生类的异常对象被做为基类异常对象捕获时,那它的派生类行为就被切掉了(sliced off)。(当一个对象通过传值方式传递给函数,也会发生一样的情况――参见Effective C++ 条款22)。
引用:通过引用捕获异常(catch-by-reference)能避开上述所有问题。不象通过指针捕获异常,这种方法不会有对象删除的问题而且也能捕获标准异常类型。也不象通过值捕获异常,这种方法没有slicing problem,而且异常对象只被拷贝一次。
M14:审慎使用异常规格(exception specifications)
extern void f1();         // 可以抛出任意的异常
void f2() throw(int);     //函数f2通过它的异常规格来声明其只能抛出int类型的异常:
f2调用f1是非常合法的,即使f1可能抛出一个违反f2异常规格的异常,使一个特殊函数unexpected被自动地调用,使程序终止。
避免违反异常规格的方法:
1.避免在带有类型参数的模板内使用异常规格。
2.如果在一个函数内调用其它没有异常规格的函数时应该去除这个函数的异常规格。(即不抛出任何异常)
3.避免调用unexpected的第三个方法是处理系统本身抛出的异常。C++允许你用其它不同的异常类型替换unexpected异常,你能够利用这个特性。

void convertUnexpected()               // 如果一个unexpected异常被

{                                      // 抛出,这个函数被调用

  throw UnexpectedException(); 

}

set_unexpected(convertUnexpected);

M15:了解异常处理的系统开销

第一,异常是C++的一部分,C++编译器必须支持异常,即在生成的代码里包含进支持异常的内容,编译器能够让你自由选择是否加入异常内容。

第二个开销来自于try块.如果你使用try块,代码的尺寸将增加5%-10%并且运行速度也同比例减慢。为了减少开销,你应该避免使用无用的try块。

编译器为异常规格生成的代码与它们为try块生成的代码一样多,所以一个异常规格一般花掉与try块一样多的系统开销。

抛出异常的开销:与一个正常的函数返回相比,通过抛出异常从函数里返回可能会慢三个数量级。这个不用担心,引用异常很少会出现。

不论异常处理的开销有多大我们都得坚持只有必须付出时才付出的原则。为了使你的异常开销最小化,只要可能就尽量采用不支持异常的方法编译程序,把使用try块和异常规格限制在你确实需要它们的地方,并且只有在确为异常的情况下(exceptional)才抛出异常。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多