条款 9:利用 destructors 避免泄露资源

  • 问题的提出:使用指针时,如果在 delete 指针之前产生异常,将会导致不能删除指针,从而产生资源泄漏。【无法释放 heap 中数据】

    class Animal {public:virtual void processAdoption() = 0;
    }
    class Cat : public Animal {public:virtual void processAdoption();
    }
    class Dog : public Animal {public:virtual void processAdoption();
    }void batchAdoption(istream& dataSource) {while (dataSource) {Animal *animal = readAnimal(dataSource); // 可能抛出异常animal->processAdoption();  // 可能抛出异常delete animal;}
    }
    // 在上面的batchAdoption()方法中
    // readAnimal()和 processAdoption() 都可能抛出异常
    // 程序中断,从而导致delete animal无法执行,内存泄漏发生。
    
  • 有两种解决方案:
    • 第一种:利用异常捕获,即 try、catch。【缺点就是代码比较冗余】

      void batchAdoption(istream& dataSource) {while (dataSource) {Animal *animal = readAnimal(dataSource); // 不能放入try中,否则animal对外部不可见try {animal->processAdoption();} catch (...) {delete animal;  // 代码冗余throw;}delete animal;  // 代码冗余}
      }
      
    • 第二种:使用对象封装资源(把资源封装在对象内)。【用类似指针的对象来取代指针,即智能指针】
      void batchAdoption(istream& dataSource) {while (dataSource) {auto_ptr animal(readAnimal(dataSource));animal->processAdoption();// 无需调用语句delete animal,出了作用域即调用析构函数}
      }
      
  • 总结:
    • 使用对象封装资源,如使用 auto_ptr,使得资源能够自动被释放。
    • 智能指针的核心思想:以一个对象存放必须自动释放的资源,并依赖该对象的析构函数释放资源。
    • 用了智能指针,即使函数内抛出异常,资源仍然会得到释放。

条款 10:在 constructors 内阻止资源泄露(resource leak)

  • 条款 9 针对的问题是:假若在函数被调用的情况下发生异常,heap 中资源将无法被释放,导致内存泄漏问题发生。
  • 而本条款针对的问题是:当类中需要包含多个 heap 对象,但是在构造函数中出现异常的情况下,如何释放掉已经创建的 heap 对象
    • 即在构造函数中,先 new A,再 new B,然而在 new B 的过程中出现异常,此时 new A 指向的内存就会出现内存泄漏。
    • 出现内存泄漏的原因是:C++ 只会析构已构造完成的对象,对象只有在其 constructor 执行完毕才算是完全构造妥当。【即在前面提及的场合下,class A 的析构函数不会被调用】
  • 在前面描述的问题中,可以如同上一条款提及的指导思路一样:先考虑异常捕获(即 try、catch)。
    • 但它也有问题:带来了重复的代码(因为需要对每个在 heap 中的内存资源进行记录,即 try catch 每一个资源)。
    • 可以提取出重复代码,将其包装到一个函数中去(即 try catch 创建所有资源的过程,然后在 catch 中调用共同的操作)。
  • try…catch 方法有一个致命的弱点:
    • 如果某一个变量声明为 const 类型,就不得不将其初始化动作放到初始化列表(member initialization list)中,我们就无法像前面说的那样使用 try…catch 来捕捉异常,依然会造成内存资源泄漏。
  • 如果这些 heap 中变量本身就是 const(如下面代码所示,image 和 audio 均是常量指针,只能放在 member initialization list 中初始化),则可以采用下面这个方案:【可以解决问题,但是代码不简洁】
    BookEntry::BookEntry(const string &name, const string& address,const string& image, const string& audio) : m_name(name),  // name 是非指针变量m_address(address),  // address 是非指针变量m_image(createImage(image)),  // image 和 audio 均是常量(const)指针m_audio(createAudio(audio)) {}
    Image* createImage(const std::string& image) { // image 在 audio 面前初始化,所以不需要捕捉异常if(image != "") return new Image(image);else return 0;
    }
    Audio* createAudio(const std::string& audio) { // audio 第二个初始化,所以捕捉异常到异常后需要释放 image 对象try {if(audio != "") return new Audio(audio);else return 0;} catch () {delete m_image;  // 捕捉到异常需要释放已经初始化的 image 对象throw;}
    }
    // 可以看到针对m_image变量定义了构建函数
    // 在heap中创建变量的工作放到这个构建函数中,并返回创建好的指针
    
  • 当然,最合适的解决方案仍是利用对象来封装资源的指导思想:
    • 就像上一个条款建议的,我们直接将指针包裹到 auto_ptr 中(将前面的 image 指针和 audio 指针改为被智能指针包裹即可),利用作用域生命周期来控制具体的行为。

条款 11:禁止异常(exceptions)流出 desrtuctors 之外

  • 析构函数会在下面两种情况下被调用:

    • 对象在正常情况下被销毁,即离开对象所在作用域或者主动销毁,对象生命周期终结,析构函数被调用,对象被销毁;
    • 异常抛出引起了栈展开(stack-unwinding),析构函数会被调用,简单地来说,异常会造成析构函数被调用。【这种情况说明了:调用析构函数的时候可能正存在着异常,但析构函数无法区分是否有异常】
  • 栈展开的相关机制有兴趣可参考相关内容,但本条款的关键在于:在进行栈展开过程中(此时说明已有一个异常),析构函数若再次抛另一个异常,则会导致标准库函数 terminate 被调用,terminate 函数调用 abort 函数,程序异常退出。【简单来说,二次异常抛出,析构函数直接中止,不仅销毁过程未做完就结束了,整个程序都会中止】
  • 因此,析构函数应该从不抛出异常,其解决方法是:在析构函数中使用 try-catch 块屏蔽所有异常,直接拦截析构函数中抛出的异常,保证不会有更多的异常向上传递:【注意 catch 语句中什么都不可以处理,因为处理语句也有可能抛出异常,这就使得局面又回到原点】
    ~Destructor() {try {doSomething();} catch (...) {// doNothing, avoid more exception}}
    
  • 总结如下,若析构函数抛出异常,有两种危害:
    1. 异常点之后的语句无法完成,析构工作没有完成。
    2. 有可能是栈展开调用析构函数,可能出现两次异常抛出导致程序终结的情况。

条款 12:了解“抛出一个exception”与“传递一个参数”或“调用一个虚函数”之间的差异

  • 本条款旨在介绍异常处理的细节。
  • 首先要理解,抛出异常与函数调用有许多类似的地方:
    1. 某个类对象被接受;
    2. 被接受的类对象可以选择不同的接收端,从而实现多态;
    3. 可以通过 by-value、by-reference 和 by-pointer 三种方式其中一种来传递类对象。【即 catch 语句的参数可以是值、引用或指针】
  • 但无论是通过什么方式来传递对象,被抛出的对象总是一个副本,这样做保证了,catch捕获的对象总能存在:
    • 即每当抛出 exception 时候,exception 总会被复制,然后复制的副本会被传递给 catch 子句。【从另外一个角度来说,无论 catch 语句对接收到的对象进行了什么处理,均是对副本的处理,不会影响原来的对象】
    • 相关示例如下:
      // 示例1:抛出的异常为局部变量
      void passThrowWidget() {Widget widget;doSomething(widget);// 抛出的对象是widget的一个副本// 当前作用域的widget在离开本函数时已经被销毁throw widget;
      }// 示例2:抛出的异常为静态局部变量
      void passThrowWidget() {static Widget widget;doSomething(widget);// 尽管本函数内的widget不会被销毁,但是抛出的widget依然是一个副本throw widget;
      }
      // 无论原对象以什么形式定义,抛出的对象总是一个副本。
      // 这样做保证了,catch捕获的对象总能存在,
      // 否则可能导致捕获的异常对象已经被销毁。
      
  • 还有另一个需要注意的:被抛出的异常对象会调用其复制构造函数,复制构造函数以静态类型为模板创建。【即以静态绑定的类型为准】
    • 实例代码如下:

      class Widget; // 基类
      class ChildWidget : public Widget { // 派生类
      }void passThrowWidget() {ChildWidget child; // ChildWidget 是 child 的类型Widget &widget = child;  // Widget 是 widget 的静态类型throw widget;  // 调用 Widget 的复制构造函数进行复制,而不是 ChildWidget
      }
      
    • 此时考虑在 catch 语句块内传播异常
      catch (Widget& w) // 方案一:不复制异常,而是直接抛出当前的异常
      {// ...throw; // 重新抛出当前的异常,不管 w 的动态类型是什么,最后都可以得到保证
      }catch (Widget& w) // 方案二:复制后抛出
      {// ...throw w; // 抛出当前的异常的副本,相当于新的异常,且副本只保留了原对象静态类型
      }
      // 方案二带来的问题是:// 复制操作带来的开销// 复制行为是基于静态类型的拷贝,因此传递抛出的对象可能不是原来想要传递的对象
      
  • catch 效率:【在这部分讨论了异常通过值、引用来传递】
    • 前面说了,传入 catch 的异常有如下三种形式:【后面再讨论 by pointer 的方式捕捉异常】

      1. catch (Widget w)【by value 方式捕捉】
      2. catch (Widget &w)【by reference 方式捕捉】
      3. catch (const Widget &w)【by reference-to-const 方式捕捉】
    • 这里就有一个地方展现了函数调用与异常捕获的不一样:
      • 前面说过了被抛出的异常对象是一个拷贝后的临时对象,则说明 catch 语句可以通过 reference-to-const 方式捕捉一个临时对象;
      • 而函数调用是不允许的,不能对传入函数中的临时变量进行修改,即无法把临时对象传递给一个 non-const reference。【对于函数调用而言,接受临时变量的函数参数只能是上面的 by value 形式和 by reference-to-const 形式】
    • 回到异常抛出,如果采用第 1 种方式 catch 异常(值传递),则抛出异常将会被复制两次:第一次是在抛出时,第二次是在 catch 时。【所以高效做法是采用引用的方式(第 2、3 种方式)捕获异常】
  • 接下来要讨论的是 catch 以 by pointer 的形式捕捉异常:
    • 指针也可以当作异常被接受,与上面复制类道理相同,抛出异常时,指针将会被复制。
    • 由于离开作用域后,局部变量会被销毁,因此不能抛出一个局部变量的指针
  • 异常与类型吻合(type match)的关系:
    • 函数调用的参数是允许隐式转换的,如 int 转为 double;而异常捕获的参数是不允许前述的基本隐式转换,即对 int 异常的抛出不会被捕获 double 异常的 catch 语句捕获到。
  • 但是异常机制又允许另外两种转换:
    1. 继承体系中的异常转换:针对基类异常的 catch 子句,可以处理继承类的异常。【该规则使用于 by value、by reference 和 by pointer 三种形式】
    2. 允许从有型指针到无型指针的转换。【即 catch (const void*) 可以捕捉任意指针类型的异常】
  • 最后要提及的一点不同是:
    • try 语句后可能会跟着多个不同的 catch 语句,而 catch 语句的匹配总是按照顺序而进行的,即 catch 语句实行最先匹配策略。【与对象调用虚函数的动作比较,会实行最佳匹配策略,被调用的函数是与对象类型最佳匹配的函数】
    • 实例如下所示:
      try{}
      catch (base& ex){// ...
      }
      catch (derived& ex){ // 这个语句永远不会被执行,因为所有针对继承类的异常都被前面的语句捕获了// ...
      } // 要想该语句被执行,只能将该语句移到 catch(base& ex) 的前面去
      
  • 最后总结,函数调用和异常抛出的区别如下:
    1. 异常对象总是会被复制,如果以 by value 方式捕捉,甚至会被复制两次。
    2. 异常抛出的对象允许的类型转换动作不多,不支持基本的类型转换。
    3. catch 子句实行最先匹配策略,以出现的顺序来进行匹配操作,第一个匹配成功者便执行。

条款 13:以 by reference 方式捕捉 exceptions

  • 本条款其实已经包含在前一条款内,但单独提出,介绍异常通过引用来捕获的好处。
  • 异常指针传递带来的麻烦:【讨论为什么不推荐指针传递异常】
    • 指针传递如下所示:

      void func() {Widget error; // 形式1static Widget errorStatic; // 形式2Widget *errorHeap = new Widget; // 形式3throw &error; // 形式1throw &errorStatic; // 形式2throw errorHeap; // 形式3
      }
      catch (Widget *widget) {// ...
      }
      
    • 三种形式的缺点如下:
      • 形式1:一旦离开局部变量的作用域,局部变量就会被销毁,此种方式是错误的。
      • 形式2:无法时刻谨记,同时长期保存一个函数内的局部变量会带来很大空间开销。
      • 形式3:外界需要维护指针,当指针用完后,就需要外界调用delete来销毁,维护成本过高。
  • 异常对象值传递带来的麻烦:【讨论为什么不推荐值传递】
    • 值传递也存在两个问题:

      1. 复制两次异常对象,抛出异常对象一次,catch 对象时又被复制一次。
      2. 不能使用虚函数实现多态。【对象切割问题】
    • 实例如下所示:
      // 问题一的体现:
      void func() {Widget widget;throw widget; // 第一次复制
      }
      catch (Widget widget) { // 第二次复制
      }// 问题二的体现:
      class exception {public:virtual const char *what() throw(); // ”throw()“关键字声明该函数不会抛出任何异常
      }
      class DerivedException : public exception {public:virtual const char *what() throw(); // 虚函数实现多态
      }
      void func() {DerivedException widget;throw widget; // 的确是抛出了派生类的异常,但是捕获函数中会将其切割为基类,随后就调用了基类的 what 函数
      }
      catch (exception widget) { // 捕捉继承体系里的所有异常widget.what(); // 调用的是exception::what(),这种情况叫做slicing(切割),即子类信息被切割掉,只留下基类的信息
      }
      
  • 异常引用传递带来的好处:
    • 虽然抛出异常时的复制无法避免,但是 catch 时采用引用方式,因此可以避免第二次复制异常,最后总共复制了一次异常对象。
    • 同时,引用使得我们可以顺利调用虚函数,实现多态,实例如下所示:
      void func() {DerivedException widget;throw widget;
      }
      catch (exception& widget) {widget.what(); // 调用的是DerivedException的what,实现了多态
      }
      

条款 14:明智运用 exception specifications

  • 所谓的异常说明,指的是明确指出一个函数可以抛出什么样的异常。【一般就不要用,不用就表明函数可以产生任意的异常】

    • 标识符 throw 即为异常限定符,异常限定符标识了函数可以抛出的异常类型。
    • 当 throw 后面的括号内容为空,即 throw(),则表示该函数不抛出任何异常。
    • void f2() throw(int); 表示 f2 只抛出类型为 int 的异常。
  • 如果函数抛出一个未列于其 exception specification 的异常,这个错误将会在运行期被检测出来,于是特殊函数 unexpected 会被自动调用。
    • 紧接着的调用链为 unexpected() -> terminate() -> abort(),因此程序如果违反异常生命,缺省结果就是程序被中止。
  • 下面将会讨论如何避免 unexpected 函数被调用:【编译器不会阻止情况发生,程序员需要自己避免异常声明不被打破】
    • 方法一,避免将异常声明放在需要型别自变量的 templates 身上;

      template<class T>
      bool operator==(const T& left, const T& right) throw() { // 这样是一种不好的做法return &left == &right;
      }
      // 我们无法确定,取地址操作符“&”是否已经被重载,且可能抛出异常。
      // 此种情况的实质是,我们无法确定,所有类对象的同名函数都不会抛出异常。
      
    • 方法二,外层函数不使用 throw() 进行修饰:如果函数 A 内调用了函数 B,而函数 B 无 exception specification,那么 A 函数本身也不要设定 exception specification。【内部允许产生所有异常,外部自然也不要加以限制】
      • 容易被忽略的情况是注册回调函数:【如下面代码所示,makeCallBack 函数不应该异常声明,因为没有任何办法清楚注册来的 func 指针可能抛出什么异常】

        typedef void (*CallbackPtr)();
        class Callback {public:Callback(CallbackPtr func) : m_func(func) {}void makeCallBack() throw() { // 这里的异常声明很容易带来问题m_func(); // 可能抛出异常}private:CallbackPtr m_func;
        }
        // 如代码所示,如果注册的“回调函数”没有throw修饰,
        // 而调用“回调函数”的外层函数却有throw修饰,
        // “回调函数”抛出异常就会引起程序终止。
        
    • 方法三,处理系统可能抛出的 exceptions;如果无法处理,可以自定义 unexpected 函数。【该节后续部分不展开了,看不懂】
      • C++ 提供了函数 set_unexpected(),可以向该函数中传递我们自定义的函数,来替换默认的 unexpected()

条款 15:了解异常处理(exception handling)的成本

  • 为了能够在运行期处理 exceptions,程序必须做大量的簿记工作。

    • 在每一个执行点,它们必须能够确认如果发送 exception,哪些对象需要析构;
    • 它们必须在每一个 try 语句块的进入点和离开点做记号;
    • 针对每个 try 语句块它们必须记录对应的 catch 子句以及能够处理的 exceptions 型别。

【读书笔记】【More Effective C++】异常(Exceptions)相关推荐

  1. [读书笔记]《Effective Modern C++》—— 移步现代 C++

    文章目录 前言 item7:区别使用 () 和 {} 创建对象 item8:优先考虑使用 nullptr 而不是 0 或者 NULL item9:优先考虑别名声明而非 typedefs item10: ...

  2. 《Head First Java》读书笔记(3) - 异常和IO

    1.异常处理 我们在调用某个方法时,会被编译器告知需要捕捉异常和处理,意味着你调用的这个方法是有风险的,可能会在运行期间出状况,你必须写出在发生状况时加以处理的代码,未雨绸缪!这就是Java中异常处理 ...

  3. 读书笔记--《Effective C#》总结

    值得推荐的一本书,适合初中级C#开发人员 第1章 C#语言元素 原则1:尽可能的使用属性(property),而不是数据成员(field) ● 属性(property)一直是C#语言中比较有特点的存在 ...

  4. 读书笔记《Effective C++》条款40:明智而审慎地使用多重继承

    一旦涉及多重继承,C++社群便分为两个基本阵营.一派认为如果单一继承是好的,多重继承一定更好.另一派主张,单一继承是好的,但多重继承不值得使用. 最先需要认清的一件事是,当用到多重继承,程序有可能从一 ...

  5. 读书笔记:Effective Java-第11章 并发Concurrency

    目录 Item 78: Synchronize access to shared mutable data Item 79: Avoid excessive synchronization Item ...

  6. CSSAPP 稀里糊涂的的读书笔记目录

    这个笔记是经过两个月断断续续的读完cssapp的总结,全书一道题没有做,很多示例代码也看的不是很仔细或者根本就没看.算是囫囵吞枣地过了一遍. 当然,这种书,我是不奢望只读一遍就能搞懂书中大部分内容的, ...

  7. Effective C++读书笔记 摘自 pandawuwyj的专栏

    Effective C++读书笔记(0)       Start   声明式(Declaration):告诉编译器某个东西的名称和类型,但略去细节.   std::size_t numDigits(i ...

  8. more effective c++和effective c++读书笔记

    转载自http://bellgrade.blog.163.com/blog/static/83155959200863113228254/,方便日后自己查阅, More Effective C++读书 ...

  9. 《Effective C++》读书笔记(第一部分)

    有人说C++程序员可以分为两类,读过Effective C++的和没读过的.世界顶级C++大师Scott Meyers 成名之作的第三版的确当得起这样的评价. 本书并没有你告诉什么是C++语言,怎样使 ...

  10. [读书笔记]Effective C++ - Scott Meyers

    [读书笔记]Effective C++ - Scott Meyers 条款01:视C++为一个语言联邦 C++四个次语言: 1. C Part-of-C++,没有模板.异常.重载. 2. Object ...

最新文章

  1. 通过分离dataSource 让我们的code具有更高的复用性.
  2. 3a三次方h c语言表达式,希尔伯特曲线——第八届蓝桥杯C语言B组(国赛)第三题...
  3. 深度学习模型的中毒攻击与防御综述
  4. 创建私有CA及私有CA的使用
  5. 肖邦夜曲21_原装进口 | 肖邦夜曲全集 鲁宾斯坦 钢琴经典 2CD
  6. 用户二次登陆,干掉第一次登录的session
  7. python调用C语言函数(方法)的几种方法
  8. 活学巧用电脑上网实例入门
  9. 软件设计师下午题java_2018上半年软件设计师下午真题(三)
  10. java学生签到系统视频教程_手把手教你做一个Java web学生信息、选课、签到考勤、成绩管理系统附带完整源码及视频开发教程...
  11. java引用数据类型可以更改类型_java,基本数据类型和引用数据类型
  12. python七段数码管绘制秒表_Python绘制七段数码管实例代码
  13. 物联网进入规模化应用时代 万物互联时代到来
  14. 索尼Xperia 2带壳渲染图曝光:外形依然很索尼
  15. Ansible(三)编写ansible的playbook文件(实现端口更改、远程主机信息采集、负载均衡)
  16. 忽略“Signal: SIGSEGV (Segmentation fault)”
  17. “VMRC控制台的连接已断开…正在尝试重新连接”的解决方法
  18. 深度迁移度量网络 Deep Transfer Metric Learning
  19. C#Assembly详解
  20. 初三毕业班主任压力过大割喉自尽

热门文章

  1. UGC、PGC、OGC
  2. C语言指针详解(初级)
  3. 简单的图书管理系统(类的练习)
  4. NOIP2013提高组 day2
  5. 联想小新Pro 16频繁蓝屏解决方案
  6. 常见内网穿透-花生壳、神卓互联、FRP、ngork分析
  7. sizeof求二维数组的大小
  8. 数据结构——图的邻接表实现
  9. 经典网络架构学习-ResNet
  10. 网站压力测试工具was