我在思考:人们说如果你手动调用析构函数,那么你正在做一些错误的事情。但这总是正确的吗?是否存在任何反例?有没有必要手动调用析构函数的情况,或者避免手动调用析构函数很难/不可能/不切实际的情况?
我在思考:人们说如果你手动调用析构函数,那么你正在做一些错误的事情。但这总是正确的吗?是否存在任何反例?有没有必要手动调用析构函数的情况,或者避免手动调用析构函数很难/不可能/不切实际的情况?
所有的答案都描述了具体情况,但是有一个通用的答案:
每当你需要仅仅销毁(在C ++中)对象而不释放内存时,你就显式地调用析构函数。
这通常发生在所有内存分配/释放与对象构建/销毁独立管理的情况下。在这些情况下,通过placement new在现有内存块上进行构造,并通过显式的析构函数调用进行销毁。
以下是原始示例:
{
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::vector
使用时:元素在push_back
期间在vector
中构造,但内存是以块的形式分配的,因此它预先存在于元素构造之前。因此,vector::erase
必须销毁元素,但不一定要释放内存(特别是如果很快会发生新的push_back
...)。
从严格的OOP意义上说,这是"不良设计"(你应该管理对象,而不是内存:事实上对象需要内存是一个"事件"),但在"低级编程"或内存不来自默认的operator new
的自由存储区时,这是"良好的设计"。
如果它在代码中随机出现,那么这是一个不良设计;如果它局部出现在专门设计用于此目的的类中,则这是一个良好的设计。
如果对象是使用重载形式的 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);
在IT技术中,手动释放内存并显式调用析构函数是一种低级的管理内存的方式,这种方式不仅设计不良,而且可能是错误的(是的,在赋值运算符中使用显式析构函数和复制构造函数是一种糟糕的设计,很可能是错误的)。
使用C++ 2011时,有另一个原因需要使用显式析构函数调用:当使用广义联合时,需要显式销毁当前对象并使用placement new创建新对象,以更改所表示对象的类型。此外,在销毁联合时,如果当前对象需要销毁,则需要显式调用当前对象的析构函数。
operator new
的重载形式”,更准确的说法是“使用placement new
”。 - Remy Lebeauoperator new(std::size_t, void*)
(以及数组的变化),而是所有重载版本的 operator new()
。 - Dietmar Kühltemp = Class(object); temp.operation(); object.~Class(); object = Class(temp); temp.~Class();
- Jean-Luc Nacif Coelhooperator=()
一起使用,甚至可能比使用swap更好。但是,具体情况具体分析。 - AdrianT* t = new T;
实际上:1. 分配了sizeof(T)大小的内存。2. 调用T的构造函数对分配的内存进行初始化。operator new执行两个操作:分配和初始化。delete t;
实际上:1. 调用T的析构函数。2. 释放为该对象分配的内存。operator delete也执行两个操作:销毁和释放。不应该显式地调用它,因为这样会调用两次。一次是手动调用,另一次是在声明对象的作用域结束时。
例如:
{
Class c;
c.~Class();
}
如果您确实需要执行相同的操作,应该有一个单独的方法。
有一种特定情况,您可能希望使用放置new
调用动态分配对象的析构函数,但这听起来不像是您永远需要的。
任何时候,如果您需要将分配与初始化分开, 您将需要使用放置new和显式调用析构函数。 今天,由于我们有标准容器,很少需要这样做, 但是如果您必须实现一些新的容器类型,就需要它。
我发现另一个需要手动调用析构函数的例子。假设你已经实现了一个类似于variant的类,它可以保存多种类型的数据:
struct Variant {
union {
std::string str;
int num;
bool b;
};
enum Type { Str, Int, Bool } type;
};
Variant
实例持有一个std::string
,并且现在你要将不同类型分配给联合体,则必须首先销毁std::string
。编译器不会自动执行此操作。有时它们是必要的:
在我的代码中,我在分配器中使用显式析构函数调用,我有一个简单的分配器实现,它使用放置 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);
}
在allocate()中还会进行分配操作,在deallocate()中进行内存释放,使用特定于平台的alloc和dealloc机制。例如,在Windows上使用LocalAlloc函数来绕过Doug Lea malloc并直接使用该分配器。
这个怎么办呢?
如果在构造函数中抛出异常,则析构函数不会被调用,因此我需要手动调用析构函数来销毁在构造函数中创建的句柄,以防止内存泄漏。
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);
}
};
ctor
的方式是错误的,正如你自己提供的原因一样:如果资源分配失败,则清理会出现问题。'ctor'不应调用this->~dtor()
。dtor
应该在已构造的对象上调用,在这种情况下,对象尚未构造。无论发生什么情况,ctor
都应处理清理工作。在ctor
代码内部,你应该使用像std::unique_ptr
这样的工具来处理自动清理,以防发生异常。将类中的HANDLE h1,h2
字段更改为支持自动清理也可能是一个好主意。 - quetzalcoatlMyClass(){ cleanupGuard1<HANDLE> tmp_h1(&SomeAPIToDestroyA) = SomeAPIToCreateA(); cleanupGuard2<HANDLE> tmp_h2(&SomeAPIToDestroyB) = SomeAPIToCreateB(); if(error) { throw MyException(); } this->h1 = tmp_h1.release(); this->h2 = tmp_h2.release(); }
就是这样。没有冒险的手动清理,也没有在部分构造对象中存储句柄直到一切安全的风险,这是一个额外的好处。如果你将类中的 HANDLE h1,h2
更改为 cleanupGuard<HANDLE> h1;
等,则甚至可能根本不需要 dtor
。 - quetzalcoatlcleanupGuard1
和cleanupGuard2
的实现取决于相关的xxxToCreate
返回什么以及相关的xxxxToDestroy
需要哪些参数。如果它们很简单,你甚至不需要编写任何代码,因为通常情况下std::unique_ptr<x,deleter()>
(或类似的)可以在两种情况下为您完成工作。 - quetzalcoatl