关于构造函数抛出异常的讨论

关于构造函数抛出异常的讨论


首先给出结论:

明确的是构造函数可以抛出异常,但是不建议这么做

C++标准规定析构函数不能、也不应该抛出异常那么当无法保证在析构函数中不发生异常时,那就是把异常完全封装在析构函数内部,决不让异常抛出函数之外 [C++11之后析构函数默认noexcept(true)]

构造函数抛出异常

无继承体系情况

  • 以new的方式创建对象

先来看这份代码

#include<iostream>
using namespace std;

class X
{
  public:
      X()
      {
        try{
          cout << "before new: " << p << endl;
          p = new int;
          cout << "after new: " << p << endl;
          cout << "X()" << endl;
          throw 1;
        }
        catch(...){
          cout << "X() catch" << endl;
        }
      }
      ~X()
      {
        cout << "~X()" << endl;
        cout << "delete p: " << p << endl;
        delete p;
      }
  private:
      int *p;
};

int main()
{
  X* ptr;
  cout << "before new X : " << ptr << endl;
  try{
     ptr = new X();
     cout << "after new X: " << ptr << endl;
     delete ptr;
  }
  catch(...)
  {
      cout << "main catch" << endl;
      cout << "delete X*: " << ptr << endl;
      delete ptr;
  }
}

执行结果如下:

在这里插入图片描述

怎么看起来和网上很多构造函数中抛异常之后析构函数不会自动调用的说法不同呢?因为这份代码是new对象,并且异常在构造函数内部捕获不再抛出,所以手动调用delete的时候没有问题(因此使用智能指针包裹对象的时候更没问题)

大部分说法针对的是栈上直接分配对象而不是new对象,而是直接在栈上创建对象,从这个角度来说是正确的,确实会导致内存泄漏的问题

  • 直接在栈上创建对象

来看这份代码

#include<iostream>
using namespace std;

class X
{
  public:
      X()
      {
        try{
          cout << "before new: " << p << endl;
          p = new int;
          cout << "after new: " << p << endl;
          cout << "X()" << endl;
          throw 1;
        }
        catch(...){
          cout << "X() catch" << endl;
          throw 1; //再次抛出(rethrow)
        }
      }
      ~X()
      {
        cout << "~X()" << endl;
        cout << "delete p: " << p << endl;
        delete p;
      }
  private:
      int *p;
};

int main()
{
  try{
     X x;
  }
  catch(...)
  {
      cout << "main catch" << endl;
  }
}

来看结果:

在这里插入图片描述

我们可以看到这种情况确实如网上大部分所说,产生内存泄漏的问题了

有继承体系情况

这里就不分栈上创建和堆上创建了,在上一部分应该可以分清楚区别了

#include<iostream>
using namespace std;

class X
{
  public:
      X()
      {
          p = new int;
      }
      ~X()
      {
          cout << "~X()" << endl;
          delete p;
          p = nullptr;
          cout << p << endl;
      }
  private:
      int *p;
};
class Y: public X
{

  public:
    Y()
    {
         f = new int;
         throw 1;
    }
    ~Y()
    {
         cout << "~Y()" << endl;
         delete f;
    }
  private:
      int *f;
};

int main()
{
  try{
     Y y;
  }
  catch(...)
  {
      cout << "main catch" << endl;
  }
}

来看结果:

在这里插入图片描述

我们构造Y的时候会在Y的初始化列表中先默认构造X,那么当Y对象的生命周期结束的时候,可以调用X的析构函数,处理内存泄漏的情况,对于Y的处理,那么就回到了刚才无继承体系的情况处理了,处理方式下面部分

构造函数抛出异常的处理方法

不管是以new方式进行创建对象还是在栈上创建对象,一般都是需要在rethrow的过程中进行析构函数的处理工作

  • 以new方式创建对象

比如这份代码(最终会段错误):

#include<iostream>
using namespace std;

class X
{
  public:
      X()
      {
        try{
          cout << "before new: " << p << endl;
          p = new int;
          cout << "after new: " << p << endl;
          cout << "X()" << endl;
          throw 1;
        }
        catch(...){
          cout << "X() catch" << endl;
          throw 1; //再次抛出段错误,因为执行流跳转没有给新对象赋地址
        }
      }
      ~X()
      {
        cout << "~X()" << endl;
        cout << "delete p: " << p << endl;
        delete p;
      }
  private:
      int *p;
};

int main()
{
  X* ptr;
  cout << "before new X : " << ptr << endl;
  try{
     ptr = new X();
     cout << "after new X: " << ptr << endl;
     delete ptr;
  }
  catch(...)
  {
      cout << "main catch" << endl;
      cout << "after new X: " << ptr << endl;
      cout << "delete X*: " << ptr << endl;
      delete ptr;
  }
}

在这里插入图片描述

这里段错误的原因是什么呢?分析一下代码

ptr = new X(); 

其中new的底层会先调用operator new,其底层再调用mallocmalloc底层再调用brk/mmap去申请空间,如果申请空间失败则会抛出std::bad_alloc(),当这些做完之后会进行调用构造函数的过程,而如果我们在18行此时抛出rethrow异常,执行流跳到40行,此时ptr = 新地址这个赋值动作没有发生,那么我们在main中调用delete就会对最开始还是野指针的X进行释放空间操作,这就会导致段错误。

正确处理过程

所以这部分的正确处理是在rethrow中进行delete操作释放资源,同时在main中的catch不能进行delete对象指针的操作

  • 在栈上创建对象

起始处理方法和上面的方法是类似的,统一采用rethrow的方法

#include<iostream>
using namespace std;

class X
{
  public:
      X()
      {
        try{
          cout << "before new: " << p << endl;
          p = new int;
          cout << "after new: " << p << endl;
          cout << "X()" << endl;
          throw 1;
        }
        catch(...){
          cout << "X() catch" << endl;
          delete p;
          p = nullptr;
          cout << "delete p :" << p << endl;
          throw 1; //再次抛出(rethrow)
        }
      }
      ~X()
      {
        cout << "~X()" << endl;
        cout << "delete p: " << p << endl;
        delete p;
      }
  private:
      int *p;
};

int main()
{
  try{
     X x;
  }
  catch(...)
  {
      cout << "main catch" << endl;
  }
}

来看结果:

在这里插入图片描述

这样虽然仍然不会调用析构函数,但是我们自己主动释放了资源,就不会有内存泄漏的问题

小结

总的来说:

  1. 首先构造函数中不推荐进行带有风险的操作,一般只进行简单的初始化工作,真的要进行很多操作最好额外使用start或者init函数

  2. 假如没有继承体系:

    1. 对于栈上分配对象来说,构造函数中如果抛出异常,那么不会编译器在该对象生命周期结束的时候不会调用析构函数,若对象中存在堆上分配的内存,则会导致内存泄漏
    2. 对于堆上分配对象来说,构造函数中如果抛出异常,那么如果在最外层catch部分进行delete对象,会导致delete的地址是未分配内存的野指针地址,产生段错误,因为没有进行赋值就因为异常跳到了新的执行流
  3. 假如有继承体系

    1.那么对于已构造部分,编译器会按照已构造的逆序进行析构,派生类需要进行rethrow来处理内存泄漏

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值