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


117

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

更具体的问题:

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

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

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


3
甚至您的具体问题也太广泛了。“该指针以后被删除”和“给出了要指向的新地址”是完全不同的。搜索更多(其中一些已回答),然后针对找不到的部分分别提出问题。
马修·弗莱申

Answers:


73

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

2
我以为您的最后一个示例声明了一个函数?这是“最烦人的解析”的一个例子。(另一个更琐碎的一点是,我想你的意思new Foo()是大写的“ F”。)
Stuart Golodetz

1
我认为Foo myfoo("foo")不是多数烦恼解析,而是char * foo = "foo"; Foo myfoo(foo);
余弦

这可能是一个愚蠢的问题,但是delete myFoo之前不应该这样Foo *myFoo = new Foo("foo");吗?否则,您将删除新创建的对象,不是吗?
Matheus Rocha

没有myFooFoo *myFoo = new Foo("foo");行。该行创建了一个全新的变量,称为myFoo,覆盖了现有变量。尽管在这种情况下,由于myFoo上述内容已在的范围内,因此已不存在if
David Schwartz

1
@galactikuh“智能指针”的作用类似于指向对象的指针,但也具有使管理该对象的生存期更容易的功能。
David Schwartz

19

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

答案是肯定的。@DavidSchwartz举了一个例子,但这是一个非常不寻常的例子。我将举一个例子说明很多C ++程序员一直使用的东西:(std::vector而且std::deque,尽管使用的不是很多)。

如大多数人所知,std::vector当/如果添加的项目超出其当前分配的容量,将分配更大的内存块。但是,执行此操作时,它具有一块内存,可以容纳比向量中当前更多的对象。

为了解决这个问题,vector幕后工作是通过对象分配原始内存Allocator(除非另有说明,否则它表示使用::operator new)。然后,当您使用(例如)push_back将项添加到时vector,向量在内部使用a placement new在其存储空间的(先前)未使用的部分中创建一个项。

现在,如果/如果您erase从向量中得到一个项目,会发生什么?它不能只使用delete-会释放其整个内存块;它需要销毁该内存中的一个对象,而又不销毁其他任何对象,也不释放它控制的任何内存块(例如,如果您erase从一个向量中减去push_back5个项目,然后立即再增加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;
    }

    // release the buffer:
~circular_buffer() { operator delete(data); }
};

#endif

与标准容器不同,它直接使用operator newoperator delete。实际使用时,您可能确实想使用分配器类,但是目前,它比分配贡献更多的精力(无论如何,IMO)。


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

1
有引用计数(shared_ptr),尽管显然不是针对普通指针的。
Pubby 2012年

1
@Pubby:好点,让我们提倡好的做法。编辑答案。
MSalters 2012年

5

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

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

3)不是。可能会有Deep Magic提出其他建议,但是通常您希望将“ new”关键字与“ delete”关键字匹配,并将所有必要的东西放入析构函数中,以确保其正确清理。如果您不这样做,请确保对析构函数进行注释,并向使用该类的任何人提供特定的说明,说明他们应如何手动清理该对象的资源。


3

为了对问题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;
}

这种事情的目的是使内存分配与对象构造脱钩。


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

  2. 范围 -考虑变量何时对程序不可见{block}如您所指出的,通常在的末尾。

  3. 手动销毁 -切勿尝试。只要让scope和RAII为您做魔术即可。


注意:正如您的链接所述,不建议使用auto_ptr。
tnecniv 2012年

std::auto_ptr在C ++ 11中已弃用,是的。如果OP实际具有C ++ 11,则他std::unique_ptr应用于单个所有者,或std::shared_ptr用于引用计数的多个所有者。
chrisaycock 2012年

“手动销毁-绝不要尝试”。我经常使用编译器无法理解的系统调用将对象指针排队​​到另一个线程。“依赖”作用域/自动/智能指针会导致我的应用程序灾难性地失败,因为在被使用者线程处理之前,调用线程删除了对象。此问题影响范围限制和refCounted对象和接口。只有指针和显式删除才可以。
马丁·詹姆斯

@MartinJames可以发布一个编译器无法理解的系统调用示例吗?您如何实现队列?不是std::queue<std::shared_ptr>?我发现pipe(),如果复制不是太昂贵,则在生产者线程和使用者线程之间会使并发变得容易得多。
chrisaycock 2012年

myObject = new myClass(); PostMessage(aHandle,WM_APP,0,LPPARAM(myObject));
马丁·詹姆斯

1

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


0

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

  1. 如果通过删除了指针delete,则将调用dtor。如果在没有delete先调用的情况下重新分配了指针,则会因为对象仍然存在于内存中而导致内存泄漏。在后一种情况下,不调用dtor。

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

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


1
“如果不先调用delete就重新分配指针,则将导致内存泄漏,因为该对象仍存在于内存中的某个位置。” 不必要。可以通过另一个指针将其删除。
马修·弗莱申

0

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

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


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

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

在链接列表中,如果我们使用std :: list <>,则不必关心解码器或内存泄漏,因为std :: list <>已为我们完成了所有这些操作。在我们自己编写的链表中,我们应该编写描述符并明确删除指针,否则会导致内存泄漏。

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

对不起,我的英语不好!


不能手动调用析构函数不是可以的,但是可以(例如,请参见我的答案中的代码)。真实的是,绝大多数时候您不应该:)
Stuart Golodetz

0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.