当使用std::list<std::string>时,我偶然发现了堆栈溢出问题std::string的内存泄漏,其中一个注释说:
不要用那么多new。我看不出你为什么在任何地方都用新的。你可以在c++中通过值创建对象,这是使用该语言的一个巨大优势。您不必分配堆上的所有内容。不要像Java程序员那样思考。
我不太确定他这么说是什么意思。
为什么在c++中应该尽可能多地根据值创建对象,这在内部有什么不同?我误解了答案吗?
当使用std::list<std::string>时,我偶然发现了堆栈溢出问题std::string的内存泄漏,其中一个注释说:
不要用那么多new。我看不出你为什么在任何地方都用新的。你可以在c++中通过值创建对象,这是使用该语言的一个巨大优势。您不必分配堆上的所有内容。不要像Java程序员那样思考。
我不太确定他这么说是什么意思。
为什么在c++中应该尽可能多地根据值创建对象,这在内部有什么不同?我误解了答案吗?
当前回答
因为堆栈速度更快,而且防漏
在c++中,只需要一条指令就可以为给定函数中的每个局部作用域对象在堆栈上分配空间,而且不可能泄漏任何内存。该评论意图(或应该意图)说“使用堆栈而不是堆”之类的话。
其他回答
我发现有几个重要的原因让我们忽略了尽量少做新事情:
Operator new的执行时间不确定
调用new可能会导致操作系统为进程分配新的物理页,也可能不会。如果你经常这样做,会很慢。或者它可能已经准备好了一个合适的内存位置;我们不知道。如果你的程序需要具有一致且可预测的执行时间(如在实时系统或游戏/物理模拟中),你需要避免在时间关键型循环中添加新的元素。
操作符new是一个隐式线程同步
是的,你听到了。你的操作系统需要确保你的页表是一致的,因此调用new会导致你的线程获得一个隐式互斥锁。如果你一直从许多线程调用new,你实际上是在序列化你的线程(我用32个cpu做过这个,每个cpu都调用new来获得几百个字节,哎呦!那是一个需要调试的皇家p.i.t.a.。)
其余的,比如速度慢、碎片化、容易出错等,其他答案已经提到了。
新就是新目标。
Recall why goto is so reviled: while it is a powerful, low-level tool for flow control, people often used it in unnecessarily complicated ways that made code difficult to follow. Furthermore, the most useful and easiest to read patterns were encoded in structured programming statements (e.g. for or while); the ultimate effect is that the code where goto is the appropriate way to is rather rare, if you are tempted to write goto, you're probably doing things badly (unless you really know what you're doing).
New也类似——它经常被用来使事情变得不必要的复杂和难以阅读,最有用的使用模式可以被编码到各种各样的类中。此外,如果您需要使用任何新的使用模式,而这些模式还没有标准类,您可以编写自己的类来编码它们!
我甚至认为new比goto更糟糕,因为需要对new和delete语句进行配对。
像goto一样,如果您认为需要使用new,那么您可能做得很糟糕——特别是如果您在一个类的实现之外这样做,这个类的目的是封装您需要做的任何动态分配。
原因很复杂。
首先,c++不进行垃圾收集。因此,对于每一个new,必须有一个对应的delete。如果您没有将此删除,那么您就有内存泄漏。现在,对于这样一个简单的例子:
std::string *someString = new std::string(...);
//Do stuff
delete someString;
这很简单。但是如果“Do stuff”抛出异常会发生什么?哎呀:内存泄漏。如果“做事情”问题提前回归会发生什么?哎呀:内存泄漏。
这是最简单的情况。如果你碰巧将这个字符串返回给某人,现在他们必须删除它。如果他们把它作为参数传递,接收它的人需要删除它吗?什么时候删除?
或者,你可以这样做:
std::string someString(...);
//Do stuff
没有删除。对象是在“堆栈”上创建的,一旦超出作用域就会被销毁。您甚至可以返回对象,从而将其内容传递给调用函数。你可以将对象传递给函数(通常作为引用或const-reference: 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 */ }
明白了吗?
因为堆栈速度更快,而且防漏
在c++中,只需要一条指令就可以为给定函数中的每个局部作用域对象在堆栈上分配空间,而且不可能泄漏任何内存。该评论意图(或应该意图)说“使用堆栈而不是堆”之类的话。
Pre-C + + 17:
因为即使您将结果包装在智能指针中,它也容易发生细微的泄漏。
考虑一个“小心”的用户,他记得在智能指针中包装对象:
foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));
这段代码很危险,因为不能保证在T1或T2之前构造shared_ptr。因此,如果新T1()或新T2()中的一个在另一个成功后失败,那么第一个对象将被泄露,因为不存在shared_ptr来销毁和释放它。
解决方法:使用make_shared。
Post-C + + 17:
这不再是一个问题:c++ 17对这些操作的顺序施加了约束,在这种情况下,确保每次调用new()必须立即构造相应的智能指针,中间没有其他操作。这意味着,在调用第二个new()时,可以保证第一个对象已经被包装在其智能指针中,从而防止在抛出异常时发生任何泄漏。
Barry在另一个回答中提供了关于c++ 17引入的新求值顺序的更详细的解释。
感谢@Remy Lebeau指出这在c++ 17中仍然是一个问题(尽管不是那么严重):shared_ptr构造函数可能无法分配它的控制块和抛出,在这种情况下,传递给它的指针不会被删除。
解决方法:使用make_shared。