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

Posted

技术标签:

【中文标题】谁删除了在构造函数中有异常的“新”操作期间分配的内存?【英文标题】:Who deletes the memory allocated during a "new" operation which has exception in constructor? 【发布时间】:2010-12-13 02:20:06 【问题描述】:

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

在使用new 运算符初始化的情况下,如何释放 C++ 类构造函数引发异常后分配的内存。例如:

class Blah

public:
  Blah()
  
    throw "oops";
  
;

void main()

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

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

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

这个在 MSDN 网站上seems to confirm this:

当使用new分配内存时 对于 C++ 类对象,该对象的 构造函数在内存之后调用 已分配。

所以,记住局部变量b 从未被分配(即在catch 块中为NULL)如何删除分配的内存?

如果能得到一个跨平台的答案也很好。即,C++ 规范是怎么说的?

澄清:我不是在谈论类在 c'tor 中分​​配内存然后抛出的情况。我很感激在这些情况下不会调用 d'tor。我说的是用于分配 THE 对象的内存(在我的例子中是Blah)。

【问题讨论】:

无论如何你都不应该在构造函数中做繁重的工作。将其留给某种“init”方法。 你很安全。如果构造函数抛出,则对象内存已被释放(请注意成员,因为未调用析构函数)。 相反。该对象应在构造函数中完全初始化,除非由于某种原因您不能使用异常。阅读 Stroustrups“C++ 编程语言”的附录 E 了解更多详情:www2.research.att.com/~bs/3rd_safe.pdf @jldupont:我希望我们可以否决您的评论。构造函数可以让对象处于随时可用的状态,因此我们不必记住调用任何 init 函数。你的建议是,IMO,非常糟糕。 但是,Google 的 C++ 指南更喜欢错误代码而不是异常代码,因此对他们来说这是有道理的,因为他们无法从构造函数报告失败。 【参考方案1】:

您应该参考类似的问题here 和here。 基本上,如果构造函数抛出异常,那么对象本身的内存被再次释放是安全的。虽然,如果在构造函数期间已经占用了其他内存,则您必须自己释放它,然后才能将构造函数排除在异常之外。

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

【讨论】:

内存是如何释放的?例如,我对某人可能使用他们自己的 operator new 的情况特别感兴趣。 你自己实现的操作符 new 和 delete 只需要知道如何分配和释放内存。额外的工作由编译器完成。当你说new Blah() 时,编译器会生成代码来 (1) 调用 new 运算符(它只是分配内存),(2) 调用 c'tor,以及 (3) 如果出现问题,调用 delete 运算符。跨度> @Adrian:只是为了迂腐;-) 如果 (1) 完成并且 (2) 抛出,则调用 (3)。 唷,太好了,我以为我要疯了 :-)。感谢您的回答。 PS:很抱歉提出一个已经涵盖的问题 - 我确实先快速浏览了一下,但看不到这个具体问题。【参考方案2】:

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

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

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

【讨论】:

你的意思是如果一个对象不能完成con结构 ...?【参考方案3】:

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

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

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

已完全初始化的成员将调用其析构函数,但如果您有任何自己拥有的 RAW 指针成员(即在析构函数中删除),则必须在执行 throw 之前进行一些清理(另一个原因不是在你的类中使用拥有的 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).
    

【讨论】:

... Note the destructor of the class that threw will not be called ... - 非常重要的一点。【参考方案4】:

来自 C++ 2003 标准 5.3.4/17 - 新:

如果上述对象初始化的任何部分因抛出异常而终止并且可以找到合适的释放函数,则调用释放函数以释放构造对象的内存,之后异常继续传播在新表达式的上下文中。如果找不到明确匹配的释放函数,则传播异常不会导致对象的内存被释放。 [注意:这适用于被调用的分配函数不分配内存的情况;否则,很可能导致内存泄漏。 ]

因此可能存在泄漏,也可能不存在泄漏 - 这取决于是否可以找到合适的解除分配器(通常情况下,除非操作员 new/delete 已被覆盖)。如果有合适的解除分配器,则如果构造函数抛出,编译器负责连接调用它。

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

【讨论】:

【参考方案5】:

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

【讨论】:

【参考方案6】:

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

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

对象中的每个数据成员都应该清理自己的烂摊子。

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

例如,不是将内存分配到原始Fred* 数据中 成员,将分配的内存放入“智能指针”成员对象中, 这个智能指针的析构函数将deleteFred 智能指针死亡时的对象。模板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&lt;Fred&gt;:

 #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
   ...
 

【讨论】:

这是在谈论对象的内容,而我认为原始发布者明确询问分配用于放置对象的内存。 不回答问题。 这解决了一个相关的问题——不是那个被问到的问题。这告诉您如何编写Blah,以便Blah 可以在抛出异常之前清理它分配的任何内容。问题是谁来清理分配给Blah 本身的内存。【参考方案7】:

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

try

    std::string *l_string =
        (_heap_cleanup_tpl<std::string>(&l_string),
        new std::string(0xf0000000, ' '));
    delete l_string;

catch(std::exception &)


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

处理这个问题的方式有点模糊,但它确实有效。可能会出现问题,因为此解决方案需要一个用户定义的 new-operator 和一个用户定义的 delete-operator 来配合它。用户定义的 new/delete-operators 必须调用 C++ 标准库对 new/delete-operators 的实现,但为了简洁起见,我将其省略了,而是改用 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);

【讨论】:

这充其量是一种特定实现的方式。 如果仅仅丢失一小块内存(例如'sizeof(std::string)')值得担心的话,当然可以考虑。让您的用户在他的计算机中添加一些额外的千兆字节内存! 您似乎在暗示所有这些样板代码对于避免某处内存泄漏是必要的。正如对这个两年前的问题的其他答案所表明的那样,这是没有必要的。或者您正在详细说明编译器可能插入的代码,以防止这些内存泄漏。在这种情况下,这完全太详细了……一个非常简单的 try/catch 块也可以说明这一点。无论哪种方式,这都是一个糟糕的答案。 我的答案是解决避免内存泄漏的唯一答案,而不是避免跨构造函数边界抛出异常。您可能会发现答案很糟糕,但这是解决上述问题的唯一答案。只需为每个“新分配”添加一个简单的语句,问题就解决了。最后两个代码块只需放入一次。真正的事实是,如果你不采取预防措施,c++ 允许内存泄漏以这种方式潜入你的程序中。 来自接受的答案:“基本上,如果构造函数抛出异常,您可以安全地再次释放对象本身的内存。”从下一个投票最高的答案开始:“然后,如果使用 new 分配对象,则使用传递给 operator new 的相同附加参数调用适当的释放函数(运算符 delete)。”从下一个投票最高的答案:“如果构造函数抛出分配给对象的内存会自动神奇地返回给系统。”来自 gcc:ideone.com/IRxHX 你想避免什么内存泄漏?【参考方案8】:

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

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;

【讨论】:

例外是在这种情况下正确的工具。仅当您不能使用异常时才能使用设置标志值。 老实说,我认为这会更糟,因为用户不清楚当构造函数成功时对象仍处于不一致状态。把它留给别人来收拾你的烂摊子不是好的作风。如果您无法从构造函数中获取导致错误的内容,我更喜欢异常方式。 您必须测试的返回值正是我们想要的!这就是我们用普通的旧 C 语言 30 年来所拥有的东西,而我们现在有多少亿被忽略的返回值??? 标准的做法是例外,没有什么奇怪的。像这样的返回值在历史上并不成功。除了例外,我们要么有一个可用的对象,要么没有,所以我们可以在没有我们可能忘记的测试的情况下使用它。

以上是关于谁删除了在构造函数中有异常的“新”操作期间分配的内存?的主要内容,如果未能解决你的问题,请参考以下文章

谁在调用删除?

Google C++ 编程风格指南:类

详解ABAP/4内表结构

赋值操作期间未抛出空指针异常

笔试题

在 JSON 反序列化期间没有为“System.String”类型定义无参数构造函数