为什么C++默认析构函数不会销毁我的对象?

32

C++规范说明默认析构函数会删除所有非静态成员。然而,我无法实现这一点。

下面是我的代码:

class N {
public:
    ~N() {
        std::cout << "Destroying object of type N";
    }
};

class M {
public:
    M() {
        n = new N;
    }
//  ~M() { //this should happen by default
//      delete n;
//  }
private:
    N* n;
};

接下来这段代码本应该打印指定的消息,但实际上并没有:

M* m = new M();
delete m; //this should invoke the default destructor

4
自动存储之所以优于动态存储,原因在于前者的使用更为方便。只有必须时才使用动态分配。 - GManNickG
2
@GMan:显然同意,但Oszkar仍然需要知道这是如何以及为什么这样工作的。 - John Dibling
@John:而他知道,因此“这就是为什么……” - GManNickG
我绝对同意自动存储(或资源管理对象,如tr1::shared_ptr)是可行的方式,但我就是不明白为什么这个规范不起作用。 - Oszkar
12
n是一个指针。该指针会被默认析构函数销毁。它所指向的内容不会被销毁,这是你的责任。 - nos
13个回答

50
你为什么认为默认情况下指向的对象应该被删除?默认析构函数会销毁指针本身,而不是它所指向的对象。
编辑:我尝试让这个更加清晰。如果你有一个局部指针,当它超出作用域时,你是否期望它所指向的对象被销毁?
{
    Thing* t = new Thing;

    // do some stuff here

    // no "delete t;"
}

t指针被清理了,但它所指向的Thing没有被清理,这就是一个内存泄漏。在你的类中也发生了类似的事情。


2
即使您添加了注释掉的部分,您可能需要在实际查看输出之前刷新流。 - jk.
1
@T.J. Crowder: 但是,毁坏对象不是规范要求的吗?@jk: 为什么我需要刷新流? - Oszkar
3
规范仅要求自动对象被销毁...在这种情况下,指针一旦超出作用域就会被销毁。但是,如果“某些内容”将指针传递给另一个不会超出作用域的对象/函数,那么Thing应该仍然被销毁吗? - Raphaël Saint-Pierre
@Fred Larson等人:或者这里的任何其他人,你们所说的指针被销毁是什么意思?如果你看一下我在这里的例子http://coliru.stacked-crooked.com/a/f7401ad0bd90407e,你会发现一个指向已被销毁的相同对象的指针仍然可以引用对象,为什么会这样?如果它被销毁了,Dummy ptr不应该指向任何东西吗? - Parham
1
@Parham:正确。在指针被销毁后继续使用它是未定义的行为。它可能会正常工作,也可能导致宇宙立即崩溃,或者介于两者之间的任何情况。 - Fred Larson
显示剩余3条评论

16

想象一下这样的情况:

class M {
public:
    M() { }
//  ~M() {        // If this happens by default
//      delete n; // then this will delete an arbitrary pointer!
//  }
private:
    N* n;
};

在C++中,指针需要自行管理内存释放,没有任何自动的机制会帮你删除它们。

默认的析构函数将会销毁所有成员对象。但是在这种情况下成员对象是指针本身而不是指向的实际对象,这可能会让你感到困惑。

然而,如果你使用智能指针而不是简单的原始指针,这种“指针”(实际上是一个类)的销毁可能会触发指向的对象的销毁。


6
因此智能指针存在,以便自动销毁。 - David Thornley
“默认析构函数确实会销毁所有成员对象。但在这种情况下,成员对象本身是一个指针,而不是它所指向的东西。这可能会让你感到困惑。” -> “确实如此。这解释清楚了。谢谢! :)” - Oszkar
@Oszkar:我认为你可以取消接受一个答案,然后接受另一个。看起来这个更合适! - Raphaël Saint-Pierre
@RaphaelSP:我得到了几个合适(而且非常好!)的答案,决定标记第一个,因为很难决定。 - Oszkar

9
默认析构函数会销毁指针。如果想要使用M的默认析构函数删除N,可以使用智能指针。将“N * n;”更改为“auto_ptr<N> n;”,然后n将被销毁。
编辑:如评论所指出,“auto_ptr<>”并非所有情况下都是最好的智能指针,但在这里看起来是合适的。它特别代表拥有权:N在M中存在期间,不再存在。复制或分配“auto_ptr<>”表示所有权的变更,这通常不是您想要的。如果您想从M传递指针,则应传递从“n.get()”获得的“N *”。
更通用的解决方案是“boost::shared_ptr<>”,它将包含在C++0x标准中。它可以在几乎任何原始指针使用的地方使用。它不是最有效的结构,并且存在循环引用问题,但通常是安全的结构。
另一个编辑:回答另一条评论中的问题, 默认析构函数的标准行为是销毁所有数据成员和基类。但是,删除原始指针只会删除指针本身,而不是指向的内容。毕竟,实现无法知道那是否是唯一的指针、重要的指针或其他任何类似的东西。智能指针背后的想法是,删除智能指针至少会导致删除所指向的内容,这通常是期望的行为。

3
在使用auto_ptr之前,你应该确保真正理解了其中的情况。它的复制和赋值语义可能不是你预期的那样。 - Scott Wolchok
@David:我明白我的例子并不是一个明智的设计解决方案。我只是试图弄清楚默认析构函数的标准行为。我会使用tr1::shared_ptr(它已经包含在gcc中)或者只是按值存储对象。对于这个问题没有表述得够具体,抱歉。 - Oszkar
@Oszkar:是的,标准行为是删除元素和基类。很抱歉我没有表述清楚。我会再次编辑。 - David Thornley

6

当指向的对象似乎属于包含的对象时,您为什么要使用指针?只需按值存储对象:

class M
{
    N n;

public:

    M() : n()
    {
    }
};

当然这是正确的方法。我只是在测试默认析构函数的行为。但是我觉得它的行为不正确。编辑:阅读了所有答案后,我发现它的行为是正确的 :) - Oszkar

4

错误的说法是析构函数会 删除 成员。它会调用每个成员(和基类)的析构函数,而对于内置类型(如指针),这意味着什么也不做。

匹配 newdelete 是你的责任(可以手动完成,也可以使用智能指针协助完成)。


3

你的论点听起来很有道理,但对于指针来说并不是这样工作的。

n 实际上正在被销毁,但这意味着 N* 的析构函数被调用了,它并没有销毁 n 指向的任何对象。将 N* 的析构函数看作是一个 int 的析构函数。它删除其值,对于指针也是如此,它删除它所指向的地址,但不需要删除你刚刚删除的地址中存储的任何对象。


是的,解答了我的问题。谢谢! - Oszkar

2
我觉得你可能对间接级别有些困惑。当一个实例被销毁时,每个数据成员确实会随之被销毁。在你的情况下,当一个被销毁并调用 :: ~M()时,它的变量< n> 确实被销毁了。问题在于< n> 是一个 ,所以尽管指针被销毁,但它指向的东西却没有被销毁。 delete 的工作方式并非如此。考虑你的简单语句:
delete n;

上述语句销毁的是指向类型为N的对象的指针n所指向的内容,而不是销毁指针n本身。
这里有一个非常好的理由,即M::~M()不会自动调用delete n;。原因是,所引用的N对象可能在多个M对象之间共享,如果其中一个M被销毁,其余的M将失去它们所指向的N,留下可怕的悬挂指针。C++不会尝试解释你使用指针的意图,它只会按照你告诉它的做法来执行。
简而言之,当M被销毁时,它确实会销毁所有成员,只是这种销毁并没有达到你想要的效果。如果你想要一个指针类型,它可以拥有一个对象,并在指针被销毁时销毁它,请查看std::auto_ptr。

2
默认析构函数长这样:
~M()
{
}

默认析构函数不插入任何与指向的内容有关的代码。如果你有一个指向堆栈变量的n,那么自动插入delete n会导致崩溃。
默认析构函数调用类的每个成员的析构函数(member.~T())。对于指针来说,这是无操作(什么也不做),就像myint.~int()什么也不做一样,但对于具有已定义析构函数的成员类,析构函数会被调用。
下面是另一个例子:
struct MyClass {
public:
    MyClass() { .. } // doesn't matter what this does

    int x;
    int* p;
    std::string s;
    std::vector<int> v;
};

默认析构函数实际上是这样做的:
MyClass::~MyClass()
{
    // Call destructor on member variables in reverse order
    v.~std::vector<int>(); // frees memory
    s.~std::string();      // frees memory
    p.~int*();             // does nothing, no custom destructor
    x.~int();              // does nothing, no custom destructor
}

当然,如果你定义了一个析构函数,在成员变量被销毁之前(显然是这样的,否则它们将无效!),析构函数中的代码会先执行。

1

尽量避免使用指针。它们是最后的手段。

class N {
public:
    ~N() {
        std::cout << "Destroying object of type N";
    }
};

class M {
public:
    M() {
       // n = new N; no need, default constructor by default
    }
//  ~M() { //this should happen by default
//      delete n;
//  }
private:
    N n; // No pointer here
};

然后这样使用它

main(int, char**)
{
    M m;
}

这将显示销毁类型为 N 的对象


指针在 C++ 中具有许多重要用途,熟练掌握其使用非常关键。 - David Thornley

1

我认为你可以从一个非常简单的例子中受益:

int main(int argc, char* argv[])
{
  N* n = new N();
} // n is destructed here

这也不会打印任何东西。

为什么?因为指针(n)被销毁了,而不是指向的对象 *n

当然,您不希望它销毁指向的对象:

int main(int argc, char* argv[])
{
  N myObject;
  {
    N* n = &myObject;
  } // n is destructed here, myObject is not

  myObject.foo();
} // myObject is destructed here

你应该记住,与像 C#Java 这样的语言不同,C++ 中有两种创建对象的方式:直接在堆栈上创建 N myObject,或者通过使用 new 创建,例如 new N(),此时对象被放置在堆上,您需要在稍后释放它。

因此,您的析构函数销毁指针,但不销毁指向的对象。如果要自动化,可以在不使用指针的情况下分配对象,或者使用 智能指针


第一个例子非常清楚地说明了析构函数的工作原理。谢谢。 - Oszkar
第二个例子仍然存在内存泄漏问题,第一个也是。 - Krishna Oza
@krish_oza:第一个确实存在内存泄漏(出于设计考虑,因为它实际上展示了析构函数未执行的缺陷);然而第二个没有(它不分配内存)。 - Matthieu M.

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