C++析构函数在何时被调用?

141

基本问题:在C++中,程序何时调用类的析构函数?有人告诉我,每当一个对象超出其作用域或被delete掉时都会被调用。

更具体的问题:

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

2)关于问题1的跟进,什么定义了一个对象何时超出其作用域(不涉及何时离开给定{块})。换句话说,在链表中什么时候调用对象的析构函数?

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


3
即使是您具体的问题也太宽泛了。“那个指针随后被删除”和“给予一个新地址来指向”是非常不同的。继续搜索(其中一些已经得到答案),然后为您找不到的部分提出单独的问题。 - Matthew Flaschen
@MatthewFlaschen,您能否请给我那些已经回答的问题的链接,我搜索了但找不到它们。 - Abhishek Mane
10个回答

84

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

2
你最后一个示例声明了一个函数?这是“最令人烦恼的解析”的一个例子。(另一个更琐碎的问题是,我想你指的是大写字母'F'的new Foo()。) - Stuart Golodetz
1
我认为 Foo myfoo("foo") 不是最令人烦恼的解析问题,但是 char * foo = "foo"; Foo myfoo(foo); 是。 - Cosine
这可能是一个愚蠢的问题,但是 delete myFoo 不应该在 Foo *myFoo = new Foo("foo"); 之前调用吗?否则你会删除刚刚创建的对象,不是吗? - Matheus Rocha
你可以使用 { 和 } 来开始一个新的作用域,你可以删除 if(1),在许多语言中都是一样的。 - aeroson
1
@galactikuh "智能指针"是一种像指向对象的指针一样工作的东西,但它具有使对象生命周期更容易管理的特性。 - David Schwartz
显示剩余3条评论

27
其他问题已经有人解决了,我只关注一个问题:你是否想手动删除一个对象。
答案是肯定的。@DavidSchwartz给出了一个例子,但这是一个相当不寻常的例子。我会举一个在很多C++程序员经常使用的东西的例子:std::vector(和std::deque,尽管它没有被广泛使用)。
正如大多数人所知道的,当你添加的项超过当前分配的数量时,std::vector将分配一个更大的内存块。然而,当它这样做时,它有一个能够容纳比向量中当前物体更多物体的内存块。
为了管理这个,vector在底层使用Allocator对象(除非你另外指定,否则它使用::operator new)分配原始内存。然后,当你使用(例如)push_back将项添加到vector时,在其内部使用placement new在其内存空间的(以前)未使用部分创建一个项。
现在,如果你从向量中erase一个项目会发生什么?它不能只使用delete——那会释放它的整个内存块;它需要在该内存中销毁一个对象,而不销毁任何其他对象或释放它控制的任何内存块(例如,如果你从向量中erase 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 直接分配和释放内存。实际使用中,您可能确实需要使用一个分配器类,但目前来说,这只会更加分散注意力而非贡献(在我看来)。

感谢您提供的出色答案。 “它不能只使用delete - 那将释放其整个内存块;” - 这是否意味着delete将删除为向量分配的整个数组?这怎么可能,我们只删除向量中的一个元素。 - starriet

10
  1. 使用new创建对象时,您需要负责调用delete。如果使用make_shared创建对象,则生成的shared_ptr将负责计数并在使用计数归零时调用delete
  2. 离开作用域意味着离开一个块。这是在析构函数被调用时发生的,假设该对象没有使用new分配(即它是一个栈对象)。
  3. 唯一需要显式调用析构函数的时候,是当您使用placement new来分配该对象时。

1
有引用计数(shared_ptr),但显然不适用于普通指针。 - Pubby
1
@Pubby:说得好,让我们推广良好的实践。编辑了答案。 - MSalters

6

1) 对象不是通过指针创建的。对于任何你使用 'new' 创建的对象,都会有一个指向它的指针被赋值。假设这就是你的意思,如果你在指针上调用 'delete',它将删除(并调用指针引用的对象的析构函数)。如果你将指针赋给另一个对象,就会产生内存泄漏;在 C++ 中没有任何东西会为你收集垃圾。

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

3) 不完全是。也许有一些特殊的魔法可以表明相反,但通常你想让你的 'new' 关键字与你的 'delete' 关键字匹配,并在你的析构函数中放入所有必要的内容以确保它能够适当地清理自己。如果你不这样做,请确保用具体的说明在注释中告诉任何使用该类的人如何手动清除该对象的资源。


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

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

3
  1. 指针 -- 普通指针不支持RAII。没有显式的delete,会导致内存泄漏。幸运的是,C++有auto pointers可以为您处理这些问题!

  2. 作用域 -- 当变量对程序不可见时,就可以考虑其作用域。通常情况下,在{block}的结尾处即可。

  3. 手动销毁 -- 永远不要尝试手动销毁。只需让作用域和RAII为您完成魔法。


注意:auto_ptr已被弃用,正如您的链接所提到的那样。 - tnecniv
std::auto_ptr在C++11中已经被弃用。如果OP确实使用了C++11,他应该使用std::unique_ptr来处理单独所有权的情况,或者使用std::shared_ptr来处理引用计数的多个所有权的情况。 - chrisaycock
手动销毁 - 永远不要尝试。我经常使用编译器无法理解的系统调用将对象指针排队到不同的线程中。“依赖”于作用域/自动/智能指针会导致我的应用程序在调用线程删除对象之前被消费者线程处理时发生灾难性故障。这个问题影响作用域限定和引用计数对象和接口。只有指针和显式删除才行。 - Martin James
@MartinJames,你能否提供一个编译器无法理解的系统调用的示例?你是如何实现队列的?不是std::queue<std::shared_ptr>吗?我发现在生产者和消费者线程之间使用pipe()可以使并发变得更容易,如果复制不太昂贵的话。 - chrisaycock
myObject = new myClass(); PostMessage(aHandle, WM_APP, 0, LPPARAM(myObject)); - Martin James
显示剩余2条评论

2

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


1
无论何时使用“new”,也就是将地址附加到指针上,或者说,你在堆上申请空间,都需要“delete”它。
1.是的,当你删除某个东西时,析构函数会被调用。
2.当链表的析构函数被调用时,它的对象的析构函数也会被调用。但如果它们是指针,则需要手动删除它们。 3.当空间被“new”声明时。

0
如果对象不是通过指针创建的(例如,A a1 = A();),则析构函数在对象被销毁时调用,总是在包含该对象的函数完成时调用。例如:
void func()
{
...
A a1 = A();
...
}//finish


当代码执行到"finish"行时,析构函数会被调用。

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

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

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

对于我的糟糕英语表示抱歉!


并不是说你不能手动调用析构函数 - 你是可以的(例如,参见我的答案中的代码)。真正正确的做法是,在绝大多数情况下你都不应该这样做 :) - Stuart Golodetz

0

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

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

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

  3. 我怀疑,但如果有一些奇怪的情况,我也不会感到惊讶。


1
如果您在调用delete之前重新分配指针,那么由于对象仍然存在于内存中,您将会出现内存泄漏的情况。但这并非一定如此,因为该对象可能已经通过另一个指针被删除了。 - Matthew Flaschen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接