谁删除在“新”操作期间分配的内存,在构造函数中有exception?
我真的不敢相信我找不到明确的答案
如何释放在C ++类构造函数抛出exception后分配的内存,在使用new
运算符初始化的情况下。 例如:
class Blah { public: Blah() { throw "oops"; } }; void main() { Blah* b = NULL; try { b = new Blah(); } catch (...) { // What now? } }
当我尝试了这一点, b
在catch块中是NULL(这是有道理的)。
在debugging的时候,我注意到conrol在进入构造函数前进入内存分配例程。
这在MSDN网站上似乎证实了这一点 :
当使用new为C ++类对象分配内存时,将在分配内存后调用该对象的构造函数。
所以,请记住,局部variablesb
从来没有被分配(即在catch块中是NULL),你如何删除分配的内存?
在这个问题上得到一个跨平台的答案也是很好的。 即C ++规范说什么?
澄清:我不是在谈论这个类在c'tor中分配内存然后抛出的情况。 我明白,在这种情况下,决不会被召唤。 我正在讨论用于分配对象的内存(在我的例子中是Blah
)。
你应该参考这里和这里的类似问题。 基本上,如果构造函数抛出一个exception,那么对象本身的内存又会被释放。 虽然,如果在构造函数中声明了其他内存,则在离开构造函数之前,您可以自行释放它。
对于你的问题,世卫组织删除内存的答案是新操作符(由编译器生成)背后的代码。 如果它识别出构造函数的exception,则必须调用类成员的所有析构函数(因为在调用构造函数代码之前,这些构造函数已经成功构造),并释放它们的内存(可能与析构函数一起recursion地执行,最有可能通过调用适当的删除 )以及释放分配给这个类本身的内存。 然后它必须从构造函数向新的调用者重新抛出catchedexception。 当然,可能还有更多的工作要做,但是我不能从我的脑海中拿出所有的细节,因为它们取决于每个编译器的实现。
如果一个对象因为构造函数抛出一个exception而无法完成销毁,那么首先发生的事情(这是作为构造函数特殊处理的一部分发生的)是所有已经构造的成员variables都被销毁 – 如果在初始化器列表中抛出一个exception这意味着只有初始化器完成的元素被销毁。
然后,如果对象被分配了new
的对象,那么相应的释放函数( operator delete
)将被调用,并传递给operator new
。 例如, new (std::nothrow) SomethingThatThrows()
会用operator new (size_of_ob, nothrow)
new (std::nothrow) SomethingThatThrows()
分配内存,尝试构造SomethingThatThrows
,销毁所有成功构造的成员,然后调用operator delete (ptr_to_obj, nothrow)
被传播 – 它不会泄漏内存。
你必须小心的是连续分配几个对象 – 如果后面的一个抛出,以前的将不会被自动释放。 最好的方法是使用智能指针,因为作为本地对象的析构函数将在堆栈展开期间被调用,并且析构函数将正确地释放内存。
如果构造函数抛出为对象分配的内存被自动奇迹般地返回给系统。
注意抛出类的析构函数将不会被调用。
但是任何基类(基础构造函数已经完成)的析构函数也会被调用。
注意:
正如大多数其他人所指出的,成员可能需要一些清理。
已完全初始化的成员将会调用它的析构函数,但是如果您拥有任何您拥有的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). } }
从C ++ 2003标准5.3.4 / 17 – 新:
如果上面描述的对象初始化的任何部分通过抛出exception终止并且可以find合适的释放函数,则释放释放函数以释放正在构造对象的存储器,之后exception继续在上下文中传播的新expression。 如果找不到明确的匹配释放函数,则传播该exception不会导致对象的内存被释放。 [注意:当被调用的分配函数不分配内存时,这是合适的。 否则,很可能导致内存泄漏。 ]
所以可能有也可能不会有泄漏 – 取决于是否可以find适当的释放器(通常情况下,除非操作符new / delete被覆盖)。在有合适的释放器的情况下,编译器负责用于在构造函数抛出时调用它。
请注意,这或多或less与构造函数中获得的资源无关,这是我第一次讨论答案的尝试 – 这是许多常见问题解答,文章和post中讨论的问题。
总而言之,如果你没有对你的其他实体进行任何分配(如你的例子),那么被分配的内存将被自动删除。 然而,任何新的语句(或任何其他直接pipe理内存)需要在构造函数中的catch语句中处理,否则删除该对象而不删除它的后续分配,而我的朋友有泄漏。
引用C ++ FAQ( parashift.com ):
[17.4]如果我的构造函数可能抛出exception,我应该如何处理资源?
对象内的每个数据成员都应该清理自己的混乱。
如果一个构造函数抛出一个exception,那么这个对象的析构函数将不会运行。 如果你的对象已经做了一些需要撤销的事情(比如分配一些内存,打开一个文件或者locking一个信号量),那么这个“需要撤消的东西” 必须被对象内部的数据成员记住。
例如,不是将内存分配给一个原始的
Fred*
数据成员,而是将分配的内存放入一个“智能指针”成员对象中,当智能指针死亡时,这个智能指针的析构函数将delete
Fred
对象。 模板std::auto_ptr
就是一个例子,比如“智能指针”。 您也可以编写自己的引用计数智能指针 。 您还可以使用智能指针来“指向”其他机器上的磁盘logging或对象 。顺便说一句,如果你认为你的
Fred
类将被分配到一个智能指针,对你的用户很好,并在你的Fred
类中创build一个typedef
:#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 ... }
所描述的问题与罗马之路一样古老,用荷兰语说。 我已经解决了这个问题,一个可能抛出exception的对象的内存分配如下所示:
try { std::string *l_string = (_heap_cleanup_tpl<std::string>(&l_string), new std::string(0xf0000000, ' ')); delete l_string; } catch(std::exception &) { }
在实际调用new-operator之前,会创build一个无名(临时)对象,它通过用户定义的新运算符(请参阅本答案的其余部分)来接收分配内存的地址。 在正常程序执行的情况下,临时对象将新运算符(新创build的和完全构造的对象,在我们的例子中是非常非常长的string)的结果传递给variablesl_string
。 在发生exception的情况下,值不会被传递,但是临时对象的析构函数会删除内存(没有正确调用主对象的析构函数)。
这是处理这个问题有点模糊的方式,但它的工作原理。 可能会出现问题,因为这个解决scheme需要一个用户定义的新操作符和一个用户定义的删除操作符来进行连接。 用户自定义的new / delete操作符将不得不调用C ++标准库的new / delete操作符的实现,但是我已经放弃了简洁性,而是依赖于malloc()
和free()
。
这不是最终的答案,但我认为这是值得的。
PS:在下面的代码中有一个“无证”的function,所以我做了一个改进。
临时对象的代码如下所示:
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) { } };
用户定义的新运算符如下所示:
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); }
我认为这是一个奇怪的构造函数引发exception。 你可以有一个返回值,并在你的主要testing?
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; }