谁删除了在构造函数中有异常的“新”操作期间分配的内存?

时间:2009-11-04 16:27:55

标签: c++ exception memory-leaks constructor

我真的不敢相信我找不到明确的答案......

如果使用new运算符初始化C ++类构造函数抛出异常,如何释放分配的内存。 E.g:

class Blah
{
public:
  Blah()
  {
    throw "oops";
  }
};

void main()
{
  Blah* b = NULL;
  try
  {
    b = new Blah();
  }
  catch (...)
  {
    // What now?
  }
}

当我尝试这个时,b在catch块中是NULL(这是有意义的)。

调试时,我注意到conrol在它到达构造函数之前进入了内存分配例程。

这是在MSDN网站上seems to confirm this

  

当new用于分配内存时   对于C ++类对象,对象是   构造函数在内存后调用   分配。

因此,请记住,永远不会分配局部变量b(即在catch块中为NULL),如何删除分配的内存?

在此问题上获得跨平台答案也很不错。即,C ++规范说的是什么?

澄清:我不是在讨论类在c'tor中分配内存然后抛出的情况。我很欣赏在这种情况下,不会召唤人。我在谈论用于分配 THE 对象的内存(在我的情况下为Blah)。

8 个答案:

答案 0 :(得分:37)

您应该参考类似问题herehere。 基本上,如果构造函数抛出异常,则可以安全地再次释放对象本身的内存。虽然,如果在构造函数期间声明了其他内存,那么在离开构造函数之前,您可以自行释放它。

对于您的问题,WHO会删除内存,答案是new-operator背后的代码(由编译器生成)。如果它识别出离开构造函数的异常,它必须调用类成员的所有析构函数(因为那些已经在调用构造函数代码之前已成功构造)并释放它们的内存(可以与析构函数调用一起递归完成,很可能是通过调用正确的 delete 并释放为此类本身分配的内存。然后它必须将构造函数中的catched异常重新抛出到 new 的调用者。 当然可能还有更多的工作需要完成,但我无法从头脑中提取所有细节,因为它们取决于每个编译器的实现。

答案 1 :(得分:26)

如果一个对象因为构造函数抛出异常而无法完成破坏,那么首先发生的事情(这是构造函数特殊处理的一部分)是所有已构造的成员变量都被销毁 - 如果抛出异常初始化列表,这意味着只有初始化程序已完成的元素才会被销毁。

然后,如果对象被分配new,则使用传递给operator delete的相同附加参数调用适当的释放函数(operator new)。例如,new (std::nothrow) SomethingThatThrows()将使用operator new (size_of_ob, nothrow)分配内存,尝试构造SomethingThatThrows,销毁任何成功构建的成员,然后在传播异常时调用operator delete (ptr_to_obj, nothrow) - 它赢了泄漏记忆。

你需要注意的是连续分配几个对象 - 如果其中一个对象抛出,之前的对象将不会被自动释放。最好的方法是使用智能指针,因为作为本地对象,它们的析构函数将在堆栈展开期间被调用,并且它们的析构函数将正确释放内存。

答案 2 :(得分:6)

如果构造函数抛出,则为对象分配的内存会自动神奇地返回给系统。

请注意,不会调用抛出的类的析构函数 但是也会调用任何基类的析构函数(基础构造函数已经完成)。

注意:
正如大多数其他人所指出的那样,会员可能需要一些清理工作。

已完全初始化的成员将调用其析构函数,但如果您拥有任何RAW指针成员(即在析构函数中删除),则必须在执行抛出之前进行一些清理(另一个原因不是在你的班级中使用拥有的RAW指针)。

#include <iostream>

class Base
{
    public:
        Base()  {std::cout << "Create  Base\n";}
        ~Base() {std::cout << "Destroy Base\n";}
};

class Deriv: public Base
{
    public:
        Deriv(int x)    {std::cout << "Create  Deriv\n";if (x > 0) throw int(x);}
        ~Deriv()        {std::cout << "Destroy Deriv\n";}
};

int main()
{
    try
    {
        {
            Deriv       d0(0);  // All constructors/Destructors called.
        }
        {
            Deriv       d1(1);  // Base constructor and destructor called.
                                // Derived constructor called (not destructor)
        }
    }
    catch(...)
    {
        throw;
        // Also note here.
        // If an exception escapes main it is implementation defined
        // whether the stack is unwound. By catching in main() you force
        // the stack to unwind to this point. If you can't handle re-throw
        // so the system exception handling can provide the appropriate
        // error handling (such as user messages).
    }
}

答案 3 :(得分:6)

从C ++ 2003标准5.3.4 / 17 - 新增:

  

如果上述对象初始化的任何部分通过抛出异常终止并且可以找到合适的释放函数,则调用释放函数以释放构造对象的内存,之后异常继续传播在new-expression的上下文中。如果找不到明确的匹配解除分配函数,则传播异常不会导致释放对象的内存。 [注意:当被调用的分配函数不分配内存时,这是合适的;否则,很可能导致内存泄漏。 ]

所以可能有或没有泄漏 - 这取决于是否可以找到合适的解除分配器(通常情况下,除非操作符new / delete被覆盖)。如果有合适的解除分配器,如果构造函数抛出,编译器负责调用它。

请注意,这或多或少与构造函数中获取的资源发生的情况无关,这是我第一次尝试回答的问题 - 并且是许多常见问题解答,文章和帖子中讨论的问题。

答案 4 :(得分:2)

它的长短是因为如果你没有在对象中进行任何其他实体的分配(如在你的例子中那样),那么分配的内存将被自动删除。但是,任何新的语句(或任何其他直接管理内存的语句)都需要在构造函数的catch语句中处理,否则删除对象而不删除它的后续分配,而你,我的朋友,有泄漏。

答案 5 :(得分:-1)

引自C ++ FAQ(parashift.com):

  

[17.4]如果我的构造函数可能抛出,我应该如何处理资源   异常?

     

对象中的每个数据成员都应该清理它自己的混乱。

     

如果构造函数抛出异常,则对象的析构函数不会抛出异常   跑。如果您的对象已经完成了需要撤消的操作   (例如分配一些内存,打开文件或锁定一个   信号量),这个“需要撤消的东西”必须被记住   由对象内的数据成员。

     

例如,而不是将内存分配到原始Fred*数据中   member,将分配的内存放入“智能指针”成员对象中,   并且这个智能指针的析构函数将delete Fred   智能指针死亡时的对象模板std::auto_ptr是一个   例如“智能指针”。你也可以write your own reference counting smart pointer。您还可以use smart pointers to "point" to disk records or objects on other machines

     

顺便说一句,如果您认为将要分配您的Fred课程   进入智能指针,对用户好,并创建typedef   在Fred课程中:

 #include <memory>

 class Fred {
 public:
   typedef std::auto_ptr<Fred> Ptr;
   ...
 };
     

该typedef简化了使用您的所有代码的语法   对象:您的用户可以说Fred::Ptr而不是。{   std::auto_ptr<Fred>

 #include "Fred.h"

 void f(std::auto_ptr<Fred> p);  // explicit but verbose
 void f(Fred::Ptr           p);  // simpler

 void g()
 {
   std::auto_ptr<Fred> p1( new Fred() );  // explicit but verbose
   Fred::Ptr           p2( new Fred() );  // simpler
   ...
 }

答案 6 :(得分:-2)

所描述的问题与通往罗马的道路一样古老,用荷兰语说。我已经解决了问题,并且可能引发异常的对象的内存分配如下所示:

try
{
    std::string *l_string =
        (_heap_cleanup_tpl<std::string>(&l_string),
        new std::string(0xf0000000, ' '));
    delete l_string;
}
catch(std::exception &)
{
}

在实际调用new - 运算符之前,会创建一个无名(临时)对象,该对象通过用户定义的new-operator接收已分配内存的地址(请参阅本答复的其余部分) 。在正常程序执行的情况下,临时对象将new-operator的结果(新创建和完全构造的对象,在我们的例子中是非常非常长的字符串)传递给变量l_string。如果发生异常,则不传递该值,但临时对象的析构函数会删除内存(无需调用主对象的析构函数)。

处理问题有点模糊,但它确实有效。可能会出现问题,因为此解决方案需要用户定义的新运算符和用户定义的删除运算符才能使用它。用户定义的new / delete-operators必须调用C ++标准库的新/删除操作符的实现,但我已将其留作简短性,而是依赖于malloc()free()

这不是最终答案,但我认为值得一试。

PS:下面的代码中有一个“未记录的”功能,所以我做了一些改进。

临时对象的代码如下:

class _heap_cleanup_helper
{
    public:
    _heap_cleanup_helper(void **p_heap_block) :
        m_heap_block(p_heap_block),
        m_previous(m_last),
        m_guard_block(NULL)
    {
        *m_heap_block = NULL;
        m_last = this;
    }
    ~_heap_cleanup_helper()
    {
        if (*m_heap_block == NULL) operator delete(m_guard_block);
        m_last = m_previous;
    }
    void **m_heap_block, *m_guard_block;
    _heap_cleanup_helper *m_previous;
    static _heap_cleanup_helper *m_last;
};

_heap_cleanup_helper *_heap_cleanup_helper::m_last;

template <typename p_alloc_type>
class _heap_cleanup_tpl : public _heap_cleanup_helper
{
    public:
    _heap_cleanup_tpl(p_alloc_type **p_heap_block) :
        _heap_cleanup_helper((void **)p_heap_block)
    {
    }
};

用户定义的new-operator如下:

void *operator new (size_t p_cbytes)
{
    void *l_retval = malloc(p_cbytes);

    if (
        l_retval != NULL &&
        *_heap_cleanup_helper::m_last->m_heap_block == NULL &&
        _heap_cleanup_helper::m_last->m_guard_block == NULL
    )
    {
        _heap_cleanup_helper::m_last->m_guard_block = l_retval;
    }
    if (p_cbytes != 0 && l_retval == NULL) throw std::bad_alloc();

    return l_retval;
}

void operator delete(void *p_buffer)
{
    if (p_buffer != NULL) free(p_buffer);
}

答案 7 :(得分:-6)

我认为构造函数引发异常有点奇怪。 你有一个返回值并在你的主要测试吗?

class Blah
{
   public:

   Blah()
       {
           if Error
           {
              this.Error = "oops";
           }
        }
};

void main()
{
Blah* b = NULL;

b = new Blah();

if (b.Error == "oops")
{
   delete (b);
   b = NULL;
}