为什么C ++程序员应该尽量减少“新”的使用?

时间:2011-06-28 00:08:14

标签: c++ memory-management heap new-operator c++-faq

我偶然发现Stack Overflow问题Memory leak with std::string when using std::list<std::string>one of the comments说:

  

停止使用new这么多。我看不出你在任何地方使用新的任何理由   你做到了。您可以在C ++中按值创建对象,它是其中之一   使用该语言的巨大优势。你不必分配   堆上的一切。别像Java程序员那样思考。

我不确定他的意思是什么。为什么要尽可能经常地用C ++中的值创建对象,它在内部有什么区别?我误解了答案吗?

19 个答案:

答案 0 :(得分:960)

有两种广泛使用的内存分配技术:自动分配和动态分配。通常,每个都有一个相应的内存区域:堆栈和堆。

堆栈

堆栈总是以顺序方式分配内存。它可以这样做,因为它要求您以相反的顺序释放内存(First-In,Last-Out:FILO)。这是许多编程语言中局部变量的内存分配技术。它非常非常快,因为它需要最少的簿记,下一个要分配的地址是隐含的。

在C ++中,这称为自动存储,因为存储在范围结束时自动声明。一旦完成当前代码块的执行(使用{}分隔),就会自动收集该块中所有变量的内存。这也是调用析构函数来清理资源的时刻。

堆允许更灵活的内存分配模式。簿记更复杂,分配更慢。由于没有隐式发布点,因此您必须使用deletedelete[](C中的free)手动释放内存。但是,缺少隐式释放点是堆灵活性的关键。

使用动态分配的原因

即使使用堆较慢并且可能导致内存泄漏或内存碎片,动态分配也有很好的用例,因为它的限制较少。

使用动态分配的两个主要原因:

  • 您不知道编译时需要多少内存。例如,在将文本文件读入字符串时,通常不知道文件的大小,因此在运行程序之前无法确定要分配的内存量。

  • 您想要分配在离开当前块后仍然存在的内存。例如,您可能希望编写一个返回文件内容的函数string readfile(string path)。在这种情况下,即使堆栈可以保存整个文件内容,也无法从函数返回并保留分配的内存块。

为什么动态分配通常是不必要的

在C ++中有一个称为析构函数的简洁构造。此机制允许您通过将资源的生命周期与变量的生命周期对齐来管理资源。这种技术称为RAII,是C ++的一个显着特点。它将资源“包装”到对象中。 std::string就是一个很好的例子。这个片段:

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}

实际上分配了可变数量的内存。 std::string对象使用堆分配内存并在其析构函数中释放它。在这种情况下,您需要手动管理任何资源,并且仍然可以获得动态内存分配的好处。

特别是,它暗示在这个片段中:

int main ( int argc, char* argv[] )
{
    std::string * program = new std::string(argv[0]);  // Bad!
    delete program;
}

不需要动态内存分配。该程序需要更多的输入(!)并引入忘记释放内存的风险。这样做没有明显的好处。

为什么要尽可能经常使用自动存储

基本上,最后一段总结了它。尽可能经常使用自动存储程序:

  • 更快输入;
  • 运行时
  • 更快;
  • 不太容易出现内存/资源泄漏。

奖励积分

在引用的问题中,还有其他问题。特别是,以下类:

class Line {
public:
    Line();
    ~Line();
    std::string* mString;
};

Line::Line() {
    mString = new std::string("foo_bar");
}

Line::~Line() {
    delete mString;
}

使用的风险实际上比下一个风险大得多:

class Line {
public:
    Line();
    std::string mString;
};

Line::Line() {
    mString = "foo_bar";
    // note: there is a cleaner way to write this.
}

原因是std::string正确定义了复制构造函数。请考虑以下程序:

int main ()
{
    Line l1;
    Line l2 = l1;
}

使用原始版本,该程序可能会崩溃,因为它在同一个字符串上使用delete两次。使用修改后的版本,每个Line实例将拥有自己的字符串 instance ,每个实例都有自己的内存,两者都将在程序结束时释放。

其他说明

由于上述所有原因,RAII的广泛使用被认为是C ++中的最佳实践。但是,还有一个额外的好处并不是很明显。基本上,它比它的各个部分的总和更好。整个机制组成。它会扩展。

如果您使用Line类作为构建块:

 class Table
 {
      Line borders[4];
 };

然后

 int main ()
 {
     Table table;
 }

分配四个std::string实例,四个Line实例,一个Table实例和所有字符串的内容,一切都自动释放

答案 1 :(得分:163)

因为堆栈快速且万无一失

在C ++中,只需要一条指令就可以为堆栈中的每个本地作用域对象分配空间,并且不可能泄漏任何内存。该评论意图(或应该有意)说出像“使用堆栈而不是堆”。

答案 2 :(得分:101)

这很复杂。

首先,C ++不是垃圾回收。因此,对于每个新的,必须有相应的删除。如果你没有把这个删除,那么你有内存泄漏。现在,对于这样一个简单的案例:

std::string *someString = new std::string(...);
//Do stuff
delete someString;

这很简单。但是如果“Do stuff”抛出异常会发生什么?糟糕:内存泄漏。如果“做东西”早期问题return会发生什么?哎呀:内存泄漏。

这是针对最简单的情况。如果您碰巧将该字符串返回给某人,现在他们必须将其删除。如果他们将其作为参数传递,接收它的人是否需要删除它?什么时候应该删除它?

或者,您可以这样做:

std::string someString(...);
//Do stuff

delete。该对象是在“堆栈”上创建的,一旦超出范围,它将被销毁。您甚至可以返回对象,从而将其内容传输到调用函数。您可以将对象传递给函数(通常作为引用或const引用:void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)。等等。

全部没有newdelete。毫无疑问,谁拥有记忆或谁负责删除记忆。如果你这样做:

std::string someString(...);
std::string otherString;
otherString = someString;

据了解,otherString包含someString数据的副本。它不是指针;它是一个单独的对象。它们可能碰巧具有相同的内容,但您可以更改一个而不影响另一个:

someString += "More text.";
if(otherString == someString) { /*Will never get here */ }

看到这个想法?

答案 3 :(得分:71)

new创建的对象最终必须delete才能泄漏。析构函数不会被调用,内存不会被释放,整个位。由于C ++没有垃圾收集,这是一个问题。

由value(即堆栈)创建的对象在超出范围时自动死亡。析构函数调用由编译器插入,并在函数返回时自动释放内存。

auto_ptrshared_ptr这样的智能指针解决了悬空参考问题,但它们需要编码规则并且还有其他问题(可复制性,参考循环等)。

此外,在大量多线程场景中,new是线程之间的争用点;过度使用new可能会对性能产生影响。根据定义,堆栈对象创建是线程本地的,因为每个线程都有自己的堆栈。

值对象的缺点是,一旦主机函数返回它们就会死亡 - 你无法通过复制或按值返回来传递那些返回调用者的引用。

答案 4 :(得分:28)

  • C ++本身不使用任何内存管理器。其他语言如C#,Java具有垃圾收集器来处理内存
  • 使用操作系统例程分配内存的C ++和过多的新/删除可能会破坏可用内存
  • 对于任何应用程序,如果经常使用内存,建议预先分配并在不需要时释放。
  • 不正确的内存管理可能导致内存泄漏,而且很难跟踪。因此,在函数范围内使用堆栈对象是一种经过验证的技术
  • 使用堆栈对象的缺点是,它在返回时会创建多个对象副本,传递给函数等。但是,智能编译器非常了解这些情况,并且它们已经针对性能进行了优化
  • 如果在两个不同的地方分配和释放内存,那么在C ++中真的很乏味。发布的责任总是一个问题,主要是我们依赖一些常用的指针,堆栈对象(最大可能)和auto_ptr(RAII对象)等技术
  • 最好的是,你可以控制内存,最糟糕的是,如果我们对应用程序采用不正确的内存管理,你将无法控制内存。由于内存损坏导致的崩溃是最糟糕的,难以追踪。

答案 5 :(得分:20)

我发现错过尽可能少的新手的几个重要原因:

运算符new具有非确定性执行时间

调用new可能会也可能不会导致操作系统为您的进程分配新的物理页面,如果您经常这样做,这可能会非常慢。或者它可能已经准备好了合适的内存位置,我们不知道。如果您的程序需要具有一致且可预测的执行时间(例如在实时系统或游戏/物理模拟中),则需要在时间关键循环中避免new

运算符new是隐式线程同步

是的,你听说过我,你的操作系统需要确保你的页面表是一致的,因此调用new将导致你的线程获得隐式互斥锁。如果你一直从许多线程调用new,你实际上是在线程序列化(我用32个CPU完成了这个,每个点击new以获得几百个字节,哎哟!那是一个皇家皮塔塔调试)

其他答案已经提到了诸如缓慢,碎片,容易出错等其他问题。

答案 6 :(得分:18)

预C ++ 17:

因为即使将结果包装在智能指针中,它也容易出现微小的泄漏

考虑一个“谨慎”的用户,他记得在智能指针中包装对象:

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));

这段代码很危险,因为无保证 shared_ptr T1T2。因此,如果new T1()new T2()中的一个在另一个成功后失败,则第一个对象将被泄露,因为不存在shared_ptr来销毁和解除分配它。

解决方案:使用make_shared

发布-C ++ 17:

这不再是一个问题:C ++ 17对这些操作的顺序施加了约束,在这种情况下,确保每次调用new()后必须紧接着构建相应的操作智能指针,中间没有其他操作。这意味着,在调用第二个new()时,可以保证第一个对象已经被包装在其智能指针中,从而防止在抛出异常时发生任何泄漏。

<击>

由Barry in another answer提供了C ++ 17引入的新评估顺序的更详细说明。

感谢@Remy Lebeau指出这是仍然 C ++ 17下的问题(虽然不那么):shared_ptr构造函数可能无法分配其控件阻止和抛出,在这种情况下传递给它的指针不会被删除。

解决方案:使用make_shared

答案 7 :(得分:17)

在很大程度上,有人将自己的弱点提升为一般规则。 本身使用new运算符创建对象没有任何错误。有一些争论的原因是你必须遵守一些规则:如果你创建了一个对象,你需要确保它将被销毁。

最简单的方法是在自动存储中创建对象,因此C ++知道在超出范围时将其销毁:

 {
    File foo = File("foo.dat");

    // do things

 }

现在,观察一下,当你在结束后从那个块上掉下来时,foo超出了范围。 C ++会自动为你调用它的dtor。与Java不同,您无需等待GC找到它。

你写过吗

 {
     File * foo = new File("foo.dat");

您希望将其与

明确匹配
     delete foo;
  }

甚至更好,将您的File *分配为“智能指针”。如果你不小心它会导致泄漏。

答案本身会误以为如果你不使用new,你就不会在堆上分配;事实上,在C ++中你不知道。至多,你知道一个小的内存,比如说一个指针,肯定是在堆栈上分配的。但是,请考虑File的实现是否类似于

  class File {
    private:
      FileImpl * fd;
    public:
      File(String fn){ fd = new FileImpl(fn);}

然后FileImpl仍然分配到堆栈上。

是的,你最好确定

     ~File(){ delete fd ; }

在课堂上也是如此;没有它,即使你根本没有显然在堆上分配,你也会从堆中泄漏内存。

答案 8 :(得分:15)

new()不应该用作 little 。它应该尽可能小心地用作 。并且应该根据实用主义的需要经常使用它。

依赖于隐式破坏的堆栈上的对象分配是一个简单的模型。如果对象的所需范围符合该模型,则无需使用new(),并使用关联的delete()并检查NULL指针。 在堆栈中有大量短期对象的情况下,应该减少堆碎片的问题。

但是,如果对象的生命周期需要超出当前范围,那么new()就是正确答案。只要确保你注意何时以及如何调用delete()和NULL指针的可能性,使用删除的对象和使用指针所带来的所有其他陷阱。

答案 9 :(得分:13)

使用new时,会将对象分配给堆。它通常在您预期扩展时使用。声明诸如

之类的对象时
Class var;

它放在堆栈上。

您将始终必须使用new调用您在堆上放置的对象上的destroy。这打开了内存泄漏的可能性。放在堆栈上的对象不容易出现内存泄漏!

答案 10 :(得分:11)

避免过度使用堆的一个值得注意的原因是性能 - 特别是涉及C ++使用的默认内存管理机制的性能。虽然在简单的情况下分配可以非常快,但在没有严格顺序的情况下对非统一大小的对象执行大量newdelete不仅会导致内存碎片,而且还会使分配算法复杂化并且在某些情况下绝对会破坏性能。

这是memory pools创建解决的问题,允许减轻传统堆实现的固有缺点,同时仍允许您根据需要使用堆。

但是,更好的是,完全避免这个问题。如果你可以把它放在堆栈上,那就这样做。

答案 11 :(得分:10)

我认为海报的意思是You do not have to allocate everything on the heap 而不是stack

基本上,对象是在堆栈上分配的(当然,如果对象大小允许的话),因为堆栈分配的成本低廉,而不是基于堆的分配,这需要分配器完成一些工作,并且因此增加了详细程度你必须管理在堆上分配的数据。

答案 12 :(得分:10)

我倾向于不同意使用新“太多”的想法。虽然原始海报使用新系统类有点荒谬。 (int *i; i = new int[9999];?真的吗?int i[9999];更加清晰。)我认为 是获得评论者山羊的原因。

当您使用系统对象时,非常罕见,您需要多个引用完全相同的对象。只要价值相同,那就重要了。并且系统对象通常不会在内存中占用太多空间。 (每个字符一个字节,字符串)。如果他们这样做,那么库应该被设计为考虑到内存管理(如果它们写得很好)。在这些情况下,(除了他的代码中的一两个新闻),new几乎毫无意义,只会引起混乱和潜在的错误。

然而,当您使用自己的类/对象时(例如原始海报的Line类),您必须开始考虑内存占用,数据持久性等问题。此时,允许多次引用相同的值是非常宝贵的 - 它允许构造链接列表,字典和图形,其中多个变量不仅需要具有相同的值,而且引用完全相同的对象在记忆中。但是,Line类没有任何这些要求。因此,原始海报的代码实际上完全不需要new

答案 13 :(得分:3)

有两个原因:

  1. 在这种情况下没有必要。你的代码不必要地变得更加复杂。
  2. 它在堆上分配空间,这意味着你以后必须记住它delete,否则会导致内存泄漏。

答案 14 :(得分:1)

核心原因是堆上的对象总是难以使用和管理,而不是简单的值。编写易于阅读和维护的代码始终是任何严肃程序员的首要任务。

另一种情况是我们使用的库提供了值语义并且不需要动态分配。 Std::string就是一个很好的例子。

对于面向对象的代码,使用指针 - 这意味着使用new预先创建它 - 是必须的。为了简化资源管理的复杂性,我们提供了许多工具来使其尽可能简单,例如智能指针。基于对象的范式或通用范例假设价值语义,并且需要更少或不需要new,就像其他地方所说的海报一样。

传统的设计模式,尤其是GoF一书中提到的模式,大量使用new,因为它们是典型的OO代码。

答案 15 :(得分:1)

new是新的goto

回想一下为什么goto如此受到谴责:虽然它是一个强大的,低级别的流量控制工具,但人们经常以不必要的复杂方式使用它,这使得代码难以理解。此外,最有用和最容易阅读的模式是在结构化编程语句中编码的(例如forwhile);最终的效果是,goto是适当的方式的代码是相当罕见的,如果你想写goto,你可能做得很糟糕(除非你真的知道你在做什么。

new类似 - 它通常用于使事情变得不必要地复杂和难以阅读,并且可以编码的最有用的使用模式已被编码到各种类中。此外,如果您需要使用已经没有标准类的任何新的使用模式,您可以编写自己的编码它们的类!

由于需要配对newgoto语句,我甚至认为new 更糟而不是delete

goto一样,如果你认为你需要使用new,你可能会做得很糟糕 - 特别是如果你是在一个类的实现之外这样做的,那个类的生命目的是封装你需要做的任何动态分配。

答案 16 :(得分:1)

还要指出上述所有正确答案,这取决于您正在执行哪种编程。以Windows中的内核开发为例->堆栈受到严格限制,您可能无法像在用户模式下那样出现页面错误。

在这样的环境中,新的或类似C的API调用是首选,甚至是必需的。

当然,这只是规则的例外。

答案 17 :(得分:0)

许多答案都涉及到了各种性能方面的考虑。我想解决困扰 OP 的评论:

<块引用>

停止像 Java 程序员那样思考。

确实,在 Java 中,如对 this question 的回答中所述,

<块引用>

在第一次显式创建对象时使用 new 关键字。

但在 C++ 中,T 类型的对象是这样创建的:T{}(或 T{ctor_argument1,ctor_arg2} 用于带参数的构造函数)。这就是为什么通常您没有理由想要使用 new

那么,为什么要使用它呢?嗯,有两个原因:

  1. 您需要创建许多在编译时未知的值。
  2. 由于 C++ 实现在普通机器上的限制 - 通过分配过多空间以常规方式创建值来防止 stack overflow

现在,除了您引用的评论所暗示的内容之外,您还应该注意,即使是上述两种情况也已得到很好的涵盖,而您不必“求助”于自己使用 new

  • 您可以使用标准库中的容器类型,这些容器类型可以保存运行时可变数量的元素(例如 std::vector)。
  • 您可以使用 smart pointers,它为您提供类似于 new 的指针,但要确保在“指针”超出范围时释放内存。

因此,它是 C++ 社区编码指南中的官方条目,以避免显式 newdeleteGuideline R.11

答案 18 :(得分:-3)

new在堆上分配对象。否则,在堆栈上分配对象。查看the difference between the two