C++析构函数,何时何地使用?

3

我总是看论坛上关于析构函数的帖子,但却完全感到困惑。

有些人说要调用析构函数(使用delete每次调用new都需要。有些人说在各种情况下自动调用析构函数,例如指针被重新分配、对象超出作用域等。有些人建议当指针作为返回值超出作用域时,该对象存在于其原始副本中,(那么是否需要显式销毁它,因为它最初是由new创建的?

一些建议认为多次调用同一个析构函数会破坏内存,因此所有的delete调用都应该与*pointer = NULL;配对以避免破坏。如果没有,那么就需要实现更高级的对象管理系统,或者实行严格的所有权规定。

我似乎无法理解有关析构函数调用顺序的讨论,即1)是否起源于基类并向下级特定类级联调用所有虚拟析构函数,2)是否起源于实例化类并向上移动到超类,或3)是否起源于类在作用域结束时所具有的特定转换,然后沿着向实例化和基类走。级联析构函数是否有效? 最终,我根本不知道严格何时以及如何删除对象,是否对象负责删除其引用的所有对象,如何清洁地处理适当的面向对象删除程序,其中一个对象被多次引用,这只是我头脑中的一团糟。正如您所看到的,我真的无法提出一个单一的问题,希望有人能提供一个干净简明的讨论,如果不是单一的“正确”方法,至少是对象删除的行业最佳实践。

我建议你直接阅读一本教材。如果你试图从论坛帖子中理解这个问题,你肯定会感到困惑。 - David Heffernan
每次使用new(未重载)时,下一步需要使用delete来释放您分配的空间。 - UnknownError1337
3
如果pointer是一个普通的一级指针(例如,像int* pointer声明的那样),那么在delete之后执行*pointer = NULL无法起到任何好处。实际上,它很可能会使程序崩溃。 - Some programmer dude
3个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
8

有三种分配方式,针对不同的方式,析构函数会以不同的方式调用:

自动分配

这些对象位于自动存储区(通常是堆栈)中:

int main()
{
  A a;
  //...
}

a的析构函数在a超出作用域时(关闭})会自动调用。

动态分配

对象存储在动态内存中(堆)。它们使用new进行分配,为了调用析构函数,您需要调用delete

int main()
{
  A* a = new A;
  delete a;    //destructor called
}
在这种情况下,建议在delete之后将a赋值为NULL。关于此有两种不同的观点(我个人不建议这样做)。动机可能是,如果不将其设置为NULL,则可能会再次调用delete并使程序崩溃。这是正确的。但是,如果您再次调用delete,那么已经存在错误或逻辑上的问题,这些问题不应该通过使代码似乎运行正常来掩盖。

静态分配

对象驻留在static内存中。无论它们在何处分配,当程序结束时,析构函数都会自动调用:

A a; //namespace scope

int main()
{
}

在这里,A的析构函数在程序终止后,即 main 函数执行完毕后被调用。


2
也许你应该处理类成员? - MSalters

3

C++语言将内存管理交给程序员,这就是你会发现有那种程度的混淆的原因。

重复Luchian Grigore所说,主要有三种内存类型:

  • 自动存储(栈)
  • 动态存储(堆)
  • 静态存储

如果你在自动存储中分配了一个对象,那么一旦作用域终止,该对象将被销毁;例如

 void foo() {
     MyClass myclass_instance;
     myclass_instance.doSomething();
 }
在上述情况下,当函数终止时,myclass_instance 会自动销毁。 如果您使用 new 在堆中分配一个对象,则需要使用 delete 调用析构函数。 在C++中,一个对象也可以有子对象。例如:
class MyBiggerClass {
    MyClass x1;
    MyClass x2;
    ...
};

这些子对象分配在与包含对象相同的内存中。

void foo() {
    MyBiggerClass big_instance;
    MyBiggerClass *p = new MyBiggerClass();
    ...
    delete p;
}
在上述情况中,两个子对象big_instance.x1big_instance.x2将被分配在自动存储(堆栈)中,而p->x1p->x2则分配在堆上。 但需要注意的是,在这种情况下,您不需要调用delete p->x1;(编译错误,p->x1不是指针)或delete &(p->x1);(语法上有效,但逻辑上有误,因为它不是显式地在堆上分配的,而是作为另一个对象的子对象)。删除主对象p就足够了。 另一个复杂性是,对象可能会保留对其他对象的指针,而不是直接包含它们:
class MyOtherBigClass {
    MyClass *px1;
    MyClass *px2;
};
在这种情况下,将会是MyOtherBigClass的构造函数需要找到子对象的内存,并且是~MyOtherBigClass需要负责销毁子对象并释放内存。 在C++中,销毁指针本身并不会自动销毁其内容。 在简单情况下,基类可以被视为隐藏的嵌入式子对象。也就是说,就像基对象的实例被嵌入到派生对象中一样。
class MyBaseClass {
    ...
};

class MyDerivedClass : MyBaseClass {
    MyBaseClass __base__;  // <== just for explanation of how it works: the base
                           //     sub-object is already present, you don't
                           //     need to declare it and it's a sub-object that
                           //     has no name. In the C++ standard you can find
                           //     this hidden sub-object referenced quite often.
    ...
};
这意味着派生对象的析构函数不需要调用基对象的析构函数,因为语言会自动处理。虚继承的情况更加复杂,但基类析构函数的调用仍然是自动的。 鉴于内存管理由程序员控制,出现了一些策略来帮助程序员避免在复杂代码中出现对象泄漏或多次销毁的问题。 1. 仔细规划实例的生命周期。您不能将其视为事后想法,否则以后将无法修复。每个对象实例都应清楚地知道是谁创建它,以及谁销毁它。 2. 当无法预先规划对象何时应该销毁时,请使用引用计数器:对于每个对象,跟踪引用它的指针数量,并在此数字达到零时销毁对象。有智能指针可以为您处理此操作。 3. 不要保留对已被销毁的对象的指针。 4. 使用容器。这些容器是专门设计用于处理包含对象的生命周期的类,例如std :: vector或std :: map。

0
如果您的代码调用了new,那么它也应该调用delete。除非您使用智能指针(当指针被销毁时会自动调用delete)。尽可能使用智能指针,并使用vectorstring来避免手动分配内存使用new - 如果您不调用new,则无需担心确保调用delete -> 没有内存泄漏,也没有对象在错误的时间被销毁等问题。 多次调用同一实例的delete绝对是个坏主意。 如果我们有这个:
class A
{
   int *p;
  public:
    A() { p = new int[10]; }
    ~A() { delete [] p; }
};

class B
{
   A a;
   ~B() { ... }
   ... 
};

class C : public B 
{
   ...
   ~C() { ... }
}

...
C *cp = new C;
....
delete cp;

然后通过delete调用C的析构函数。B的析构函数由C的析构函数调用,A的析构函数由B的析构函数调用。这是自动的,编译器会“确保这发生”。

如果我们不调用new:

... 
{
    C c;
    ...
}   // Destructor for C gets called here (and B and A as describe above)

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