【问题标题】:Passing newly allocated data directly to a function将新分配的数据直接传递给函数
【发布时间】:2008-12-29 20:16:02
【问题描述】:

在学习不同的语言时,我经常看到动态分配的对象,最常见的是 Java 和 C#,如下所示:

functionCall(new className(initializers));

我知道这在内存管理语言中是完全合法的,但是这种技术可以在 C++ 中使用而不会导致内存泄漏吗?

【问题讨论】:

    标签: c++ memory-leaks


    【解决方案1】:

    您的代码是有效的(假设 functionCall() 实际上保证指针被删除),但它很脆弱,会让大多数 C++ 程序员的警钟敲响。

    您的代码存在多个问题:

    • 首先,谁拥有指针?谁负责释放它?调用代码无法做到这一点,因为您不存储指针。这意味着被调用的函数必须这样做,但是对于查看该函数的人来说并不清楚。同样,如果我从其他地方调用代码,我当然不希望函数在我传递给它的指针上调用 delete!
    • 如果我们让您的示例稍微复杂一点,它可能会泄漏内存,即使被调用的函数调用了 delete。说它看起来像这样:functionCall(new className(initializers), new className(initializers)); 假设第一个分配成功,但是第二个抛出异常(可能是内存不足,或者类构造函数抛出异常)。 functionCall 永远不会被调用,并且无法释放内存。

    简单(但仍然很混乱)的解决方案是先分配内存,存储指针,然后在声明的同一范围内释放它(因此调用函数拥有内存):

    className* p = new className(initializers);
    functionCall(p);
    delete p;
    

    但这仍然是一团糟。如果 functionCall 抛出异常怎么办?然后 p 不会被删除。除非我们在整个事情上添加一个 try/catch,但是,天哪,这很混乱。 如果函数变得有点复杂,并且可能在 functionCall 之后但在 delete 之前返回怎么办?哎呀,内存泄漏。无法维持。错误的代码。

    所以一个不错的解决方案是使用智能指针:

    boost::shared_ptr<className> p = boost::shared_ptr<className>(new className(initializers));
    functionCall(p);
    

    现在处理内存的所有权。 shared_ptr 拥有内存,并保证它会被释放。当然,我们可以使用 std::auto_ptr 代替,但 shared_ptr 实现了您通常期望的语义。

    请注意,我仍然在单独的行上分配内存,因为在进行函数调用时在同一行上进行多次分配的问题仍然存在。其中一个可能仍然抛出,然后你已经泄漏了内存。

    智能指针通常是您处理内存管理所需的绝对最小值。 但通常,不错的解决方案是编写自己的 RAII 类。

    className 应该在堆栈上分配,并在其构造函数中,使用new 进行必要的分配。在其析构函数中,它应该释放该内存。这样就可以保证不会发生内存泄漏,并且可以让函数调用变得如此简单:

    functionCall(className(initializers));
    

    C++ 标准库是这样工作的。 std::vector 就是一个例子。你永远不会用new 分配一个向量。您在堆栈上分配它,并让它在内部处理其内存分配。

    【讨论】:

    • 避免构造一个空 ptr,并分配它,你不能保证 T p = T(x) 将被编译优化为 T p(x); IE。 boost::shared_ptr p(new className(initializers));在这种特定情况下,您可能还认为 std::auto_ptr 是内存安全的,无需共享。
    【解决方案2】:

    是的,只要你在函数内部释放内存。但这绝不是 C++ 的最佳实践。

    【讨论】:

    • 那么更好的做法是什么?
    • 分配、调用函数、解除分配。需要额外的变量来指向分配的对象。
    • 像往常一样,更好的做法是 RAII。如果您的用户代码中有对 new 可见的调用,则很有可能您做错了。至少将分配包装在智能指针中。我刚刚添加了一个答案来证明这一点。
    • 如果你可以改变函数的代码然后把新的放在那里,并传递初始化程序。尽可能将变量保持在本地。如果你无权访问代码,那么这将导致内存泄漏。创建一个指向新对象的指针,排成一行并将其传入..
    • BTW .. 这在 c# 中也是最好的,函数的函数调用对于调试来说总是一个坏主意。每行一个想法。 :)
    【解决方案3】:

    视情况而定。

    这会将内存的“所有权”传递给 functionCAll()。它要么需要释放对象,要么保存指针,以便以后释放。像这样传递原始指针的所有权是将内存问题构建到代码中的最简单方法之一——泄漏或双重删除。

    【讨论】:

    • 像这样传递所有权通常是一件麻烦事,因为通过查看拥有它的原始指针并不明显。
    【解决方案4】:

    在 C++ 中,我们不会像那样动态创建内存。
    相反,您将创建一个临时堆栈对象。

    如果您希望对象的生命周期大于对函数的调用,则只需要通过 new 创建一个堆对象。在这种情况下,您可以将 new 与智能指针结合使用(有关示例,请参见其他答案)。

    // No need for new or memory management just do this
    functionCall(className(initializers));
    
    // This assumes you can change the functionCall to somthing like this.
    functionCall(className const& param)
    {
        << Do Stuff >>
    }
    

    如果你想传递一个非 const 引用,那么这样做:

    calssName tmp(initializers);
    functionCall(tmp);
    
    functionCall(className& param)
    {
        << Do Stuff >>
    }
    

    【讨论】:

      【解决方案5】:

      如果您调用的函数具有接受所有权语义,则它是安全的。我不记得有什么时候需要这个,所以我认为这很不寻常。

      如果函数以这种方式工作,它应该将其参数作为智能指针对象,以便意图明确;即

      void functionCall(std::auto_ptr<className> ptr);
      

      而不是

      void functionCall(className* ptr);
      

      这使得所有权的转移显式,当函数的执行超出范围时,调用函数将释放 ptr 指向的内存。

      【讨论】:

        【解决方案6】:

        这适用于在堆栈上创建的对象,但不适用于 C++ 中的常规指针。

        一个自动指针也许可以处理它,但我还没有弄乱它们足以知道。

        【讨论】:

        • 常规 auto_ptr 不会(函数调用本身会复制 auto_ptr,这会导致所有权转移),但 const auto_ptr 应该(如果我正确理解 Josuttis 的书的话)会这样做你所期望的。
        【解决方案7】:

        一般来说,不会,除非您想要泄漏内存。事实上,在大多数情况下,这是行不通的,因为

        new T();
        

        在 C++ 中是 T*,而不是 T(在 C# 中,new T() 返回 T)。

        【讨论】:

          【解决方案8】:
          猜你喜欢
          • 2016-02-07
          • 1970-01-01
          • 2018-01-15
          • 1970-01-01
          • 2011-07-05
          • 1970-01-01
          • 1970-01-01
          • 2020-04-09
          • 2021-02-22
          相关资源
          最近更新 更多