当一个对象超出其作用域时,析构函数会被调用吗?

49
例如:
int main() {
    Foo *leedle = new Foo();

    return 0;
}

class Foo {
private:
    somePointer* bar;

public:
    Foo();
    ~Foo();
};

Foo::~Foo() {
    delete bar;
}

析构函数会被编译器隐式调用,否则会出现内存泄漏问题吗?

我对动态内存分配不太熟悉,如果这不是一个可用的测试案例,我很抱歉。


4
不,你需要自己调用“delete leedle”。 - juanchopanza
4
问题标题中的答案是“是”,问题主体中的答案是“否”。您必须手动删除所有您使用 new 创建的对象。如果您在构造函数中使用 new(并且没有抛出异常),则可以在析构函数中调用 delete,它会为您清理内存。 - Rapptz
2
你的代码甚至都不合法。请阅读一本C++书籍——这是一个在SO上多次讨论过的基础问题。 - Lightness Races in Orbit
1
了解一下RAII并使用智能指针(shared_ptr,unique_ptr等)。 - Johny
1
可能是在C++中的对象销毁的重复问题。 - Lightness Races in Orbit
显示剩余3条评论
5个回答

113

是的,自动变量将在封闭的代码块结束时被销毁。但请继续阅读。

您的问题标题询问当变量超出作用域时是否会调用析构函数。您可能想要询问的是:

Foo 在 main() 结束时会调用析构函数吗?

根据您提供的代码,该问题的答案为,因为 Foo 对象具有动态存储期,我们很快就会看到。

请注意这里自动变量的定义:

Foo* leedle = new Foo();

在这里,leedle是将被销毁的自动变量。 leedle只是一个指针。 leedle指向的东西没有自动存储期限,并且不会被销毁。 因此,如果你这样做:

void DoIt()
{
  Foo* leedle = new leedle;
}

你泄漏了通过new leedle分配的内存。


必须使用delete删除任何已经使用new分配的内存:

void DoIt()
{
  Foo* leedle = new leedle;
  delete leedle;
}

使用智能指针可以使这个过程变得更加简单和健壮。在 C++03 中:

void DoIt()
{
  std::auto_ptr <Foo> leedle (new Foo);
}

或者在C++11中:

void DoIt()
{
  std::unique_ptr <Foo> leedle = std::make_unique <Foo> ();
}

智能指针被用作自动变量,就像上面的例子一样。当它们超出范围并被销毁时,在析构函数中它们会自动(使用delete)删除指向的对象。因此,在上述两种情况下,都不会发生内存泄漏。


让我们试着澄清一下这里的一些术语。在C++中,变量拥有存储期。在C++03中,有三种存储期:

1:自动:具有自动存储期的变量将在封闭代码块的末尾被销毁。

考虑以下代码:

void Foo()
{
  bool b = true;
  {
    int n = 42;
  } // LINE 1
  double d = 3.14;
} // LINE 2

在这个例子中,所有变量都具有自动存储期。 bd 将在第2行被销毁。 n 将在第1行被销毁。

2:静态:具有静态存储期的变量将在程序开始之前分配,并在程序结束时被销毁。

3:动态:具有动态存储期的变量将在使用动态内存分配函数(例如new)分配时分配,并在使用动态内存分配函数(例如delete)销毁时销毁。

在我上面的原始示例中:

void DoIt()
{
  Foo* leedle = new leedle;
}

leedle是一个具有自动存储期的变量,将在结束大括号时销毁。 leedle指向的东西具有动态存储期,在上面的代码中不会被销毁。您必须调用delete来释放它。

C++11还添加了第四种存储期:

4: thread:具有线程存储期的变量在线程开始时分配,在线程结束时释放。


6
很好的回答--我发现这个主题通过首先指出[自动]指针将被销毁,然后讨论[动态]间接对象的语义,得到了很有效的解决。在Eric的更正之后,这个答案就完美了。 - Lightness Races in Orbit
@LightnessRacesinOrbit:看一下,我又做了一次编辑。 - John Dibling
更加技术上准确,但我认为你失去了简洁性。 - Lightness Races in Orbit

8
是的,如果一个对象超出作用域范围,析构函数会被调用。但是,如果你只有一个指针在作用域内,因为指针没有特定的析构函数,所以不会间接调用Foo的析构函数,所以在这种情况下析构函数不会被调用。
这个例子是智能指针(如std::unique_ptr和std::shared_ptr)的应用领域。这些是实际的类,与原始指针不同,它们有析构函数,可以(有条件地)调用指向对象的delete。
顺便说一下,Foo的析构函数删除bar,但bar从未被初始化或赋值给指向实际对象的地址,所以delete调用会产生未定义的行为,可能会崩溃。

2
确实会出现内存泄漏。当超出范围的对象(Foo*)的析构函数被调用时,但指向的对象(你分配的Foo)的析构函数却没有被调用。
从技术上讲,由于您在主函数中,这不是内存泄漏,因为在应用程序未终止之前,您可以访问每个分配的变量。在这方面,我引用Alexandrescu(来自Modern C ++,关于单例模式的章节)的话:
“当您分配累积数据并且失去所有引用时,将出现内存泄漏。这里并非如此:没有任何累积,我们持有有关分配的内存的知识直到应用程序结束。此外,所有现代”
当然,这并不意味着您不应该调用delete,因为这将是一种极其糟糕(而且危险)的做法。

1
你可以再详细解释一下。 - Tux
对不起,我在写的时候不小心提交了。 - Stefano Falasca

1
首先需要注意的是代码无法编译;new返回在堆上分配的对象的指针。你需要:
int main() {
    Foo *leedle = new Foo();
    return 0;
}

现在,由于new使用动态存储分配对象而不是自动存储,所以它不会在函数结束时超出作用域。因此,它也不会被删除,导致内存泄漏。

1
请停止使用过时的“堆”与“栈”的术语。 - Lightness Races in Orbit
3
这是一个抽象泄漏问题。该编程语言允许您创建具有自动、静态和动态存储期限的对象。它们最终在物理或虚拟内存中的位置既不重要,也未受语言规范的指定。反复使用“堆栈”和“堆”只会加剧这一噩梦... - Lightness Races in Orbit
是的,@R.MartinhoFernandes...还有线程本地存储! - Lightness Races in Orbit
1
我已经编辑了答案,现在反映了更现代的术语吗? - Joni

-5
在这种情况下,当主函数返回时,程序结束,操作系统将处理释放所有资源。如果是其他函数,你需要使用delete来释放资源。

1
没错,但你不能仅仅依赖操作系统为你完成所有工作。 ;) - Tux
7
教授糟糕的实践给一个显然的新手减分。此外,C++标准并不保证操作系统会回收资源,甚至不保证操作系统存在。 - John Dibling
即使如此,操作系统可能只是简单地通过调用析构函数来回收内存。 - Christian Rau
操作系统保证会释放所有内存,但这与当前问题无关。 - Nick
并非总是如此 - 例如,如果一个在GPU上占用资源的对象被操作系统收回内存,因为操作系统不保证调用任何析构函数,甚至不知道它们的存在,则 vidmem 中将会发生内存泄漏。 - Isaac Woods

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