130

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

更具体的问题:

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

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

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

4

10 回答 10

79

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");
}
于 2012-04-10T00:15:14.560 回答
23

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

答案是肯定的。@DavidSchwartz 举了一个例子,但这是一个相当不寻常的例子。我将举一个很多 C++ 程序员一直在使用的例子:(std::vector并且std::deque,虽然它的使用并不多)。

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

为了管理它,vector幕后的工作是通过对象分配原始内存Allocator(除非您另外指定,否则意味着它使用::operator new)。然后,当您使用(例如)push_back向 中添加一个项目时vector,向量在内部使用 aplacement new在其内存空间的(以前)未使用的部分中创建一个项目。

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

为此,向量通过显式调用析构函数直接销毁内存中的对象,而不是使用delete.

如果,也许,其他人要编写一个使用连续存储的容器,大致类似于 a 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)。

于 2012-04-10T02:20:31.837 回答
10
  1. 当你用 来创建一个对象时new,你负责调用delete. 当您使用 创建对象时make_shared,结果shared_ptr负责保持计数并delete在使用计数变为零时调用。
  2. 超出范围确实意味着离开一个块。这是调用析构函数的时候,假设对象没有被分配new(即它是一个堆栈对象)。
  3. 唯一需要显式调用析构函数的时候是在为对象分配位置new时。
于 2012-04-10T00:14:33.737 回答
6

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

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

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

于 2012-04-10T00:15:30.137 回答
3
  1. 指针——常规指针不支持 RAII。没有明确的delete,就会有垃圾。幸运的是,C++ 有自动指针可以为您处理这个问题!

  2. 作用域——想想当一个变量对你的程序变得不可见时。{block}正如您所指出的,通常这是在 的末尾。

  3. 手动销毁——切勿尝试这样做。让范围和 RAII 为您创造奇迹。

于 2012-04-10T00:14:05.277 回答
3

详细回答问题 3:是的,在某些情况下(很少见)您可能会显式调用析构函数,特别是作为放置 new 的对应项,正如 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;
}

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

于 2012-04-10T00:36:11.873 回答
1

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

于 2012-04-10T00:17:42.570 回答
1

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

于 2018-10-14T06:38:11.143 回答
0

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

  1. 如果指针被删除,delete则将调用 dtor。如果在没有delete先调用的情况下重新分配指针,则会出现内存泄漏,因为该对象仍然存在于内存中的某个位置。在后一种情况下,不调用 dtor。

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

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

于 2012-04-10T00:18:08.207 回答
0

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

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


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

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

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

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

对不起我的英语不好!

于 2012-04-10T00:33:38.607 回答