手动调用析构函数总是一个糟糕的设计标志?

时间:2013-01-06 21:33:21

标签: c++ coding-style destructor

我在想:他们说如果你手动调用析构函数 - 你做错了什么。但情况总是这样吗?有反例吗?有必要手动调用它或难以/不可能/不切实际地避免它的情况吗?

13 个答案:

答案 0 :(得分:89)

所有答案都描述了具体案例,但有一般答案:

每次只需要销毁对象(在C ++意义上)而不释放对象所在的内存,就可以显式调用dtor。

这通常发生在独立于对象构造/销毁的情况下管理内存分配/解除分配的所有情况。在这些情况下,构造通过 placement new 发生在存在的内存块上,并且通过显式dtor调用进行破坏。

以下是原始示例:

{
  char buffer[sizeof(MyClass)];

  {
     MyClass* p = new(buffer)MyClass;
     p->dosomething();
     p->~MyClass();
  }
  {
     MyClass* p = new(buffer)MyClass;
     p->dosomething();
     p->~MyClass();
  }

}

另一个值得注意的例子是std::allocator使用时的默认std::vectorvector期间元素在push_back中构造,但内存以块的形式分配,所以它预先 - 存在元素构造。因此,vector::erase必须销毁元素,但不一定要释放内存(特别是如果新的push_back必须很快发生......)。

在严格的OOP意义上是“糟糕的设计”(你应该管理对象,而不是内存:事实对象需要内存是“事件”),它是“低级编程”中的“好设计”,或者是如果内存未从“免费商店”中获取,则默认operator new购买。

如果它在代码周围随机发生,这是一个糟糕的设计,如果它本地发生在专门为此目的而设计的类中,那就是好的设计。

答案 1 :(得分:82)

如果使用重载形式的operator new()构造对象,则需要手动调用析构函数,除非使用“std::nothrow”重载:

T* t0 = new(std::nothrow) T();
delete t0; // OK: std::nothrow overload

void* buffer = malloc(sizeof(T));
T* t1 = new(buffer) T();
t1->~T(); // required: delete t1 would be wrong
free(buffer);

外部管理内存的处于相当低的水平,如上所述明确地调用析构函数,然而, 是一个糟糕设计的标志。实际上,它实际上不仅仅是糟糕的设计,而且是完全错误的(是的,使用显式析构函数后跟赋值运算符中的复制构造函数调用是一个糟糕的设计,可能是错误的。)

使用C ++ 2011还有另一个使用显式析构函数调用的原因:使用广义联合时,有必要在更改所表示对象的类型时显式销毁当前对象并使用placement new创建新对象。此外,当联合被销毁时,如果需要销毁,则必须显式调用当前对象的析构函数。

答案 2 :(得分:8)

不,取决于具体情况,有时它是合法的,良好设计。

要了解为什么以及何时需要明确调用析构函数,让我们看一下“new”和“delete”发生了什么。

要动态创建一个对象,T* t = new T;在引擎盖下:1。分配sizeof(T)内存。 2.调用T的构造函数来初始化分配的内存。 operator new执行两项操作:分配和初始化。

要销毁引擎盖下的对象delete t;:1。调用T的析构函数。 2.释放为该对象分配的内存。 operator delete也做了两件事:破坏和释放。

一个写入构造函数进行初始化,而析构函数进行破坏。当您显式调用析构函数时,只会执行销毁,但不是取消分配

因此,显式调用析构函数的合法用法可能是,“我只想破坏对象,但我不(或不能)释放内存分配(还)。”

一个常见的例子是为某些对象池预先分配内存,否则这些对象必须动态分配。

创建新对象时,您可以从预先分配的池中获取内存块并执行“placement new”。完成对象后,您可能希望显式调用析构函数来完成清理工作(如果有)。但是你实际上不会释放内存,因为操作符删除会完成。而是将块返回池中以供重用。

答案 3 :(得分:7)

引用常见问题解答you should call the destructor explicitly when using placement new

  

这是你唯一一次明确调用析构函数的时间。

我同意,但这很少需要。

答案 4 :(得分:6)

不,你不应该明确地调用它,因为它会被调用两次。一次用于手动调用,另一次用于声明对象的范围结束。

例如

{
  Class c;
  c.~Class();
}

如果你真的需要执行相同的操作,你应该有一个单独的方法。

有一个specific situation你可能想要在一个动态分配的对象上调用一个析构函数,其中包含一个展示位置new,但它听起来并不是你需要的东西。

答案 5 :(得分:4)

任何时候你需要将分配与初始化分开, 你需要放置析构函数的新的和显式的调用 手动。今天,它很少有必要,因为我们有 标准容器,但如果你必须实现一些新的排序 容器,你需要它。

答案 6 :(得分:2)

有些情况下他们是必要的:

在我编写的代码中,我在分配器中使用显式析构函数调用,我实现了使用placement new的简单分配器将内存块返回到stl容器。在毁灭中我有:

  void destroy (pointer p) {
    // destroy objects by calling their destructor
    p->~T();
  }

在构造中:

  void construct (pointer p, const T& value) {
    // initialize memory with placement new
    #undef new
    ::new((PVOID)p) T(value);
  }

还使用特定于平台的alloc和dealloc机制在deallocate()中的allocate()和内存释放中进行分配。这个分配器用于绕过doug lea malloc并直接用于Windows上的LocalAlloc。

答案 7 :(得分:2)

这个怎么样?
如果从构造函数中抛出异常,则不会调用析构函数,因此我必须手动调用它以销毁在异常之前在构造函数中创建的句柄。

class MyClass {
  HANDLE h1,h2;
  public:
  MyClass() {
    // handles have to be created first
    h1=SomeAPIToCreateA();
    h2=SomeAPIToCreateB();
    ...
    try {
      if(error) {
        throw MyException();
      }
    }
    catch(...) {
      this->~MyClass();
      throw;
    }
  }
  ~MyClass() {
    SomeAPIToDestroyA(h1);
    SomeAPIToDestroyB(h2);
  }
};

答案 8 :(得分:1)

我从未遇到需要手动调用析构函数的情况。我似乎记得甚至Stroustrup声称这是不好的做法。

答案 9 :(得分:0)

我发现有三次需要这样做:

  • 在memory-mapped-io或共享内存
  • 创建的内存中分配/解除分配对象
  • 在使用C ++实现给定的C接口时(是的,今天仍然不幸发生这种情况(因为我没有足够的影响力来改变它))。
  • 实现分配器类时

答案 10 :(得分:-2)

记忆与其他资源没什么不同:你应该看看http://channel9.msdn.com/Events/GoingNative/GoingNative-2012/Keynote-Bjarne-Stroustrup-Cpp11-Style,尤其是Bjarne谈论RAII的部分(约30分钟左右)

所有必需的模板(shared_ptr,unique_ptr,weak_ptr)都是C ++ 11标准库的一部分

答案 11 :(得分:-2)

找到另一个示例,在该示例中,您将必须手动调用析构函数。假设您实现了一个类似变体的类,其中包含几种数据类型之一:

struct Variant {
    union {
        std::string str;
        int num;
        bool b;
    };
    enum Type { Str, Int, Bool } type;
};

如果Variant实例持有一个std::string,现在您要为联合分配其他类型,则必须首先销毁std::stringThe compiler will not do that automatically

答案 12 :(得分:-3)

我还有另一种情况,我认为调用析构函数是完全合理的。

在编写“重置”类型的方法以将对象恢复到其初始状态时,调用析构函数来删除正在重置的旧数据是完全合理的。

version