手动调用析构函数总是一个糟糕的设计标志?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手动调用析构函数总是一个糟糕的设计标志?相关的知识,希望对你有一定的参考价值。
我在想:他们说如果你手动调用析构函数 - 你做错了什么。但情况总是这样吗?有反例吗?有必要手动调用它或者难以避免的情况/不可能/不切实际的情况吗?
如果使用重载形式的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显式销毁当前对象并创建新对象。此外,当联合被销毁时,如果需要销毁,则必须显式调用当前对象的析构函数。
我发现有3次需要这样做:
- 在memory-mapped-io或共享内存创建的内存中分配/解除分配对象
- 当使用C ++实现给定的C接口时(是的,今天仍然不幸发生这种情况(因为我没有足够的影响力来改变它))
- 实现分配器类时
记忆与其他资源没什么不同:你应该看看http://channel9.msdn.com/Events/GoingNative/GoingNative-2012/Keynote-Bjarne-Stroustrup-Cpp11-Style,特别是Bjarne谈论RAII的部分(约30分钟左右)
所有必需的模板(shared_ptr,unique_ptr,weak_ptr)都是C ++ 11标准库的一部分
找到另一个示例,您必须手动调用析构函数。假设您已经实现了一个类似变体的类,它包含几种类型的数据之一:
struct Variant {
union {
std::string str;
int num;
bool b;
};
enum Type { Str, Int, Bool } type;
};
如果Variant
实例持有std::string
,现在你要为联合分配不同的类型,你必须先破坏std::string
。 The compiler will not do that automatically。
我有另一种情况,我认为调用析构函数是完全合理的。
在编写“重置”类型的方法以将对象恢复到其初始状态时,调用析构函数来删除正在重置的旧数据是完全合理的。
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.
}
};
所有答案都描述了具体案例,但有一般答案:
每次需要销毁对象(在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
使用时的默认std::vector
:在vector
期间,元素在push_back
中构造,但是内存以块的形式分配,因此它预先存在元素构造。因此,vector::erase
必须销毁元素,但不一定要释放内存(特别是如果新的push_back必须很快发生......)。
在严格的OOP意义上它是“糟糕的设计”(你应该管理对象,而不是内存:事实对象需要内存是“事件”),它是“低级编程”中的“好设计”,或者是内存是没有取自“免费商店”默认的operator new
购买。
如果它在代码周围随机发生,这是一个糟糕的设计,如果它本地发生在专门为此目的而设计的类中,那么这是一个很好的设计。
不,取决于情况,有时它是合法和良好的设计。
要了解为什么以及何时需要明确调用析构函数,让我们看看“新”和“删除”发生了什么。
要动态创建一个对象,引擎盖下的T* t = new T;
:1。分配sizeof(T)内存。 2.调用T的构造函数来初始化分配的内存。 operator new执行两项操作:分配和初始化。
在引擎盖下摧毁对象delete t;
:1。调用T的析构函数。 2.释放为该对象分配的内存。 operator delete也做了两件事:破坏和释放。
一个写入构造函数进行初始化,而析构函数进行破坏。显式调用析构函数时,只执行销毁,但不执行解除分配。
因此,显式调用析构函数的合法用法可能是,“我只想破坏对象,但我不(或不能)释放内存分配(但)。”
一个常见的例子是为某些对象池预先分配内存,否则这些对象必须动态分配。
创建新对象时,您将从预先分配的池中获取内存块并执行“placement new”。完成对象后,您可能希望显式调用析构函数来完成清理工作(如果有)。但是你实际上不会释放内存,因为操作符删除会完成。而是将块返回池中以供重用。
由FAQ引用,you should call the destructor explicitly when using placement new。
这是你唯一一次明确调用析构函数的时间。
我同意,但这很少需要。
不,你不应该明确地调用它,因为它会被调用两次。一次用于手动调用,另一次用于声明对象的范围结束。
例如。
{
Class c;
c.~Class();
}
如果您确实需要执行相同的操作,则应该使用单独的方法。
有一个specific situation,你可能想在一个动态分配的对象上调用一个析构函数,它有一个位置new
,但它听起来并不是你需要的东西。
无论何时需要将分配与初始化分开,您都需要手动放置析构函数的新显式和显式调用。今天,它很少有必要,因为我们有标准容器,但如果你必须实现一些新的容器,你将需要它。
有些情况下他们是必要的:
在代码我工作我在分配器中使用显式析构函数调用,我有简单分配器的实现,使用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。
那这个呢? 如果从构造函数抛出异常,则不会调用析构函数,因此我必须手动调用它以销毁在异常之前在构造函数中创建的句柄。
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);
}
};
我从来没有遇到需要手动调用析构函数的情况。我似乎记得甚至Stroustrup声称这是不好的做法。
以上是关于手动调用析构函数总是一个糟糕的设计标志?的主要内容,如果未能解决你的问题,请参考以下文章