为什么C ++程序员要尽量减less“新”的使用?
我偶然发现堆栈溢出问题内存泄漏与std ::string时,使用std :: list <std :: string> , 其中一个评论说:
停止使用
new
这么多。 我看不出任何你在任何地方使用新的理由。 您可以在C ++中创build对象,这是使用该语言的巨大优势之一。 您不必分配堆栈中的所有内容*。 不要像Java程序员那样思考。
我不确定他的意思。 为什么应该尽可能经常用C ++的价值创build对象,内部又有什么不同? 我误解了答案吗?
*评论原本说“堆”,这已经由主持人纠正到预定的“堆”。
有两种广泛使用的内存分配技术:自动分配和dynamic分配。 通常,每个内存都有相应的内存区域:堆栈和堆。
堆
堆栈始终以顺序方式分配内存。 它可以这样做,因为它需要你以相反的顺序释放内存(先进先出:FILO)。 这是许多编程语言中局部variables的内存分配技术。 这是非常非常快的,因为它需要最less的簿记,下一个地址分配是隐含的。
在C ++中,这被称为自动存储,因为存储在作用域结束时被自动声明。 只要当前代码块(使用{}
分隔)的执行完成,该块中所有variables的内存就会自动收集。 这也是调用析构函数来清理资源的时刻。
堆
堆允许更灵活的内存分配模式。 记账更复杂,分配也更慢。 由于没有隐式释放点,所以必须使用delete
或delete[]
( free
in C)手动释放内存。 但是,没有隐式释放点是堆的灵活性的关键。
使用dynamic分配的原因
即使使用堆缓慢并且可能导致内存泄漏或内存碎片,对于dynamic分配来说也有非常好的用例,因为它的限制较less。
使用dynamic分配的两个关键原因:
-
你不知道在编译时需要多less内存。 例如,在将文本文件读入string时,通常不知道文件的大小,因此在运行程序之前,您无法决定分配多less内存。
-
你想分配内存,这将离开当前块后保持。 例如,你可能想写一个返回文件内容的函数
string readfile(string path)
。 在这种情况下,即使堆栈可以保存整个文件内容,也不能从函数返回并保留分配的内存块。
为什么dynamic分配通常是不必要的
在C ++中有一个称为析构函数的整洁构造。 该机制允许您通过将资源的生命周期与variables的生命周期alignment来pipe理资源。 这种技术被称为RAII ,是C ++的区别点。 它将资源“包装”到对象中。 std::string
就是一个很好的例子。 这段代码:
int main ( int argc, char* argv[] ) { std::string program(argv[0]); }
实际上分配了一个可变数量的内存。 std::string
对象使用堆分配内存,并在析构函数中释放它。 在这种情况下,您不需要手动pipe理任何资源,仍然可以获得dynamic内存分配的好处。
特别是,这意味着在这个片段中:
int main ( int argc, char* argv[] ) { std::string * program = new std::string(argv[0]); // Bad! delete program; }
有不需要的dynamic内存分配。 该程序需要更多的input(!),并引入忘记释放内存的风险。 它没有明显的好处。
为什么你应该尽可能经常使用自动存储
基本上,最后一段是总结。 尽可能经常使用自动存储使您的程序:
- 打字速度更快;
- 运行时更快;
- 不太容易出现内存/资源泄漏。
奖励积分
在所引用的问题中,还有其他问题。 特别是以下class级:
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; }
使用原来的版本,这个程序可能会崩溃,因为它使用相同的stringdelete
两次。 使用修改后的版本,每个Line
实例将拥有自己的string实例 ,每个实例都有自己的内存,并且都将在程序结束时释放。
其他说明
由于以上所有原因,广泛使用RAII被认为是C ++的最佳实践。 但是,还有一个额外的好处,并不是很明显。 基本上,它比各部分的总和要好。 整个机制组成 。 它规模。
如果您使用Line
类作为构build块:
class Table { Line borders[4]; };
然后
int main () { Table table; }
分配四个std::string
实例,四个Line
实例,一个Table
实例和所有string的内容, 一切都自动释放 。
因为这个堆栈是快速和安全的
在C ++中,只需要一条指令就可以为给定函数中的每个本地作用域对象分配空间,并且不可能泄漏任何内存。 该评论打算(或应该打算)说“使用堆栈而不是堆”。
这很复杂。
首先,C ++不是垃圾收集。 因此,对于每一个新的,都必须有相应的删除。 如果你不能把这个删除,那么你有一个内存泄漏。 现在,对于这样一个简单的例子:
std::string *someString = new std::string(...); //Do stuff delete someString;
这很简单。 但是,如果“做什么”引发exception会发生什么? 糟糕:内存泄漏。 如果“做东西”问题会提前return
会发生什么? 糟糕:内存泄漏。
这是最简单的情况 。 如果您恰好将该string返回给某人,现在他们必须将其删除。 如果他们把它作为一个论点,接受它的人是否需要删除呢? 他们什么时候应该删除它?
或者,你可以这样做:
std::string someString(...); //Do stuff
不delete
。 该对象是在“堆栈”上创build的,一旦超出范围就会被销毁。 你甚至可以返回对象,从而将其内容传递给调用函数。 您可以将对象传递给函数(通常作为引用或常量引用: void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)
等等。
全部没有new
和delete
。 没有谁拥有记忆或谁负责删除它的问题。 如果你这样做:
std::string someString(...); std::string otherString; otherString = someString;
据了解, otherString
具有someString
数据的副本。 这不是一个指针; 这是一个单独的对象。 他们可能碰巧拥有相同的内容,但可以在不影响其他内容的情况下进行更改:
someString += "More text."; if(otherString == someString) { /*Will never get here */ }
看到这个想法?
new
创build的对象必须最终delete
以免泄漏。 内存将不会被释放,整个位。 由于C ++没有垃圾收集,这是一个问题。
由值创build的对象(即堆栈)在超出范围时自动死亡。 编译器插入析构函数调用,并在函数返回时自动释放内存。
像auto_ptr
, shared_ptr
这样的智能指针解决了悬挂的引用问题,但是它们需要编码规范,并且还有其他问题(可复制性,引用循环等)。
而且,在multithreading的情况下, new
是线程之间的争用点; 可能会对过度使用new
的性能产生影响。 由于每个线程都有自己的堆栈,所以创build堆栈对象是按照线程定义的。
值对象的缺点是,一旦宿主函数返回,它们就会死亡 – 只能通过复制或返回值来传递给调用者的引用。
- C ++本身并不使用任何内存pipe理器。 其他语言如C#,Java都有垃圾回收器来处理内存
- C ++使用操作系统例程来分配内存,而太多的新的/删除可能会碎片化可用内存
- 对于任何应用程序,如果经常使用内存,build议在不需要时预先分配内存并释放内存。
- 不正确的内存pipe理可能导致内存泄漏,而且很难跟踪。 所以在函数范围内使用堆栈对象是一种成熟的技术
- 使用堆栈对象的缺点是,它会在返回时创build对象的多个副本,传递给函数等。然而,智能编译器非常了解这些情况,并且已经对性能进行了优化
- 如果在两个不同的地方分配和释放内存,那么在C ++中真的很乏味。 发布的责任总是一个问题,我们主要依赖一些可以访问的指针,堆栈对象(最大可能的)和像auto_ptr(RAII对象)
- 最好的情况是,你已经控制了内存,最糟糕的是,如果我们对应用程序使用了不恰当的内存pipe理,你将无法控制内存。 由于内存损坏造成的崩溃是最难以追查的。
在很大程度上,这是一个把自己的弱点提高到一般水平的人。 使用new
运算符创build对象本身没有任何问题。 有什么争论的是,你必须这样做,有一些纪律:如果你创build一个对象,你需要确保它将被销毁。
最简单的方法就是在自动存储中创build对象,所以C ++知道当它超出范围时就将其销毁:
{ File foo = File("foo.dat"); // do things }
现在,观察一下,当你在结束大括号之后脱离那个块时, foo
超出了范围。 C ++会自动为你调用它的dtor。 与Java不同,您不需要等待GCfind它。
你写了吗?
{ 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 ; }
在课堂上; 没有它,即使你根本没有在堆上分配,你也会从堆中泄漏内存。
我发现,尽可能less做一些新事物的几个重要原因是错过的:
new
运营商有一个非确定性的执行时间
调用new
可能会或可能不会导致操作系统为您的进程分配新的物理页面,如果您经常这样做,可能会很慢。 或者它可能已经有一个合适的内存位置准备好了,我们不知道。 如果你的程序需要有一致的和可预测的执行时间(比如在实时系统或者游戏/物理模拟中),你需要避免在时间关键循环中出现新问题。
运算符new
是一个隐含的线程同步
是的,你听到我的,你的操作系统需要确保你的页表是一致的,因此调用new
将导致你的线程获得一个隐式的互斥锁。 如果你一直从许multithreading中调用new
,那么你实际上是串行化你的线程(我已经用32个CPU完成了这个工作,每个线程都打上了new
字节,每个字节需要几百个字节,哎!这是一个皇家皮塔debugging)
其他如缓慢,碎片化,容易出错等已被其他答案提及。
当你使用new时,对象被分配给堆。 通常在您预期扩展时使用。 当你声明一个对象如,
Class var;
它被放置在堆栈上。
你将永远不得不打电话销毁你放在堆上的对象。 这打开了内存泄漏的可能性。 放置在堆栈上的对象不容易泄漏内存!
new()
不应该尽可能less使用。 应尽可能小心使用。 而且应该按照实用主义的说法,尽可能多地使用。
依靠它们的隐式破坏来分配堆栈上的对象是一个简单的模型。 如果一个对象的所需范围适合该模型,那么就不需要使用new()
和关联的delete()
并检查NULL指针。 在堆栈中有很多短暂的对象分配的情况下,应该减less堆碎片的问题。
但是,如果对象的生命周期需要超出当前范围,那么new()
就是正确的答案。 只要确保您注意何时以及如何调用delete()
以及NULL指针的可能性,使用已删除的对象以及使用指针所带来的所有其他陷阱。
因为即使将结果包装在智能指针中 ,也容易发生细微的泄漏。
考虑一个“小心”的用户,他记得在智能指针中包装对象:
foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));
这段代码很危险,因为不能保证 在 T1
或T2
之前构造shared_ptr
。 因此,如果new T1()
或new T2()
在另一个成功后失败,则第一个对象将被泄漏,因为不存在shared_ptr
来销毁和释放它。
解决scheme:使用make_shared
。
我认为海报的意思是说You do not have to allocate everything on the
heap
You do not have to allocate everything on the
而不是You do not have to allocate everything on the
stack
。
基本上,对象被分配到堆栈上(如果对象的大小允许,当然),因为堆栈分配的成本低廉,而不是基于堆的分配,这涉及到分配器的一些工作,并增加了冗长,因为那么你必须pipe理在堆上分配的数据。
我倾向于不同意使用新“太多”的想法。 虽然原来的海报用系统类的新东西有点荒谬。 ( int *i; i = new int[9999];
really? int i[9999];
更清晰。)我认为这是得到评论者的山羊。
当你使用系统对象的时候,你很less需要一个以上的引用来完全相同的对象。 只要价值是一样的,那就重要了。 系统对象通常不占用内存中的太多空间。 (每个字符一个字节,一个string)。 如果他们这样做,图书馆的devise应该考虑到内存pipe理(如果写得好的话)。 在这些情况下(除了其中的一两个新闻外),新的实际上是没有意义的,只是为了引入混淆和潜在的错误。
然而,当你使用自己的类/对象时(例如原来的海报的Line类),你必须开始考虑内存占用,数据持久性等问题。 此时,允许多次引用相同的值是非常宝贵的 – 它允许像链接列表,字典和graphics这样的构造,其中多个variables不仅需要具有相同的值,还要引用内存中完全相同的对象 。 但是,Line类没有任何这些要求。 所以原来的海报代码其实绝对没有new
需求。
避免过度使用堆的一个值得注意的原因是性能 – 具体涉及C ++使用的默认内存pipe理机制的性能。 虽然在微不足道的情况下分配是相当快的,但是在没有严格的顺序的情况下对大小不一的对象进行大量的new
和delete
不仅导致内存碎片化,而且使分配algorithm变得复杂,并且在某些情况下会绝对地破坏性能。
这就是需要解决的内存池问题,可以缓解传统堆实现的固有缺点,同时仍然允许您根据需要使用堆。
更好的是,完全避免这个问题。 如果你可以把它放在堆栈上,那就这样做。
两个原因:
- 在这种情况下没有必要 你让代码不必要地变得更复杂。
- 它在堆上分配空间,这意味着你必须记住稍后
delete
它,否则会导致内存泄漏。
其核心原因是堆上的对象总是比简单的值难以使用和pipe理。 编写易于阅读和维护的代码始终是任何严肃程序员的首要任务。
另一种情况是我们使用的库提供了值语义,并且不需要进行dynamic分配。 Std::string
就是一个很好的例子。
然而对于面向对象的代码来说,使用一个指针(这意味着使用new
来创build它)是必须的。 为了简化资源pipe理的复杂性,我们有几十种工具使其尽可能简单,比如智能指针。 基于对象的范式或generics范式假定价值语义,并且需要较less或不需要new
,就像其他地方所述的海报一样。
传统的devise模式,尤其是GoF书中提到的那些,使用了很多new
的东西,因为它们是典型的OO代码。
new
是新的goto
。
回想一下为什么goto
是如此的唾骂:虽然它是stream量控制的一个强大的低级工具,但是人们经常以不必要的复杂方式使用它,导致代码难以遵循。 此外,最有用和最容易阅读的模式被编码在结构化编程语句(例如for
或while
); 最终的效果是, goto
代码是比较less见的合适的方法,如果你试图写goto
,那么你可能做得不好(除非你真的知道你在做什么)。
new
是相似的 – 它经常被用来使事情变得不必要的复杂和难以阅读,并且最有用的使用模式可以被编码已被编码成各种类别。 此外,如果您需要使用任何尚未使用标准类的新使用模式,则可以编写自己的类来编码它们!
我甚至会认为,由于需要配对new
语句和delete
语句, new
语句比goto
更糟 。
就像goto
一样,如果你认为你需要使用new
,你可能会做的很糟糕 – 特别是如果你不这样做,那么在实现一个类的目的是要封装你需要做的dynamic分配。
new
的堆分配对象。 否则,对象将被分配到堆栈上。 查看两者的区别 。