【问题标题】:Do Default Destructors Do Anything?默认析构函数有什么作用吗?
【发布时间】:2013-02-01 08:13:38
【问题描述】:

我想知道默认类析构函数在被调用时是否真的做任何事情。

我一直在研究它,我发现如果我用一个调用它自己的析构函数的函数创建一个类,它根本不会做任何事情(即所有变量保持不变,实例仍然存在并且可用) .

这是否意味着类析构函数可以被认为是所有类都具有的继承虚函数,并且可以重新定义它(删除指针等并清除成员变量)但是如果不重新定义它'什么都不做?

如果是这样,析构函数不能本质上用作“清除所有数据”类型的函数,并通过清除动态内存分配的变量并重新使用它而不是让计算机在堆上找到一块新的内存?

谢谢。

【问题讨论】:

  • 如果调用析构函数手动释放内存,如果对象随后被销毁会发生什么?我不确定您的目标是什么,但它可能更适合简单的成员函数。
  • 这个问题的答案可能会有所帮助:stackoverflow.com/questions/1036019/…
  • 考虑使用内存池或放置 new/delete 而不是这种优化。请记住:“过早的优化是万恶之源”。

标签: c++ destructor


【解决方案1】:
  • 默认构造函数调用所有成员变量的默认构造函数,不包括原始类型(char、int、指针)。
  • 析构函数可以显式调用,但并不意味着对象的析构。如果对象在堆栈上,那么它不可能对它做任何事情。
  • 默认情况下,析构函数不是虚拟的,但如果您打算从类继承,它们确实应该是虚拟的。
  • 如果对象被释放(超出范围、从堆中删除或封闭对象以任何方式销毁),则将调用 desctuctor。

【讨论】:

  • 我所说的“虚拟”是指每个类默认都有一个(并且可以调用它),但它什么也不做;我的意思不是继承的意思:)。但这是否意味着类的任何结构或类的成员变量也会调用其析构函数?
  • @Edward 是的。隐含地,总是。如果您确实为您的课程编写了析构函数,您仍然不必调用成员的析构函数。您只需要关心delete-ing 动态分配的对象、关闭打开的文件并释放与您的对象关联的其他资源。如果你没有类似的东西,那么你不需要编写析构函数。
  • 关于继承的观点具有误导性:拥有一个带有非虚拟析构函数的基类是完全安全的。只有当你打算在运行时多态中使用基类的静态类型时,析构函数才应该是虚拟的(换句话说,当你打算通过指针访问具有动态类型“派生类”的实例时 / (非常量)类型“基类”的引用)。
  • @KonradRudolph 如果您开始进行子类化/接口,我认为这很可能。我认为通常在基类中使用虚拟析构函数是一个好习惯。价格没那么重要。在极少数情况下,可以单独推理。
  • @Notinlist 否。在 C++ 中拥有不使用虚拟析构函数的基类是非常普遍的做法。例如,大多数标准库实现在大多数容器类的代码中都使用它。它也被用在无数的习惯用法中,例如策略类、mixin 和 CRTP,在所有这些情况下,没有虚拟析构函数是真正的动机——事实上,一些用例依赖(因为否则该模式的优势是没有实际意义的)。
【解决方案2】:

我一直在研究它,我发现如果我用一个调用它自己的析构函数的函数创建一个类,它根本不会做任何事情(即所有变量保持不变,实例仍然存在并且可用) .

考虑这段代码:

#include <iostream>

struct A
{
    ~A()
    {
        std::cout << "A::~A" << std::endl;
    }
};

struct B
{
    A a;
};

int main()
{
    B b;
    std::cout << "Calling b.~B()" << std::endl;
    b.~B();
    std::cout << "Done" << std::endl;
}

You'll see 调用B 的默认析构函数调用A 的析构函数,因为B 包含A

Calling b.~B()
A::~A
Done
A::~A

只有当b 超出范围时,堆栈才会展开,合成的B::~B() 会被调用,而A::~A() 的内存才会被释放。

【讨论】:

  • 感谢@Kleist,我曾一度简化了我的代码,但忘记更新输出。 ideone.com/8fxvWQ 显示它按说明运行。
【解决方案3】:

除了 Notinlist 的回答:

默认构造函数调用基类的构造函数。

如果是这样,析构函数本质上不能用作“清除所有数据”吗 一种功能,并通过以下方式使代码的某些部分更有效 清除动态内存分配的变量并重新使用它 而不是让计算机在 堆?

您在描述一个内存池。如果您愿意,您的对象可能会获取内存缓冲区并将其返回到您发明的某个池系统/从该池系统返回。但在大多数情况下,分配足够快且频率足够低,以至于人们(不再)这样做了。并不是说它们很少发生,但它们需要发生很多才能注意到性能损失。

【讨论】:

    【解决方案4】:

    手动调用析构函数通常是个坏主意。关于 destructors 的 C++ 常见问题解答部分有很多关于此的有用信息。

    如果您确实想明确地销毁一个对象,您可以使用额外的作用域来安全地调用析构函数(请参阅此常见问题解答entry)。此方法还可以防止您使用已销毁的对象实例。尽管该实例似乎可用,但实际上并非如此。

    如果您的意图是释放某个类的实例所拥有的部分资源,但不是全部,您可以尝试两种方法:

    • 在类上定义一个clear()(或类似的)方法。
    • 确保在调用clear() 后保持类的不变量。

    假设您最初手动调用析构函数的方法有效,或者您选择执行类似于上述clear() 方法的操作,在这两种情况下您以后都可能会遇到问题。

    在 C++ 中一种很好理解且经常实践的资源管理方法是 Resource Acquisition Is Initialization(通常缩写为 RAII,但如果名称混淆,请忽略该名称,这个概念是可以理解的)。有关有用信息,请参阅 this 维基百科文章或此 answer

    这里是 tl;dr 虽然:

    • 资源的生命周期应始终与 一个对象。
    • 对象的生命周期从构造函数完成时开始
    • 对象的生命周期在析构函数完成时结束。

    遵循这个习惯用法通常可以防止 C++ 资源管理问题发生。

    【讨论】:

    • 我仍然不确定为什么明确调用析构函数是一个坏主意。如果析构函数在删除诸如指针之类的变量(例如 if(pointer != nullptr) delete pointer;)之前有保护,那么它肯定不会有害。是否只是为了防止程序员忘记析构函数默认销毁和不销毁的内容(例如,将调用其析构函数的成员变量或继承类)?
    • @Edward 不,这是完全错误的。首先,那个守卫不做任何事情,它没用。其次,即使是有效的守卫也没有保护作用——即使是默认的析构函数也不能多次调用,因为它调用了所有成员变量的析构函数(参见 Notinlist 的答案)。最后,您可以手动调用析构函数,但必须非常小心,因为正如我所说,析构函数不允许被重复调用,所以你必须防止任何后续的自动调用。
    猜你喜欢
    • 2011-06-17
    • 1970-01-01
    • 1970-01-01
    • 2022-01-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-17
    • 2014-05-22
    相关资源
    最近更新 更多