C + + 析构函数什么时候被调用?

基本问题: 在 C + + 中,程序什么时候调用类的析构函数方法?有人告诉我,当一个物体超出作用域或者受到 delete的影响时,它就被称为 delete

更具体的问题:

1)如果这个对象是通过一个指针创建的,而这个指针后来被删除或者被赋予一个新的地址来指向,那么它所指向的对象是否会调用它的析构函数(假设没有其他任何东西指向它) ?

2)跟踪问题1,什么定义对象何时超出范围(不关心对象何时离开给定的{ block })。换句话说,什么时候对链表中的对象调用析构函数?

3)你想过手动调用析构函数吗?

119537 次浏览
  1. 指针 ——常规指针不支持 RAII。如果没有显式的 delete,就会产生垃圾。幸运的是,C + + 有 自动指针为您处理这个问题!

  2. 作用域 ——考虑变量何时成为程序的 隐形的。正如您所指出的,通常是在 {block}的末尾。

  3. 手动破坏 ——永远不要尝试这个。只要让范围和 RAII 为你施展魔法。

  1. 使用 new创建对象时,需要负责调用 delete。使用 make_shared创建对象时,结果 shared_ptr负责保持计数,并在使用计数为零时调用 delete
  2. 超出范围就意味着离开一个街区。这是调用析构函数的时候,假设对象是用 new分配的 没有(即它是一个堆栈对象)。
  3. 大约只有在用 < em > 放置 new 分配对象时才需要显式调用析构函数。

1)如果这个对象是通过一个指针创建的,而这个指针后来被删除或者被赋予一个新的地址来指向,那么它所指向的对象是否会调用它的析构函数(假设没有其他任何东西指向它) ?

这取决于指针的类型。例如,智能指针经常在删除对象时删除它们。普通指针则不然。当指针指向另一个对象时也是如此。一些智能指针会销毁旧对象,或者如果它没有更多的引用就会销毁它。普通的指针没有这样的智能。它们只是保存一个地址,并允许您通过这样做来对它们指向的对象执行操作。

2)跟踪问题1,什么定义对象何时超出范围(不关心对象何时离开给定的{ block })。换句话说,什么时候对链表中的对象调用析构函数?

这取决于链表的实现。典型的集合在被销毁时会销毁所有包含的对象。

因此,指针的链表通常会销毁指针,但不会销毁指向的对象。(这可能是正确的。它们可能是其他指针的引用。)但是,专门设计用于包含指针的链表可能会自行删除对象。

智能指针的链表可以在删除指针时自动删除对象,或者在指针没有更多引用时自动删除对象。一切都取决于你自己。

3)你想过手动调用析构函数吗?

当然。一个例子是,如果您想用同一类型的另一个对象替换一个对象,但不想释放内存,只是为了再次分配内存。你可以就地销毁旧物件,然后就地建造一个新物件。(然而,总的来说,这是个坏主意。)

// pointer is destroyed because it goes out of scope,
// but not the object it pointed to. memory leak
if (1) {
Foo *myfoo = new Foo("foo");
}




// pointer is destroyed because it goes out of scope,
// object it points to is deleted. no memory leak
if(1) {
Foo *myfoo = new Foo("foo");
delete myfoo;
}


// no memory leak, object goes out of scope
if(1) {
Foo myfoo("foo");
}

1)对象不是“通过指针”创建的。有一个指针分配给任何对象,你’新’。假设这是您的意思,如果您在指针上调用“ delete”,它实际上将删除(并调用析构函数)指针解引用的对象。如果您将指针分配给另一个对象,那么将出现内存泄漏; C + + 中没有任何内容会为您收集垃圾。

2)这是两个不同的问题。当声明变量的堆栈帧从堆栈中弹出时,该变量就超出了作用域。通常这是你离开街区的时候。堆中的对象永远不会超出作用域,尽管它们在堆栈上的指针可能会超出作用域。没有什么特别能保证调用链表中对象的析构函数。

3)其实不是。也许深度魔术会建议不同,但通常你想匹配你的“新”关键字与你的“删除”关键字,并把一切都在你的析构函数,以确保它适当地清理自己。如果不这样做,请确保向使用该类的任何人注释析构函数,并说明他们应该如何手动清理该对象的资源。

无论何时使用“ new”,也就是将地址附加到指针,或者声明堆上的空间,都需要“删除”它。
是的,当您删除某些内容时,会调用析构函数。
2.当调用链表的析构函数时,它的对象的析构函数也被调用。但如果它们是指针,则需要手动删除它们。 3. 当空间被“新”占据时。

是的,当一个对象在堆栈上超出作用域时,或者当您对一个指向对象的指针调用 delete时,就会调用析构函数(也称为 dtor)。

  1. 如果通过 delete删除指针,那么 dtor 将被调用。如果重新分配指针而不首先调用 delete,就会出现内存泄漏,因为该对象仍然存在于内存的某个地方。在后一种情况下,不调用 dtor。

  2. 一个好的链表实现会在列表被销毁时调用列表中所有对象的 dtor (因为您要么调用了某个方法来销毁它,要么它超出了作用域本身)。这取决于实现。

  3. 我对此表示怀疑,但如果出现一些奇怪的情况,我也不会感到惊讶。

如果对象不是通过指针创建的(例如,A a1 = A () ;) ,当对象被销毁时,总是在对象所在的函数完成时调用析构函数。例如:

void func()
{
...
A a1 = A();
...
}//finish


当执行代码行“ Finish”时,将调用析构函数。

如果对象是通过指针创建的(例如,A * a2 = new A () ;) ,则在删除指针时调用析构函数(删除 a2;)。如果用户没有显式删除该点,或者在删除该点之前没有给出新地址,则会发生内存泄漏。那是只虫子。

在链表中,如果我们使用 std: : list < > ,我们不需要关心解释器或内存泄漏,因为 std: : list < > 已经为我们完成了所有这些操作。在我们自己编写的链表中,我们应该写一个解析函数,然后显式地删除指针。否则,将导致内存泄漏。

我们很少手动调用析构函数,它是为系统提供的一个函数。

对不起,我的英语不好!

对问题3给出一个详细的答案: 是的,有(罕见的)情况下,你可能会明确地调用析构函数,特别是作为一个位置的对应物,就像 dasblinkenlight 观察到的那样。

举一个具体的例子:

#include <iostream>
#include <new>


struct Foo
{
Foo(int i_) : i(i_) {}
int i;
};


int main()
{
// Allocate a chunk of memory large enough to hold 5 Foo objects.
int n = 5;
char *chunk = static_cast<char*>(::operator new(sizeof(Foo) * n));


// Use placement new to construct Foo instances at the right places in the chunk.
for(int i=0; i<n; ++i)
{
new (chunk + i*sizeof(Foo)) Foo(i);
}


// Output the contents of each Foo instance and use an explicit destructor call to destroy it.
for(int i=0; i<n; ++i)
{
Foo *foo = reinterpret_cast<Foo*>(chunk + i*sizeof(Foo));
std::cout << foo->i << '\n';
foo->~Foo();
}


// Deallocate the original chunk of memory.
::operator delete(chunk);


return 0;
}

这种方法的目的是将内存分配与对象构造解耦。

其他人已经解决了其他问题,所以我只看一点: 您是否曾经想要手动删除一个对象。

答案是肯定的。@ DavidSchwartz 举了一个例子,但这是 很公平中不寻常的一个。我将举一个很多 C + + 程序员一直在使用的例子: std::vector(和 std::deque,尽管使用得不是很多)。

大多数人都知道,当/如果添加的内存项超过其当前分配所能容纳的内存时,std::vector将分配更大的内存块。然而,当它这样做时,它就拥有了一块内存,这块内存能够容纳当前在向量中的 更多对象。

为了实现这一点,vector在幕后所做的就是通过 Allocator对象分配 生的内存(除非您另外指定,否则这意味着它使用 ::operator new)。然后,当您使用(例如) push_backvector添加项时,向量在内部使用 placement new在其内存空间的(以前)未使用部分创建项。

现在,当/如果你从矢量中取一个项目 erase会发生什么?它不能仅仅使用 delete——这将释放它的整个内存块; 它需要在不破坏任何其他内存块的情况下摧毁其中的一个对象,或者释放它控制的任何内存块(例如,如果你从一个向量中取出5个项目,然后立即从 push_back取出5个项目,当你这样做时,向量将 没有重新分配内存,这就是 保证

为此,向量通过使用 delete显式调用析构函数 没有直接销毁内存中的对象。

如果有人使用连续存储编写一个容器,就像 vector那样(或者像 std::deque那样使用连续存储的一些变体) ,那么几乎可以肯定,您会使用相同的技术。

例如,让我们考虑如何为循环环缓冲区编写代码。

#ifndef CBUFFER_H_INC
#define CBUFFER_H_INC


template <class T>
class circular_buffer {
T *data;
unsigned read_pos;
unsigned write_pos;
unsigned in_use;
const unsigned capacity;
public:
circular_buffer(unsigned size) :
data((T *)operator new(size * sizeof(T))),
read_pos(0),
write_pos(0),
in_use(0),
capacity(size)
{}


void push(T const &t) {
// ensure there's room in buffer:
if (in_use == capacity)
pop();


// construct copy of object in-place into buffer
new(&data[write_pos++]) T(t);
// keep pointer in bounds.
write_pos %= capacity;
++in_use;
}


// return oldest object in queue:
T front() {
return data[read_pos];
}


// remove oldest object from queue:
void pop() {
// destroy the object:
data[read_pos++].~T();


// keep pointer in bounds.
read_pos %= capacity;
--in_use;
}
  

~circular_buffer() {
// first destroy any content
while (in_use != 0)
pop();


// then release the buffer.
operator delete(data);
}


};


#endif

与标准容器不同,它直接使用 operator newoperator delete。对于真正的使用,您可能确实想要使用分配器类,但是目前为止,它更多的是分散注意力而不是贡献(无论如何,我是这样认为的)。

请记住,对象的构造函数是在为该对象分配内存之后立即调用的,而析构函数是在释放该对象的内存之前调用的。