【问题标题】:Class destructor memory handling in C++C++ 中的类析构函数内存处理
【发布时间】:2011-02-19 10:36:20
【问题描述】:

隐式析构函数不会处理哪些潜在的内存泄漏?我知道如果你有任何东西存储在堆上,它不会处理它,如果你有一个文件或数据库的连接,那需要手动处理。还有别的事吗?比如说,像向量这样的非基本数据类型呢?

另外,在显式析构函数中,您是否需要销毁本来会被隐式析构函数销毁的非堆变量,或者它们是自动处理的?

谢谢

【问题讨论】:

  • 在良好的 C++ 代码中,文件和 DB-Connections 应该使用 RAII,并且不需要任何手动关闭。
  • @pmr:如果不是“连接”对象的析构函数,那么谁来关闭数据库连接?

标签: c++ memory-leaks destructor


【解决方案1】:

隐式析构函数不会处理哪些潜在的内存泄漏?我知道如果你有任何东西存储在堆上,它不会处理它,如果你有一个文件或数据库的连接,那需要手动处理。还有别的事吗?比如说,像向量这样的非基本数据类型呢?

简单地说,你是对的。隐式析构函数唯一没有处理的事情是以指针或需要显式释放的其他类型资源的形式分配的内存。

关于向量或任何其他类类型对象;所有类都有一个析构函数,负责释放它们的数据。这种类型的对象的析构函数在超出范围时被调用。所有基本数据类型,如:int, float, double, short, bool 等都以类似的方式发布。

另外,在显式析构函数中,您是否需要销毁本来会被隐式析构函数销毁的非堆变量,或者它们是自动处理的?

答案是它们是自动处理的,实际上你永远不应该尝试显式调用对象的析构函数。

在隐式析构函数中会发生以下情况:

  • 类的每个成员变量都依次调用其析构函数。

在显式析构函数中会发生以下情况:

  • 显式析构函数的主体被执行
  • 类的每个成员变量都依次调用其析构函数。

因此您可以看到显式析构函数与隐式析构函数大致相同,只是您可以采取任何必要的手动干预。

现在作为关于管理内存分配对象的一点建议,您几乎应该始终使用 RAII(资源获取就是初始化)。关键是智能指针,这些指针在超出范围时会被正确删除,就像非堆分配的对象一样。一旦您开始使用它们,所有权就会成为一个问题,但这是另一天的故事。开始使用智能指针的好地方是boost::shared_ptr。 (顺便说一句,如果您还没有加入 boost 并且您编写 c++ 代码,请帮自己一个忙……)

【讨论】:

    【解决方案2】:

    你需要了解关于析构函数的三件事。假设您有一个对象t,属于T 类,包含三个成员。

    class T
    {
        A a;
        B b;
        C c;
    
        // ...
    
    } t;
    
    1. 析构 t 总是意味着按顺序调用以下析构函数:~T()~C()~B()~A()您无法影响这些语义。阻止成员的破坏是不可能的。 无论您手动定义~T() 还是编译器生成它,这种情况总是会发生。

    2. 隐式生成的析构函数总是无操作的。但是,如第 1 点所述,析构函数 ~C()~B()~A() 之后仍会执行。

    3. 标量类型的析构函数,尤其是 C 风格的指针,总是无操作。这就是为什么当你有一个 C 风格的指针成员时你几乎总是需要手动编写析构函数(以及复制构造函数和赋值运算符)的唯一原因——在 ~T() 完成之后,销毁一个 C 风格的指针成员什么都不做,因此对 pointees 的任何清理都必须在 ~T() 中完成。

    我希望这可以为您解决问题。

    【讨论】:

      【解决方案3】:

      我认为这个问题是颠倒的。不要考虑对象销毁做什么:考虑它做了做什么。

      如果一个类只有一个隐式析构函数,那么当它被销毁时,所有非静态数据成员也会被销毁,所有基类子对象也是如此。

      您可以争论这是否是“由隐式析构函数”完成的 - 如果您编写析构函数,这些事情仍然会发生。它是对象销毁过程本身的一部分,而不是析构函数代码的一部分。

      所以,如果你的“文件连接”是 FILE* 类成员,那么默认析构函数不会释放它,因为 FILE* 是 C 的东西,它没有析构函数。销毁 FILE* 什么都不做。如果你的“文件连接”是std::ofstream,那么它确实有一个析构函数,析构函数会尝试刷新并关闭连接。

      Vector 有一个析构函数,可以释放向量的资源。这意味着向量中的元素依次调用其析构函数。

      对于您拥有的每个成员变量,您应该查看相应的文档以了解如何处理该类型的资源。显然,有了经验,你就会开始记住答案。

      如果成员变量是一个指针,那么隐式析构函数不会对它做任何事情。因此,如果它指向堆分配的内存,并且该对象持有指向该内存的唯一指针,那么您需要释放它以避免内存泄漏。

      如果您发现自己在 C++ 中编写了一个需要释放多个东西的析构函数,那么您可能设计的类很糟糕。几乎可以肯定,在课堂上的其他地方存在异常安全错误:可以正确编写代码,但如果您有足够的经验来正确编写代码,那么您就有足够的经验通过不同的设计让自己的生活更轻松;- )。解决方案是编写具有单一职责的类——管理单一资源——并将它们用作成员变量。或者,更好的是,找到管理您正在使用的资源的库类。 shared_ptr 非常灵活。

      “需要你销毁非堆变量”

      C++ 中没有“堆”和“非堆”变量这样的东西。销毁指针不会释放指向的东西,原因很明显,指针类型中的任何内容都不会告诉您它指向的东西是如何分配的,或者谁“拥有”它。

      【讨论】:

        【解决方案4】:

        你已经做出了错误的假设。如果我的类使用std::auto_ptr<Data> 保存堆分配的数据,则隐式 dtor 处理它。不会泄露任何内存。原因是隐式 dtor 与任何其他 dtor 一样,将调用所有成员和基类的 dtor。

        现在,一个好的类设计将所有重要资源作为具有适当 dtor 的成员。因此,dtor 主体不需要做任何事情来防止资源泄漏。当然,该规则有一个例外:资源管理类本身只管理一种资源,因此会在其 dtor 中清理该资源。

        【讨论】:

          【解决方案5】:

          一个类的析构函数会隐式调用所有非静态成员的析构函数,然后是虚拟基类的析构函数,然后是它自己的代码。

          如果您将 STL 容器作为类成员,则无需显式执行任何操作——它们会在析构函数中释放自己的内存。

          【讨论】:

          • 除非 STL 容器包含原始指针;在这种情况下,您可能需要显式销毁指向的对象。
          【解决方案6】:

          隐式析构函数调用所有成员变量的析构函数。

          如果你有一个原始指针,它的析构函数什么也不做。因此,如果您拥有它指向的内存,则必须明确释放它。

          这同样适用于任何其他资源,或者您希望执行的任何其他操作,成员变量的默认析构函数并未暗示。

          仍然会调用成员的默认析构函数,因此向量、auto_ptrs、使用 std 流或其他 C++ 库打开的文件都会被销毁。道德是将需要释放的任何 OS 对象包装在整理自己的 C++ 类中,这样应用程序类就不必担心它。

          【讨论】:

          • 不完全是。析构函数实际上像其他任何东西一样销毁原始指针;但是,它不会破坏指向的对象。
          • 类的析构函数调用原始指针的析构函数。原始指针的析构函数什么都不做。原始指针未被“销毁”;它或它在销毁过程中引用的任何东西都没有发生任何事情。
          【解决方案7】:

          向量等会释放自己,因为它们的析构函数被调用了。

          事实上,你的问题在于任何会导致悬空指针(或句柄或其他)问题的东西,即任何需要手动关闭或取消分配的东西都不会在隐式析构函数中。任何能正确自毁的东西都可以。这就是为什么 RAII 成语如此受欢迎的原因:)

          【讨论】:

            猜你喜欢
            • 2015-08-28
            • 2011-03-31
            • 2018-03-17
            • 2010-11-08
            • 2016-10-25
            • 1970-01-01
            • 1970-01-01
            • 2021-03-10
            • 1970-01-01
            相关资源
            最近更新 更多