【问题标题】:Exception in derived class constructor派生类构造函数中的异常
【发布时间】:2011-11-13 05:57:36
【问题描述】:

我在处理派生类中的构造函数异常时遇到了一些问题。当派生类构造函数抛出错误,但父类已经分配了一些对象。会调用父类的析构函数吗?

例子:

class A
{
  A() { /* Allocate some stuff */ };

  virtual ~A() { /* Deallocate stuff */ };
};

class B : public A
{
  B() 
  { 
    /* Do some allocations */
    ...

    /* Something bad happened here */
    if (somethingBadHappened) 
    {
      /* Deallocate B Stuff */
      ...

      /* Throws the error */
      throw Error; /* Will A destructor be called? I know B destructor won't */
    };

  };

  ~B() { /* Deallocate B Stuff */ };
}

我想知道执行以下操作是否是个好主意:

B()
{ 
  /* Do some allocations */
  ...

  /* Something bad happened here */
  if (somethingBadHappened) 
  {
    /* Deallocate B Stuff */
    this->~B();

    /* Throws the error */
    throw Error; /* Will A destructor be called? I know B destructor won't */
  };
};

如果不是,那么做这些事情的好方法是什么?

【问题讨论】:

  • 您自己尝试过什么吗?您可以将调试消息放入各种构造函数和析构函数中,看看会发生什么。
  • 显式调用析构函数不会释放为对象分配的内存。
  • @Daniel:另外,您不能在构造函数中调用自己的析构函数,因为在那个阶段您还没有活动对象。

标签: c++ exception inheritance constructor destructor


【解决方案1】:

异常将导致堆栈展开到正确捕获异常的点。这意味着在抛出异常之前的范围内创建的任何对象都将被销毁,包括本例中的基类对象。

试试这个:

#include <iostream>

class A
{
public:
  A() { std::cout << "A::A()\n";}
  ~A() {std::cout << "A::~A()\n";}
};

class B : public A
{
public:
   B()
   {
      std::cout << "B::B()\n";
      throw 'c';
   }

   // note: a popular point of confusion -- 
   //   in this example, this object's destructor
   //   WILL NOT BE CALLED!
   ~B()
   {
      std::cout << "B::~B()\n";
   }
};


int main()
{
   try
   {
      B b;
   }

   catch(...)
   {
      std::cout << "Fin\n";
   }
   return 0;
}

输出应该是:(注意 B::~B() 没有被调用)

A::A()
B::B()
A::~A()
Fin

只要您不尝试释放尚未分配的资源,就可以安全地按照您在问题中显示的方式手动调用析构函数。最好将这些资源包装在某种类型的 RAII 容器中(std::auto_ptrboost::shared_ptr 等)以避免调用析构函数的必要性。

Mooing Duck 很好地说明了在构造函数中抛出异常时堆栈展开是如何工作的:

【讨论】:

  • 第一部分的答案很好,这个->~B呢?
  • 好的,谢谢。关于第一个问题,我现在明白了。在第二个中,我认为调用析构函数将是一团糟,因为 A::~() 将被调用两次。但我会将危险的指针初始化为 null 并创建一个函数来进行清理。
  • 这是我制作的图表,显示了构建/投掷/展开的步骤:i.imgur.com/LZXxH.png 如果您愿意,我们可以将其包含在答案中。
【解决方案2】:

一直没有想到这一点,但也许可以考虑在 try/catch 块中创建对象。如果构造函数抛出异常,delete 对象,如果它是使用new 创建的。

try
{
    B* b = new B();
}
catch
{
    delete b;
    //log error
}

如果不使用newb分配内存,则不需要在catch块中调用delete。

确保您的 B 析构函数不会对从未创建的对象调用 delete。我建议在执行任何可能导致异常的操作之前,在构造函数中将所有指向对象的指针设置为 0。这样,如果调用了析构函数,deleteing 他们是安全的。

【讨论】:

  • 如果 B() 抛出,则没有要删除的对象。
  • @Bo: 对象的内存已经被分配了。
  • 不,如果构造函数没有完成,则不会将任何内容分配给b。异常也可能是bad_alloc,然后肯定没有什么可删除的。
【解决方案3】:

您在问题的第二部分中尝试编写干净的构造函数B::B() 的失败尝试突出了在一个类中承担过多责任的设计的尴尬。如果您只使用单一职责组件,您通常可以完全不编写任何显式错误检查,而让异常处理机制以递归方式完成其工作。

考虑一下:

B::B()
{
  try { this->p1 = get_dangerous_pointer(); }
  catch(...) { throw; } // OK

  try { this->p2 = suicidal_function(); }
  catch(...) {
    clean_up(p1);
    throw;
  }

  try { this->p3 = get_monstrous_amounts_of_memory(); }
  catch(...)
  {
    clean_up(p2);
    clean_up(p1);
    throw;
  }
}

如您所见,为一个只有三个不同职责的类编写正确的构造函数是一场维护噩梦。

正确的解决方案是让每个资源都归一个包装类所有,该包装类的唯一职责就是拥有该资源,并且即使面对最异常的异常,也会自动进行清理。

还请注意,在从任何构造函数中调用成员函数时,您必须非常小心。一个对象的生命周期直到构造函数完成后才开始,所以当你在构造函数中时,你正在处理一个“正在构造的对象”——有点像心脏直视手术……对你自己。特别是,你不能调用析构函数,因为你只能销毁完整的对象。

【讨论】:

    【解决方案4】:

    最好的办法是在构造中捕获异常,然后将对象置于会产生错误的状态(例如对象读取文件,在构造函数中打开文件失败,然后读取将不起作用)。

    只要保持对象一致即可。

    【讨论】:

    • 如果可以保持对象一致,则无需抛出异常。但是如果你不能创建对象呢?
    猜你喜欢
    • 1970-01-01
    • 2013-04-05
    • 1970-01-01
    • 2014-02-03
    • 2015-07-18
    • 2016-07-19
    • 2020-02-28
    • 2011-12-29
    • 1970-01-01
    相关资源
    最近更新 更多