提问



我在使用std :: list< std :: string>时偶然发现Stack Overflow问题内存泄漏与std :: string,其中一条评论说:



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



我不太确定他的意思是什么。为什么对象应该尽可能经常地通过C ++中的值创建,它在内部有什么不同?我是否误解了答案?

最佳参考


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


堆栈



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


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




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


使用动态分配的原因



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


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



  • 你不知道在编译时你需要多少内存。例如,当把文本文件读成字符串时,你通常不知道文件的大小,所以你不能决定多少内存分配,直到您运行该程序。

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



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



在C ++中,有一个称为析构函数的简洁结构。这种机制允许您通过将资源的生命周期与变量的生命周期对齐来管理资源。这种技术称为RAII,是区别点。 C ++。它将资源包装到对象中。std::string是一个完美的例子。这个片段:[103]


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 ++中的最佳实践。但是,还有一个额外的好处并不是很明显。基本上,它比它的各个部分的总和更好。整个机制组成。它可以扩展。[104]


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


 class Table
 {
      Line borders[4];
 };


然后


 int main ()
 {
     Table table;
 }


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

其它参考1


因为堆栈快速且万无一失



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

其它参考2


这很复杂。


首先,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;


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


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


看到这个想法?

其它参考3


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


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


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


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


值对象的缺点是它们在主机函数返回后会死亡 - 您无法通过复制或按值返回来将引用传递回调用者。

其它参考4



  • C ++本身并没有使用任何内存管理器。其他语言如C#,Java有垃圾收集器来处理内存

  • 使用操作系统例程分配内存的C ++和过多的新/删除可能会破坏可用内存

  • 对于任何应用程序,如果经常使用内存,建议预先分配它并在不需要时释放。

  • 不正确的内存管理可能会导致内存泄漏并且很难跟踪。因此在函数范围内使用堆栈对象是一种经过验证的技术

  • 使用堆栈对象的缺点是,它在返回时会创建多个对象副本,传递给函数等。但是,智能编译器非常了解这些情况并且它们已经针对性能进行了优化

  • 如果在两个不同的地方分配和释放内存,C ++真的很乏味。发布的责任总是一个问题,主要是我们依赖一些常用的指针,堆栈对象(最大可能)和auto_ptr等技术(RAII对象)

  • 最好的是,你可以控制内存,最糟糕的是,如果我们对应用程序采用不正确的内存管理,你将无法控制内存。由于内存损坏导致的崩溃是最恶劣,难以追查的。


其它参考5


在很大程度上,有人将自己的弱点提升为一般规则。使用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 ; }


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

其它参考6


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


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



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


运算符new是隐式线程同步



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


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

其它参考7


使用new时,会将对象分配给堆。它通常在您预期扩展时使用。当你声明一个像这样的对象时,


Class var;


它放在堆栈上。


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

其它参考8


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


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


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

其它参考9


预C ++ 17:



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



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


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


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


解决方案:使用make_shared


发布-C ++ 17:



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


Barry在另一个答案中提供了由C ++ 17引入的新评估顺序的更详细解释。

其它参考10


我认为这张海报意味着You do not have to allocate everything on the heap 而不是stack


基本上对象在堆栈上分配(当然,如果对象大小允许),因为堆栈分配的成本低廉,而不是基于堆的分配,这涉及分配器的相当多的工作,并且增加了冗长,因为那时你必须管理堆上分配的数据。

其它参考11


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


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


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

其它参考12


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


这就是创建解决内存池的问题,允许减轻传统堆实现的固有缺点,同时仍允许您根据需要使用堆。[106]


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

其它参考13


两个原因:



  1. 在这种情况下没有必要。你让你的代码变得更加复杂。

  2. 它在堆上分配空间,这意味着你以后必须记住delete,否则会导致内存泄漏。


其它参考14


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


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


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


传统的设计模式,特别是GoF书中提到的模式,使用new很多,因为它们是典型的OO代码。[107]

其它参考15


new是新的goto


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


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


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


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

其它参考16


new在堆上分配对象。否则,在堆栈上分配对象。查看两者之间的差异。[108]