我在想:他们说如果你手动调用析构函数 - 你做错了什么。但总是这样吗?有没有反例?需要手动调用它或很难/不可能/不切实际避免它的情况?
12 回答
所有答案都描述了具体情况,但有一个普遍的答案:
每次只需要销毁对象(在 C++ 意义上)而不释放对象所在的内存时,都显式调用 dtor。
这通常发生在内存分配/释放与对象构造/销毁独立管理的所有情况下。在这些情况下,构造是通过在现有内存块上放置 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
by 使用时的默认值std::vector
:元素是在vector
during中构造的push_back
,但内存是按块分配的,因此它预先存在元素构造。因此,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);
然而,像上面显式调用析构函数那样在相当低的级别上管理内存是糟糕设计的标志。可能,它实际上不仅是糟糕的设计,而且是完全错误的(是的,在赋值运算符中使用显式析构函数后跟复制构造函数调用是一个糟糕的设计,很可能是错误的)。
在 C++ 2011 中,使用显式析构函数调用还有另一个原因:使用通用联合时,需要显式销毁当前对象,并在更改表示对象的类型时使用placement new 创建一个新对象。此外,当联合被销毁时,如果需要销毁,则需要显式调用当前对象的析构函数。
不,您不应该显式调用它,因为它会被调用两次。一次用于手动调用,另一次用于声明对象的范围结束。
例如。
{
Class c;
c.~Class();
}
如果你真的需要执行相同的操作,你应该有一个单独的方法。
在特定情况下,您可能希望在具有位置的动态分配的对象上调用析构函数,new
但这听起来并不需要。
不,视情况而定,有时它是合法且良好的设计。
要了解为什么以及何时需要显式调用析构函数,让我们看看“new”和“delete”会发生什么。
要动态创建对象, T* t = new T;
在后台: 1. 分配 sizeof(T) 内存。2.调用T的构造函数初始化分配的内存。运算符 new 做了两件事:分配和初始化。
要在底层销毁对象delete t;
: 1. 调用 T 的析构函数。2. 为该对象分配的内存被释放。操作符 delete 也做了两件事:销毁和释放。
一个是写构造函数来做初始化,而写析构函数来做销毁。当您显式调用析构函数时,只完成了析构,而不是释放。
因此,显式调用析构函数的合法用途可能是,“我只想销毁对象,但我不(或不能)释放内存分配(还)。”
一个常见的例子是为某些对象池预先分配内存,否则这些对象必须动态分配。
创建新对象时,您从预分配的池中获取内存块并执行“新放置”。完成对象后,您可能需要显式调用析构函数来完成清理工作(如果有)。但是您实际上不会像操作员 delete 那样释放内存。相反,您将块返回到池中以供重用。
每当您需要将分配与初始化分开时,您都需要手动放置新的和显式调用析构函数。今天,它很少需要,因为我们有标准容器,但如果你必须实现某种新的容器,你会需要它。
在某些情况下,它们是必要的:
在我处理的代码中,我在分配器中使用显式析构函数调用,我实现了简单的分配器,它使用放置新来将内存块返回到 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 机制。此分配器用于绕过 doug lea malloc 并直接使用例如 Windows 上的 LocalAlloc。
那这个呢?
如果构造函数抛出异常,则不会调用析构函数,因此我必须手动调用它以销毁在异常之前在构造函数中创建的句柄。
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);
}
};
我发现 3 次我需要这样做:
- 在 memory-mapped-io 或共享内存创建的内存中分配/释放对象
- 使用 C++ 实现给定的 C 接口时(是的,不幸的是,今天仍然会发生这种情况(因为我没有足够的影响力来改变它))
- 在实现分配器类时
我从来没有遇到过需要手动调用析构函数的情况。我似乎记得甚至 Stroustrup 声称这是不好的做法。
找到另一个示例,您必须手动调用析构函数。假设您已经实现了一个类似变体的类,该类包含几种类型的数据之一:
struct Variant {
union {
std::string str;
int num;
bool b;
};
enum Type { Str, Int, Bool } type;
};
如果Variant
实例持有 a std::string
,而现在您要为联合分配不同的类型,则必须破坏第std::string
一个。编译器不会自动执行此操作。
我还有另一种情况,我认为调用析构函数是完全合理的。
在编写“重置”类型的方法以将对象恢复到其初始状态时,调用析构函数来删除正在重置的旧数据是完全合理的。
class Widget
{
private:
char* pDataText { NULL };
int idNumber { 0 };
public:
void Setup() { pDataText = new char[100]; }
~Widget() { delete pDataText; }
void Reset()
{
Widget blankWidget;
this->~Widget(); // Manually delete the current object using the dtor
*this = blankObject; // Copy a blank object to the this-object.
}
};