【问题标题】:what is the best way of deallocating memory in c++ [closed]在 C++ 中释放内存的最佳方法是什么 [关闭]
【发布时间】:2011-11-01 18:49:04
【问题描述】:

我在 C++ 中使用类和嵌套类。我确实有很多类和嵌套类。我不知道什么时候应该释放内存。是否有软件可以向您显示在何处或何时从类中释放指针?

谁能帮帮我? 谢谢

我想提一下,我在 ubuntu 下使用 c++ 工作。我的文件是 .cpp 和 .h 文件。没有主线。 我正在使用类 *x= 新类()。我没有删除 x 因为我不知道什么时候应该删除它。

【问题讨论】:

  • 这个问题的前提是存在选择,但事实并非如此:每种分配内存的方法都有一种且只有一种正确的释放方法。范围结束时自动变量消失;智能指针管理自己;放置new 需要你手动调用析构函数; new[] 分配给哑指针需要delete[]new* 需要 delete;和(天堂禁止!)malloc/calloc 要求您自己管理freeing。或许sunset想知道分配内存的每种选择的优缺点……

标签: c++ pointers memory-management


【解决方案1】:

释放内存的最佳方法不是手动进行,而是使用RAII/SBRM,其中每个资源自己负责释放它所获得的资源。

使用智能指针。

【讨论】:

  • 智能指针是什么意思?代码已经写好了。正如我所提到的,我有一个 .h 和一个 .cpp 文件。我想创建一个静态库,但在此之前我想解决我的代码中内存泄漏的问题。
  • 除了构造函数之外,我应该在每个类中使用析构函数吗?这意味着使用该库的客户端负责释放内存。
  • @sunset:你还是要更改代码,那么为什么不切换到智能指针和 RAII 呢?它会让你的生活更轻松。
  • @sunset:智能指针是指std::shared_ptrstd::unique_ptr,或者如果你的编译器不支持C++11 boost::shared_ptrboost::scoped_ptr
  • @sunset:我认为您需要阅读我在答案中发布的链接以了解我们的建议。此外,如果您不想更改代码以使用 RAII(尽管我强烈建议您这样做),请使用内存分析和泄漏检测工具(如 valgrind)来检测程序中的内存泄漏并释放它那些通过调用delete 导致指针泄漏的泄漏。
【解决方案2】:

在 C++ 中,分配动态内存时一个非常重要的概念是所有权。当不再需要对象时,所有者负责delete。在将指针作为函数参数或返回值传递时,记录所有权是否转移是至关重要的:

class Foo {
public:
    void setHandler(Handler* handler);
    Bar* getBar();
};

Foo::setHandler() 是否拥有给定处理程序的所有权?如果是这样,Foo 的任务是确保在某个时候删除该对象。否则,调用者仍然是所有者。

同样,Foo::getBar() 的用户应该删除它收到的Bar*,还是Foo 会继续拥有它?

【讨论】:

  • 删除对象是 foo 的任务是什么意思?
  • 还有一个问题:如果在方法 setHandler 中创建指向另一个类的指针怎么办?我应该在方法关闭之前使用删除指针吗?
  • @sunset,这个问题没有简单的答案。这完全取决于您对指针的操作。您在最后一次使用指针后删除它。另外你必须问自己你真的需要创建一个指针吗?既然可以创建对象,为什么还要创建指针?换句话说,为什么不直接使用Class *x = new Class(); 而不是Class x;。这些都不是对或错,这完全取决于您要做什么。您必须考虑您创建的对象的生命周期
【解决方案3】:

知道何时删除指针非常困难。使用智能指针为您完成工作,boost::shared_ptr 是显而易见的起点,

【讨论】:

    【解决方案4】:

    释放内存的最佳方法不是显式释放它,而是使用自动变量。 (在 C++ 中,您不必动态分配所有内容)

    MyClass obj; // it gets deallocated automatically
    

    如果您需要使用动态内存分配(使用new()),那么最好的方法是使用smart pointers(同样是自动对象并负责delete 指向的内存)。

    否则,当您确定已完成 p 时,您应该使用 delete p; 释放它。

    【讨论】:

    • 感谢您的回答。你能告诉我 MyClass obj 和 MyClass *obj = new MyClass(); 有什么区别吗?什么时候应该使用第一种形式,什么时候应该使用第二种?谢谢!!
    • @sunset,您应该始终使用第一种形式。仅当您不确定是否已完成 *obj 时才使用第二种形式 (obj = new MyClass)。如果你知道当你完成obj;只使用自动变量(第一版)
    • +1 这个简单的答案。听上去,OP 的印象是你总是不得不说new。提倡 RAII 和智能指针可能只有在您清楚自动存储和动态存储之间的区别之后才会有所帮助。
    • @sunset:关于你所有的基本问题,你为什么不花点钱买一本好书呢?
    【解决方案5】:

    设计软件系统的你应该知道对象的生命周期,否则事情迟早很可能会出错。这意味着您需要考虑特定对象需要多长时间(谁使用它)。除此之外,您还需要知道谁拥有该对象。这两件事会告诉你如何创建对象

    了解对象的生命周期及其所有权将告诉您是否需要堆栈或堆上的对象。前者,自动变量,您可以这样声明:

    {
        ...
        SomeClass sc; 
        ...
    } // sc will be destroyed here as it goes out of scope
    

    是首选,如果 sc 仅在该范围内需要。

    如果您使用new 在堆上分配对象,您应该有这样做的理由。例如。它们在某些其他对象之间共享,或者 API 要求您传递某个智能指针。

    SomeObject* so = new SomeObject;
    

    像上面这样写意味着你必须自己处理释放,不推荐!使用智能指针这样事情就可以好好清理一下,像这样:

    {
        ...
        boost::scoped_ptr<SomeObject> so(new SomeObject);
        ...
    } // boost::scoped_ptr will automatically handle deallocation here
    

    如果把上面的改成boost::shared_ptr

    {
        AnotherObject another;
        ...
        {
            ...
            boost::shared_ptr<SomeObject> so(new SomeObject);
            another.setObject(so); // sets so in another
            ...
        } // no deallocation of so here because another has a reference to it!!
    } // when another goes out of scope (destroyed) so will be destroyed as well
    

    一个 shared_ptr 可能超出范围,但它持有的对象仍然存在,因为另一个对象具有对它的引用。如果您有很多人使用某些资源并且您希望资源在用户还活着时可用,这会非常方便。

    简而言之,尽可能使用自动变量,并在需要时使用boost::scoped_ptrboost::shared_ptr 等智能指针。

    【讨论】:

      【解决方案6】:

      致 OP:您的 cmets 表示对 C++ 的工作方式有些困惑。有一些重要的事情你需要了解一些事情:

      1. 处理指针本质上是危险的。不正确地使用指针可能会引入未定义的行为,这不仅是您的程序中的错误,而且是编译器可能无法检测到的错误,甚至不会给您警告,并且在运行时不知道您的程序将如何表现:它可能会立即崩溃,但它也可能会继续运行,但会导致程序的另一部分(100% 正确)崩溃或只是表现得与应有的不同。没有自动方法可以确定地检测这些情况,因此良好的设计至关重要。
      2. 指向对象的指针与其指向的对象不同。附带说明一下,如果您来自 Java 之类的语言,那么在 C++ 中,“对象”一词也指内置类型的变量,例如 int、double 等。
      3. 指向 X 类型的指针是一个值类型,它可能指向也可能不指向 X 类型的对象。除了具有特殊值 0 (NULL) 之外,它可能只是指向内存中的随机位置不包含 X 类型的对象。这样的指针称为“悬空指针”。
      4. 访问由指针指向的对象的行为称为解除对指针的引用。取消引用悬空指针或空指针是未定义的行为。此外,虽然您的程序可以检查指针是否为空,但无法检查它是否为悬空指针。程序员有责任以这样一种方式构建程序,使悬空指针永远不会被取消引用。
      5. 在您的一个问题中,您询问了有关创建指针的问题。创建指针绝对不意味着您必须在其上调用delete,因为创建指针与创建指针指向的对象不同。考虑以下代码,它创建了一个指针:SomeType* pointer;
        虽然这确实创建了一个指针,但它是一个悬空指针,如果您尝试在其上调用 delete,您将获得未定义的行为。
      6. 内存分配的问题与指针本身无关,就像对象一样,内存被分配或取消分配。通常,您不会在 C++ 中显式分配或取消分配内存,而是创建和销毁对象,并为它们隐式分配内存。
      7. new 运算符不创建指针,它创建动态对象,为它们分配所需的内存,并返回指向新创建对象的指针。动态分配的对象的生命周期将持续存在,并且它们将继续占用它们占用的内存,直到您在指针上调用 delete,这会破坏动态对象(而不是指针本身)并释放它占用的内存。如果您重复分配动态对象并且在停止使用它们后未能删除它们,则您的内存将被未使用的对象填满:这称为内存泄漏。
      8. 指针不仅可以指向动态分配的对象,还可以指向其他类型的对象:局部变量、结构或类成员、数组元素。正如无法判断指针是否悬空一样,也无法判断它是否指向动态对象。尝试在指向动态对象以外的任何对象的有效指针上调用 delete 会导致未定义行为。
      9. 可能有许多指针指向同一个对象。如果对象被销毁,无论是在动态对象的情况下调用 delete,还是以任何其他方式,所有指向它的指针都将成为悬空指针。
      10. 指针变量或类/结构成员本身就是对象,但它们的生命周期与它们指向的任何对象无关:就像在创建指针时没有创建指向的对象一样,指向的对象 (如果有)在指针被销毁时不会被销毁。这样就可以安全地销毁指向同一对象的多个指针,或销毁悬空指针。

      使用new 创建动态对象并使用delete 销毁它们时的另一个问题是异常安全:在许多情况下,最初看起来像delete 的代码可能不会这样做,因为抛出了异常。例如:

      {
          SomeType *p = new Sometype
          // some code
          delete p;
      }
      

      在这种情况下,如果“某些代码”抛出异常,则永远无法到达 delete p 并且会发生内存泄漏。类似地:

      class A {
          SomeType *p;
          // more members
      public:
          A(): p(new SomeType()) /* more intializers */ { /* body */ }
          ~A() { delete p; }
      };
      

      在这种情况下,如果 A 构造函数在 p 初始化后抛出异常,则析构函数将永远不会被调用,并且会发生内存泄漏。

      鉴于这些陷阱,存在多种设计技术来解决这些问题。

      除非必要,否则不要使用指针和/或动态对象:在许多情况下,您无需动态分配对象即可。在局部变量的情况下,变量的生命周期仅限于函数范围,您可以将对象定义为自动变量,而不是:

      {
          SomeType* p = new SomeType();
          // do something with *p
          delete p;
      }
      

      你会的:

      {
          SomeType var;
          // do something with var
      } // var destroyed here
      

      在第二种情况下,var 将被销毁,并且当它超出范围时,它的内存将被释放 - 即使抛出异常也是如此。

      同样,对于包含其他对象的对象,你可以直接将该对象作为成员包含进来,这样就可以代替:

      class Containing {
          Contained* member;
      };
      

      你可以的

      class Containing {
          Contained member;
      };
      

      在这种情况下,包含成员的内存在包含对象内部,并且在包含对象被释放时将被释放,此外,如果包含成员在包含对象的构造期间被初始化,但包含构造函数随后抛出异常,保证包含的成员将被正确销毁,包括调用它自己的析构函数,即使包含对象的析构函数没有被调用。

      由于其中不涉及指针,因此您不必担心悬空指针或空指针,您还可以获得减少时间和内存开销的额外优势。

      缺点是:数据结构中没有递归(因此对于列表、树等,您需要使用指针),没有多态性(指向 SomeType 的指针可能指向 SomeType 的对象或派生自 SomeType 的类型,自动SomeType 类型的变量只能是该特定类型),并且没有延迟初始化(这对于类数据成员尤其重要,其构造函数参数必须始终在包含的类初始值设定项列表中给出,而指针可以(重新)分配随时。

      在必须使用动态对象的情况下,您需要依靠设计来确保删除动态对象。典型的模式是“所有者”的概念——负责删除动态对象的实体。在这个概念中,每个动态对象都有一个且只有一个“所有者”,它是当前正在执行的函数或另一个持有指向所拥有对象的指针的对象。因此,可能有许多指向一个对象的指针,但只有一个是拥有指针。这适用于许多情况:创建动态对象的函数仅在函数运行时才存在,它将拥有该对象,这意味着函数本身中必须有执行删除的代码。在任何类型的分层数据结构(如树)中,“父”对象都可以拥有其“子”。一些动态数据结构,如链表,可以包装在拥有其动态节点的对象中,并负责创建和删除它们,在此示例中,当一个元素被添加到列表中或从列表中删除时。

      在更复杂的情况下,所有权可以在不同的代码段之间转移。例如,当使用insert 方法添加动态对象时,动态对象的集合可能会承担在其外部创建的对象的所有权,并使用具有get-and-remove 语义(pop、dequeue.. .)。创建动态对象并返回指向它们的指针的函数可以将所有权传递给调用者,等等。

      对于保持动态对象所有权的对象,您将始终希望确保它们在析构函数中被删除,因为如果没有发生这种情况,当所有者被销毁时,所拥有的对象将保持没有所有者,这意味着它将永远不会被删除,实际上成为内存泄漏。拥有者也可以在自己销毁之前删除拥有的对象,这在很多情况下都很有用。一个警告:确保所有者没有留下一个悬空指针,它会认为它指向一个有效的对象并尝试再次销毁。在某些情况下,当您将拥有的对象替换为新的对象时,您会销毁它,在这种情况下,您不会遇到此问题,因为拥有指针现在将指向新的拥有对象,这也是有效的。在其他情况下,您需要将拥有指针设置为 0,以便对象知道它不再拥有该指针下的任何内容(在空指针上调用 delete 也是安全的)。

      在函数体中使用手动deletes 或析构函数来维护所有权有几个问题:首先,这种所有权没有在代码中表示,您必须单独管理它,这很容易出错(因为编译器将并且不能在这里检测到任何错误)。我上面提到的异常问题使情况变得更加复杂:编写代码以便无论在哪里抛出异常都会调用 delete 是很困难的,而且容易出错。

      其他答案中提到的 boost 和标准库中的智能指针有助于以异常安全的方式明确表达所有权语义。 STL 的 auto_ptr 和 boost scoped_ptr 具有上述严格的所有权语义,分别具有和不具有转移所有权的能力。如果您有幸使用当前的标准 C++,unique_ptr 是 auto_ptr 的替代品,它消除了它的一些问题。新标准 STL、boost 和 TR1 的 shared_ptr 具有“共享所有权”语义,其中一个对象有多个“所有者”,并在最后一个所有者放手时被销毁。使用这些智能指针类型中的每一种都有优点和注意事项,您应该在使用它们之前阅读它们。

      【讨论】:

        猜你喜欢
        • 2011-01-12
        • 1970-01-01
        • 2010-10-08
        • 2011-08-29
        • 2019-06-09
        • 2020-03-14
        相关资源
        最近更新 更多