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

问题描述 投票:809回答:18

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

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

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

c++ memory-management heap new-operator c++-faq
18个回答
971
投票

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

Stack

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

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

Heap

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

Reasons to use dynamic allocation

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

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

  • 您不知道在编译时需要多少内存。例如,在将文本文件读入字符串时,通常不知道文件的大小,因此在运行程序之前无法确定要分配的内存量。
  • 您想要分配在离开当前块后将保留的内存。例如,您可能想要编写一个返回文件内容的函数string readfile(string path)。在这种情况下,即使堆栈可以保存整个文件内容,也无法从函数返回并保留分配的内存块。

Why dynamic allocation is often unnecessary

在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;
}

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

Why you should use automatic storage as often as possible

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

  • 更快打字;
  • 跑步时更快;
  • 不太容易出现内存/资源泄漏。

Bonus points

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

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实例将拥有自己的字符串实例,每个实例都有自己的内存,两者都将在程序结束时释放。

Other notes

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

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

 class Table
 {
      Line borders[4];
 };

然后

 int main ()
 {
     Table table;
 }

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


13
投票

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

Class var;

它放在堆栈上。

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


11
投票

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

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

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


10
投票

我认为这张海报的意思是说You do not have to allocate everything on theheap而不是stack

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


10
投票

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

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

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


3
投票

两个原因:

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

2
投票

new是新的goto

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

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

我甚至认为newgoto更糟糕,因为需要配对newdelete声明。

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


1
投票

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

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

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

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


1
投票

还有一点指向上述所有正确答案,这取决于您正在进行的编程类型。例如在Windows中开发内核 - >堆栈受到严重限制,您可能无法像在用户模式下那样出现页面错误。

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

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


-4
投票

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


166
投票

Because the stack is faster and leak-proof

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


102
投票

原因很复杂。

首先,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 */ }

看到这个想法?


73
投票

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

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

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

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

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


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

23
投票

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

Operator new has a non-deterministic execution time

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

Operator new is an implicit thread synchronization

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

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


18
投票

到-17 C ++:

Because it is prone to subtle leaks even if you wrap the result in a smart pointer.

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

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

这段代码很危险,因为无法保证在shared_ptrT1之前构造T2。因此,如果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


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 ; }

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


15
投票

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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.