【读书笔记】【More Effective C++】异常(Exceptions)_more effective c++ 异常-程序员宅基地

技术标签: # More Effective C++  c++  

条款 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; // 形式1
        static Widget errorStatic; // 形式2
        Widget *errorHeap = new Widget; // 形式3
        
        throw &error; // 形式1
        throw &errorStatic; // 形式2
        throw 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 型别。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_44705592/article/details/126552809

智能推荐

jupyter简单快速安装方法!在vscode使用jupyter打开即可使用_vscode shift+enter进入juypter-程序员宅基地

首先,安装一个稳定的pip版本:python -m pip install pip==20.0.1 -i https://pypi.tuna.tsinghua.edu.cn/simple/其次,再安装jupyter notebookpip install -i https://pypi.tuna.tsinghua.edu.cn/simple jupyter最后,在vscode安装jupyter使用方法:新建一个ipynb文件即可快捷键两个必记:shift+enter表示增加一个cellc_vscode shift+enter进入juypter

基于FIFO实现数据的接收(TMS320F28335)_串口fifo接收数据_bjc15050103的博客-程序员宅基地

1.首先,系统的初始化,初始化SCI时钟功能void InitPeripheralClocks(void){ EALLOW;// HISPCP/LOSPCP prescale register settings, normally it will be set to default values SysCtrlRegs.HISPCP.all = 0x0001; SysCtrlRegs.LOSPCP.all = 0x0002; SysCtrlRegs.PCLKCR0.bi_串口fifo接收数据

垂直拆分和水平拆分_垂直划分和水平划分-程序员宅基地

垂直拆分就是要把表按模块划分到不同数据库表中(当然原则还是不破坏第三范式),这种拆分在大型网站的演变过程中是很常见的。当一个网站还在很小的时候,只有小量的人来开发和维护,各模块和表都在一起,当网站不断丰富和壮大的时候,也会变成多个子系统来支撑,这时就有按模块和功能把表划分出来的需求。其实,相对于垂直切分更进一步的是服务化改造,说得简单就是要把原来强耦合的系统拆分成多个弱耦合的服务_垂直划分和水平划分

Linux 安装 Node.js 步骤指导_node.js安装教程linux-程序员宅基地

Node 是一个让 JavaScript 运行在服务端的开发平台因为业务需求,想在自己的后台中部署 node.js 服务在此整理一下,在 Linux 系统下的 Node.js 安装指导,希望能帮到各位有需求的小伙伴..._node.js安装教程linux

Linux命令之查看文件占用空间大小-du,df-程序员宅基地

转载自:《du命令》-linux命令五分钟系列之三du(disk usage),顾名思义,查看目录/文件占用空间大小#查看当前目录下的所有目录以及子目录的大小$ du -h $ du -ah#-h:用K、M、G的人性化形式显示#-a:显示目录和文件 du -h tmp du -ah tmp#只查看当前目录下的tmp目录(包含子目录)的大小#查看当前目录...

单元测试系列:Mock工具之Mockito实战_mockito怎么mock service中工具类的行为-程序员宅基地

原文https://www.cnblogs.com/zishi/p/6780719.html原文链接:http://www.cnblogs.com/zishi/p/6780719.html在实际项目中写单元测试的过程中我们会发现需要测试的类有很多依赖,这些依赖项又会有依赖,导致在单元测试代码里几乎无法完成构建,尤其是当依赖项尚未构建完成时会导致单元测试无法进行。为了解决这类问题我们引入了M..._mockito怎么mock service中工具类的行为

随便推点

【UWB定位】 - DWM1000模块调试简单心得 - 2_读取dwm1000 一直是0-程序员宅基地

UWB定位 - DWM1000模块调试简单心得 - 1 上一篇搭建了下软硬件的基础环境,这篇开始记录调试需要注意的地方或者”坑”。先以一基站一标签来进行。1、将我们的模块连接后上电。注意这里DWM1000模块(也就是stm32开发板)一定要使用独立电源(5v / 1.5A ↑↑↑)单独供电,如果你用USB to TTL或者电脑的USB接口给stm32开发板供电(dwm10003.3V与stm3..._读取dwm1000 一直是0

控件命名规则_控件命名规范-程序员宅基地

控件名简写控件名简写Web 窗体LabellblTextBoxtbButtonbtnLinkButtonlbHyperLinkhlRepeatorrpt_控件命名规范

数据的存储介质-磁盘的RAID-程序员宅基地

上次介绍了磁盘,这篇来介绍一下RAID要介绍RAID技术的原因,其实是因为目前大部分分布式存储在做的事情其实RAID在很多年前就已经做到了,所以如果你希望做存储相关的事情,那么RAID是必须要理解,但不一定要用到的概念:)计算机要存储和读取数据,主要依托这么两个部件:1.通信管道和通信协议,心灵感应还需要靠电波通信呢不是?~一般来说这通信管道在计算机内就是总线,使用电信号,在...

mediacodec 编码yv12为h264 编码一帧后dequeueOutputBuffer 一直返回-1_海思 dequeueoutputbuffer -1-程序员宅基地

在用Android MediaCodec编码h264的时候,会遇到,dequeueOutputBuffer在成功获取到config帧(sps pps)及第一个I帧后,dequeueOutputBuffer然后结果一直为-1的情况, 在三星note3及小米3,小米6都是这样的,解决方案如下:关键在下面这个函数的第四个参数上,就是时间戳,这个参数一定要填写,可以类似的这么简单的算一下:..._海思 dequeueoutputbuffer -1

Qt5.11+Opencv3.4学习笔记之配置Opencv-程序员宅基地

首先,到opencv官网上面下载opencv的安装包。由于我实在Windows环境下进行的配置,所以说下载win pack。下载完之后双击运行,出现下图所示窗口。找一个吉利的路径把他存放好(最好不要是中文路径),而后点击Extract。等待读条,读条完毕后他会生成两个文件夹和几个txt文件,如下图所示。第二步,到cmake官网上下载并安装cmake。然后打开bin文件夹下的...

uC-libc uClibc glibc的区别-程序员宅基地

uClinux的通用c库:uC-libc和uClibc的区别概述uClinux通常使用两种c库:uC-libc和uClibc.尽管它们名字近似,但有很大区别.本文是对它们不同点的快速浏览.uC-libc是uClinux的原始c 库,它基于Linux-8086 c库,该c 库是ELKs 工程的一部分,支持m68000结构.uC-libc是一个相当全面的c 库,但它的一些API是非标准的,一些通用