【问题标题】:Is Foo* f = new Foo good C++ code是 Foo* f = new Foo 好的 C++ 代码吗
【发布时间】:2011-01-03 15:44:39
【问题描述】:

通过阅读我的旧 C++ 日志,我发现了一些东西。

其中一篇文章断言

Foo *f = new Foo();

基本上是几乎不能接受的专业 C++ 代码,自动内存管理解决方案是合适的。

是这样吗?

编辑:改写:对于新的 C++ 代码,直接内存管理是否不可接受,一般大多数新代码是否应该使用 auto_ptr(或其他管理包装器)?

【问题讨论】:

  • 这样的问题让我觉得我在与其他人不同的星球上工作。以下所有答案都表明 new/delete 已过时、危险、邪恶、已弃用;在我的(大)公司,它基本上是我们用于分配动态对象的唯一机制!我们有容器类,但它们都只是调用下面的 new/delete。
  • 注意:Foo* f = new Foo 完全有效。
  • @Crashworks:他们在下面做什么并不重要。在下面,您的循环或函数调用只是一个 goto,但这并不意味着“goto 可以使用”。下面,每个指针和引用都是通过破解内存地址来实现的,但这并不意味着您可以安全地将指针视为内存地址。这些都被抽象出来了,就像 new/delete 通常在容器类和 RAII 包装器后面抽象出来一样
  • apropos of nothing:我的一个朋友曾经编写了一个编译器,带有警告“警告:过度使用元语法变量 'foo'”(想想看,这可能是错误 B-)
  • 可能值得注意的是,上述问题不是关于仅仅调用new 来分配一个Foo。它是关于使用new 然后将结果存储在原始指针中。由于没有提及其他内容,我假设指针也没有作为 RAII 类中的私有成员隐藏起来,而是实际上被传递并直接在应用程序中使用。而且是一种危险的做法,无论“new是邪恶的”

标签: c++ memory-management memory-leaks


【解决方案1】:

这个例子很像 Java。
在 C++ 中,我们仅在需要时使用动态内存管理。
更好的选择是声明一个局部变量。

{
    Foo    f;

    // use f

} // f goes out of scope and is immediately destroyed here.

如果您必须使用动态内存,请使用智能指针。

// In C++14
{
    std::unique_ptr<Foo>  f = std::make_unique<Foo>(); // no need for new anymore
}

// In C++11
{
    std::unique_ptr<Foo>  f(new Foo);  // See Description below.
}

// In C++03
{
    std::auto_ptr<Foo>    f(new Foo);  // the smart pointer f owns the pointer.
                                       // At some point f may give up ownership to another
                                       // object. If not then f will automatically delete
                                       // the pointer when it goes out of scope..

}

提供了一大堆 os 智能指针 int std:: 和 boost::(现在有些在 std::tr1 中)选择合适的指针并使用它来管理对象的生命周期。

Smart Pointers: Or who owns you baby?

从技术上讲,您可以使用 new/delete 进行内存管理。
但在真正的 C++ 代码中,它几乎从未完成。几乎总是有比手动进行内存管理更好的选择。

一个简单的例子是 std::vector。在幕后,它使用 new 和 delete。但你永远无法从外面分辨出来。这对类的用户是完全透明的。用户所知道的是,向量将取得对象的所有权,并且当向量被销毁时,它将被销毁。

【讨论】:

    【解决方案2】:

    我前段时间停止编写此类代码。有几种选择:

    基于范围的删除

    {
        Foo foo;
        // done with foo, release
    }
    

    scoped_ptr 用于基于范围的动态分配

    {
        scoped_ptr<Foo> foo( new Foo() );
        // done with foo, release
    }
    

    shared_ptr 用于应该在许多地方处理的事情

    shared_ptr<Foo> foo;
    { 
        foo.reset( new Foo() );
    } 
    // still alive
    shared_ptr<Foo> bar = foo; // pointer copy
    ...
    foo.reset(); // Foo still lives via bar
    bar.reset(); // released
    

    基于工厂的资源管理

    Foo* foo = fooFactory.build();
    ...
    fooFactory.release( foo ); // or it will be 
                               // automatically released 
                               // on factory destruction
    

    【讨论】:

    • 我相信Foo foo(); 不会编译,因为 C++ 认为它是一个函数原型。请改用Foo foo;
    • 您的第三个示例无法编译。
    【解决方案3】:

    一般来说,您的示例不是异常安全的,因此不应使用。如果这条线直接跟随新的抛出?堆栈展开,您刚刚泄漏了内存。作为堆栈展开的一部分,智能指针将为您处理它。如果您倾向于不处理异常,那么除了 RAII 问题之外没有任何退路。

    【讨论】:

      【解决方案4】:

      如果您使用异常,那么这种代码实际上肯定会导致资源泄漏。即使您禁用异常,在手动将 new 与 delete 配对时,清理工作也很容易搞砸。

      【讨论】:

        【解决方案5】:

        这完全取决于我们的意思。

        • 不应该使用new 来分配内存吗?当然应该,我们别无选择。 new 是在 C++ 中动态分配对象的方法。当我们需要动态分配 T 类型的对象时,我们使用new T(...)
        • 当我们想要实例化一个新对象时,是否应该默认调用new。在 java 或 C# 中,new 用于创建新对象,因此您可以在任何地方使用它。在 C++ 中,它仅用于堆分配。几乎所有对象都应该是堆栈分配的(或作为类成员就地创建),以便语言的范围规则帮助我们管理它们的生命周期。 new 通常不需要。通常,当我们想在堆上分配新对象时,您会将其作为更大集合的一部分进行,在这种情况下,您应该将对象推送到您的 STL 容器中,并让它担心分配和释放内存。如果您只需要一个对象,通常可以将其创建为类成员或局部变量,而无需使用new
        • new 是否应该出现在您的业务逻辑代码中?很少,如果有的话。如上所述,它通常可以而且应该隐藏在包装类中。 std::vector 例如动态分配它需要的内存。所以vector用户 不必关心。我只是在堆栈上创建一个向量,它会为我处理堆分配。当向量或其他容器类不合适时,我们可能想要编写自己的 RAII 包装器,它在构造函数中分配一些内存 new,并在析构函数中释放它。然后该包装器可以被堆栈分配,因此该类的 user 永远不必调用 new

        其中一篇文章断言,Foo *f = new Foo(); 基本上是几乎不可接受的专业 C++ 代码,自动内存管理解决方案是合适的。

        如果他们的意思是我认为他们的意思,那么他们是对的。正如我上面所说,new 通常应该隐藏在包装类中,自动内存管理(以作用域生命周期的形式和对象超出作用域时调用其析构函数)可以为您处理。这篇文章没有说“永远不要在堆上分配任何东西”或永远不要使用new”,而只是说“当你使用new时,不要只存储指向已分配内存的指针。将它放在某种类中,当它超出范围时可以处理释放它。

        而不是Foo *f = new Foo();,您应该使用以下之一:

        Scoped_Foo f; // just create a wrapper which *internally* allocates what it needs on the heap and frees it when it goes out of scope
        shared_ptr<Foo> f = new Foo(); // if you *do* need to dynamically allocate an object, place the resulting pointer inside a smart pointer of some sort. Depending on circumstances, scoped_ptr, or auto_ptr may be preferable. Or in C++0x, unique_ptr
        std::vector<Foo> v; v.push_back(Foo()); // place the object in a vector or another container, and let that worry about memory allocations.
        

        【讨论】:

          【解决方案6】:

          我认为,所有这些“......最佳实践......”问题的问题在于它们都考虑了没有上下文的代码。如果您问“一般”,我不得不承认直接内存管理是完全可以接受的。它在语法上是合法的,并且不违反任何语言语义。

          至于替代方案(堆栈变量、智能指针等),它们都有其缺点。而且它们都没有直接内存管理所具有的灵活性。您必须为这种灵活性付出的代价是您的调试时间,并且您应该意识到所有风险。

          【讨论】:

          • 是的,每个替代方案都有各自的缺点。但综合起来,它们不是像“直接”内存管理那样灵活吗?有什么可以通过直接 new/delete 调用来实现,而 任何 替代方法都无法实现的事情吗?
          • to jalf:有时你希望对象的析构函数在特定位置被准确地调用。您无法使用任何类型的共享指针来实现它,因为它们会推迟对象销毁,直到没有人指向该对象。弱指针提供开销,这可能是不可接受的。如果您想将对象的生命周期延长到范围之外,则作用域指针无能为力。
          【解决方案7】:

          一般情况下不会,但一般情况不是一般情况。这就是为什么首先发明了像 RAII 这样的自动方案。

          从我写给另一个问题的答案:

          程序员的工作是表达 用他的语言优雅地表达事物 选择。

          C++ 有很好的语义 建设和破坏 堆栈上的对象。如果一个资源 可以在一个期限内分配 范围块,然后是一个好的程序员 可能会走最少的路 反抗。对象的生命周期是 由可能是大括号分隔 反正已经在那里了。

          如果没有好的方法可以将 对象直接在堆栈上,也许它 可以作为 成员。现在它的寿命有点 更长,但 C++ 仍然有很多作用 自动地。对象的生命周期 由父对象分隔 - 问题已被委派。

          不过,可能没有一位父母。 下一个最好的事情是一系列 领养父母。这是什么 auto_ptr 是为了。还是很不错的 因为程序员应该知道 所有者是哪个特定的父母。 对象的生命周期由 其序列的生命周期 拥有者。沿着链条向下一步 决定论和优雅本身是 shared_ptr: 生命周期由 业主池的联合。

          > 但也许这个资源不是 与任何其他对象并发,设置 对象或控制流 系统。它是根据某个事件创建的 发生并摧毁另一个 事件。虽然有很多 用于划分生命周期的工具 代表团和其他生命周期,他们 不足以计算任何 任意函数。所以程序员 可能决定写一个函数 几个变量来确定是否 一个物体正在形成或 消失,并致电newdelete.

          最后,写函数可以 难的。也许管理的规则 对象会花费太多时间并且 实际计算的内存!而且它 可能真的很难表达 他们优雅地回到我的身边 原点。所以为此我们有 垃圾收集:对象 生命周期由您想要的时间划分 它和当你不这样做时。

          【讨论】:

          【解决方案8】:

          没有。

          在某些情况下不使用自动内存管理系统是有充分理由的。 这些可能是性能、循环引用导致的数据结构复杂性等。

          但是,如果您有充分的理由不使用更智能的方法,我建议仅使用带有 new/malloc 的原始指针。看到不受保护的分配让我害怕,让我希望编码人员知道他们在做什么。

          某种智能指针类,如 boost::shared_ptr、boost::scoped_ptr 将是一个好的开始。 (这些将是 C++0x 标准的一部分,所以不要害怕它们;)

          【讨论】:

          • 您是否假设“自动内存管理”仅包括引用计数?最简单的一个怎么样:“当对象超出范围时在析构函数中释放”?这实际上就是scoped_ptr 所做的。那怎么不是“自动内存管理”?
          • "这实际上是 scoped_ptr 已经做的" 这实际上是普通变量已经做的。
          • 我是说在没有明确控制对象生命周期的情况下分配堆上的对象很容易出错,而且通常是个坏主意。我没有特别假设所使用的自动内存管理的类型,只是说智能指针是一个容易开始的地方。更大的列表将包括引用计数方案、RAII 方案、对象池等。
          【解决方案9】:

          使用某种智能指针方案,您可以获得自动内存管理、引用计数等,而只需少量开销。您为此付出了代价(在内存或性能方面),但为此付出代价可能是值得的,而不必一直担心它。

          【讨论】:

          • 假设“自动内存管理”只能引用引用计数,这有什么问题?
          • @jalf:在 C++ 智能指针中,“自动内存管理”通常意味着引用计数。其他任何东西都需要基础设施。
          • 不正确。 std::vector&lt;int&gt; vec; 是自动内存管理。它分配的所有内存都被自动清理干净。想想auto 关键字(在其当前的 C++0x 之前的含义中)。为什么你认为它被称为auto?因为它自动管理对象的生命周期。所有的局部变量和类成员都是自动管理的开始。自动内存管理是默认设置。只有当我们跳过箍并调用new 时,我们才会丢失它,并且必须显式创建基础架构才能将其取回。
          • 顺便说一句,没有基础设施如何进行引用计数?据我所知,它需要更多的基础设施,而不仅仅是让范围决定生命周期。
          • jaif:我不是说“自动内存管理”只能指引用计数。
          【解决方案10】:

          首先,我认为应该是Foo *f = new Foo();

          我不喜欢使用这种语法的原因是很容易忘记在代码末尾添加delete 并让你的记忆泄漏。

          【讨论】:

          • 你会用什么其他语法来完成同样的事情?
          • 不完全一样的东西,不过我一般用Foo f = Foo();
          • 哎呀。忘了括号。这不是我所指的。 :)
          • 好吧,那我想我误解了你在问什么。
          • new Foo 是正确的——如果您调用默认构造函数,则不需要括号。 (但是是的,我会避免新/删除。)
          猜你喜欢
          • 2010-12-12
          • 1970-01-01
          • 1970-01-01
          • 2011-11-23
          • 2012-08-27
          • 1970-01-01
          • 2020-02-19
          • 2018-02-27
          • 1970-01-01
          相关资源
          最近更新 更多