何时调用C++析构函数

When is a C++ destructor called?

本文关键字:析构函数 C++ 调用 何时      更新时间:2023-10-16

基本问题:程序何时在C++调用类的析构函数方法?有人告诉我,每当一个对象超出范围或受到delete

更具体的问题:

1) 如果对象是通过指针创建的,并且该指针后来被删除或被赋予指向的新地址,那么它所指向的对象是否调用其析构函数(假设没有其他对象指向它)?

2)跟进问题1,什么定义了对象何时超出范围(而不是对象何时离开给定的{块})。那么,换句话说,析构函数何时对链表中的对象调用?

3) 您是否要手动调用析构函数?

1) 如果对象是通过指针创建的,并且该指针后来被删除或被赋予指向的新地址,那么它所指向的对象是否调用其析构函数(假设没有其他对象指向它)?

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

2)跟进问题1,什么定义了对象何时超出范围(而不是对象何时离开给定的{块})。那么,换句话说,析构函数何时对链表中的对象调用?

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

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

智能指针的链接列表可以在删除指针时自动删除对象

,或者在指针没有更多引用时自动删除对象。完全由您来挑选您想要的棋子。

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");
}

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

答案是肯定的。@DavidSchwartz举了一个例子,但这是一个相当不寻常的例子。我将举一个C++程序员一直在使用的例子:std::vector(和std::deque,尽管它没有那么多使用)。

正如大多数人所知,当您添加的项目超过其当前分配可以容纳的项目时,std::vector将分配更大的内存块。但是,当它这样做时,它有一个内存块,能够容纳比当前在矢量中更多的对象。

为了管理它,vector幕下所做的是通过Allocator对象分配原始内存(除非您另有指定,否则这意味着它使用 ::operator new )。然后,当您使用(例如)push_back将项目添加到vector时,矢量在内部使用placement new在其内存空间的(以前)未使用部分创建项目。

现在,当您/如果从矢量中erase一个项目时会发生什么?它不能只使用delete - 这将释放其整个内存块;它需要销毁该内存中的一个对象而不销毁任何其他对象,或者释放它控制的任何内存块(例如,如果您从 vector 中erase 5 个项目,然后立即再push_back 5 个项目,则保证 vector 不会在您这样做时重新分配内存。

为此,向量通过显式调用析构函数直接销毁内存中的对象,而不是使用 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。对于实际使用,您可能确实希望使用分配器类,但目前它更多地分散注意力而不是贡献(无论如何,IMO)。

  1. 当你用 new 创建对象时,你负责调用delete 。当您使用 make_shared 创建对象时,生成的shared_ptr负责在使用计数变为零时保持计数并调用delete
  2. 超出范围确实意味着留下一个障碍。这是调用析构函数的时候,假设对象没有分配new(即它是一个堆栈对象)。
  3. 大约唯一需要显式调用析构函数的时间是使用放置new分配对象时。

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

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

3)不是真的。可能有 Deep Magic 会提出相反的建议,但通常您希望将"新"关键字与"删除"关键字匹配,并将所有内容放入析构函数中,以确保它正确清理自己。如果不这样做,请务必向使用该类的任何人注释析构函数,说明他们应如何手动清理该对象的资源。

  1. 指针 -- 常规指针不支持 RAII。没有明确的delete,就会有垃圾。幸运的是,C++有自动指针为您处理此问题!

  2. 作用域 -- 考虑变量何时对程序不可见。通常这是在{block}末尾,正如您所指出的。

  3. 手动销毁 -- 切勿尝试此操作。让scope和RAII为您创造魔力。

详细回答问题 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;
}

这种事情的目的是将内存分配与对象构造分离。

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

每当您使用"new",即将地址附加到指针上,或者说,您在堆上占用空间时,都需要"删除"它。
1.是的,当您删除某些内容时,将调用析构函数。
2.当链表的析构函数被调用时,它的对象的析构函数被调用。但如果它们是指针,则需要手动删除它们。3.当空间被"新"认领时。

是的,当对象超出范围(如果它在堆栈上)或当您在指向对象的指针上调用delete时,将调用析构函数(也称为 dtor)。

  1. 如果通过delete删除指针,则将调用 dtor。如果在不先调用delete的情况下重新分配指针,则会得到内存泄漏,因为该对象仍存在于内存中的某处。在后一种情况下,不调用 dtor。

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

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

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

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


当代码执行为"完成"行时,将调用析构函数。

如果对象是通过指针创建的(例如,A * a2 = new A();),则在删除指针时调用析构函数(删除 a2;)。如果用户未明确删除该点或在删除该点之前未为其指定新地址,则会发生内存泄漏。这是一个错误。

在链表中,如果我们使用 std::list<>,我们不需要关心 desctructor 或内存泄漏,因为 std::list<> 已经为我们完成了所有这些。在我们自己编写的链表中,我们应该写下desctructor并显式删除指针。否则,会导致内存泄漏。

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

对不起,我的英语不好!