我在使用std :: list <std :: string>时偶然发现了std :: string的内存溢出问题内存泄漏,其中一条评论说:
停止使用
new
太多。我看不到您在任何地方使用新产品的任何原因。您可以在C ++中按值创建对象,这是使用该语言的巨大优势之一。
您不必在堆上分配所有内容。
不要像Java程序员那样思考。
我不确定他的意思。
为什么要在C ++中尽可能频繁地通过值创建对象,它在内部有什么不同?
我是否误解了答案?
我在使用std :: list <std :: string>时偶然发现了std :: string的内存溢出问题内存泄漏,其中一条评论说:
停止使用
new
太多。我看不到您在任何地方使用新产品的任何原因。您可以在C ++中按值创建对象,这是使用该语言的巨大优势之一。
您不必在堆上分配所有内容。
不要像Java程序员那样思考。
我不确定他的意思。
为什么要在C ++中尽可能频繁地通过值创建对象,它在内部有什么不同?
我是否误解了答案?
Answers:
有两种广泛使用的内存分配技术:自动分配和动态分配。通常,每个都有一个对应的内存区域:堆栈和堆。
堆栈始终以顺序方式分配内存。这样做是因为它要求您以相反的顺序释放内存(先进先出:FILO)。这是许多编程语言中用于局部变量的内存分配技术。这是非常非常快的,因为它需要最少的簿记并且下一个要分配的地址是隐式的。
在C ++中,这称为自动存储,因为该存储在作用域末尾自动声明。当前代码块(使用分隔{}
)的执行完成后,将自动收集该块中所有变量的内存。这也是调用析构函数清理资源的时刻。
堆允许更灵活的内存分配模式。簿记更加复杂,分配也较慢。因为没有隐式释放点,所以必须使用C中的delete
或delete[]
(free
)手动释放内存。但是,缺少隐式释放点是堆灵活性的关键。
即使使用堆的速度较慢,并且可能导致内存泄漏或内存碎片,但由于动态内存分配的局限性较小,因此存在很好的用例。
使用动态分配的两个主要原因:
您不知道在编译时需要多少内存。例如,当将文本文件读取为字符串时,通常不知道文件的大小,因此在运行程序之前,您无法确定要分配多少内存。
您要分配在离开当前块后将保留的内存。例如,您可能想编写一个string readfile(string path)
返回文件内容的函数。在这种情况下,即使堆栈可以容纳整个文件内容,也无法从函数返回并保留分配的内存块。
在C ++中,有一个名为destructor的简洁构造。此机制使您可以通过将资源的生存期与变量的生存期对齐来管理资源。该技术称为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
实例将拥有自己的字符串实例,每个实例都具有自己的内存,并且两者都将在程序结尾处释放。
由于上述所有原因,广泛使用RAII被认为是C ++中的最佳实践。但是,还有其他好处尚不明显。基本上,它比各个部分的总和要好。整个机制组成。它可以缩放。
如果将Line
类用作构建块:
class Table
{
Line borders[4];
};
然后
int main ()
{
Table table;
}
分配四个std::string
实例,四个Line
实例,一个Table
实例和所有字符串的内容,并且所有内容都会自动释放。
Monster
吐出一个Treasure
到World
。它的Die()
方法将宝藏带给世界。world->Add(new Treasure(/*...*/))
死亡后,必须将其用于保存珍宝。备选方案是shared_ptr
(可能是过大的),auto_ptr
(所有权转让的语义不佳),按值传递(浪费)和move
+ unique_ptr
(尚未广泛实施)。
在C ++中,只需要一条指令就可以为给定函数中的每个局部作用域对象在堆栈上分配空间,并且不可能泄漏任何该内存。该注释意图(或应该意图)说诸如“使用堆栈而不是堆”之类的内容。
int x; return &x;
原因很复杂。
首先,C ++不会被垃圾收集。因此,对于每个新项,必须有一个对应的删除项。如果您无法放入此删除项,则内存泄漏。现在,对于这样的简单情况:
std::string *someString = new std::string(...);
//Do stuff
delete someString;
这很简单。但是,如果“东西”抛出异常会怎样?糟糕:内存泄漏。如果“做东西” return
提早发布会怎样?糟糕:内存泄漏。
这是最简单的情况。如果您碰巧将该字符串返回给某人,则现在他们必须删除它。并且,如果他们将其作为参数传递,那么接收它的人是否需要删除它?他们什么时候应该删除它?
或者,您可以执行以下操作:
std::string someString(...);
//Do stuff
没有delete
。对象是在“堆栈”上创建的,一旦超出范围,它将被销毁。您甚至可以返回对象,从而将其内容传输到调用函数。您可以将对象传递给函数(通常作为引用或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 */ }
看到这个主意了吗?
main()
在程序中将对象动态分配到中,则该对象在程序的存在期间一直存在,由于这种情况,无法在堆栈上轻松创建该对象,并且将其指针传递给需要访问该对象的任何函数,在程序崩溃的情况下会导致泄漏吗?还是安全?我会假设是后者,因为OS分配所有程序的内存也应该在逻辑上对其进行分配,但是在涉及到时,我不想做任何事情new
。
new
最终创建的对象必须delete
避免它们泄漏。整个过程都不会调用析构函数,也不会释放内存。由于C ++没有垃圾回收,因此这是一个问题。
由值创建的对象(即在堆栈上)在超出范围时会自动死亡。析构函数调用由编译器插入,并且在函数返回时自动释放内存。
智能指针一样unique_ptr
,shared_ptr
解决了悬挂引用问题,但它们需要编码规则,并有其他潜在问题(可拷贝性,参考循环等)。
同样,在高度多线程的情况下,new
线程之间的争用是重点。过度使用会对性能产生影响new
。根据定义,堆栈对象的创建是线程局部的,因为每个线程都有自己的堆栈。
值对象的缺点是,一旦宿主函数返回,它们就会死掉-您不能仅通过复制,返回或按值移动将对它们的引用传递回调用方。
new
最终必须delete
避免它们泄漏”。-更糟糕的是,new[]
必须与匹配delete[]
,并且delete
new[]
-ed内存或delete[]
new
-ed内存会导致未定义的行为-很少有编译器对此发出警告(某些工具,如Cppcheck会在可能的情况下进行)。
malloc()
向其发出调用或与其朋友分配所需的内存。但是,堆栈无法释放堆栈中的任何项目,释放堆栈内存的唯一方法是从堆栈顶部展开。
我发现错过了一些尽可能少的新事物的重要原因:
new
执行时间不确定调用new
可能会或可能不会导致操作系统为您的进程分配新的物理页面,如果您经常这样做,可能会很慢。或者它可能已经准备好合适的存储位置,我们不知道。如果您的程序需要具有一致且可预测的执行时间(例如,在实时系统或游戏/物理模拟中),则需要避免new
出现时间紧迫的循环。
new
是隐式线程同步是的,您听说过,您的操作系统需要确保页面表是一致的,因此调用new
将导致您的线程获取隐式互斥锁。如果您一直new
从多个线程进行调用,则实际上是在对线程进行序列化(我已经用32个CPU进行了此操作,每个CPU都new
获得了数百个字节,哎呀!这是调试的皇家皮塔饼)
诸如慢,碎片,易于出错等之类的其他问题已经在其他答案中提及。
void * someAddress = ...; delete (T*)someAddress
mlock()
或类似的电话。这是因为系统可能内存不足,并且堆栈没有可用的物理内存页面,因此操作系统可能需要在执行之前交换或向磁盘写入一些缓存(清除脏内存)。
考虑一个“谨慎”的用户,他记得将对象包装在智能指针中:
foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));
此代码是危险的,因为有没有保证,要么shared_ptr
被构建之前无论是T1
或T2
。因此,如果一个new T1()
或new T2()
另一个成功后失败,则第一个对象将被泄漏,因为不shared_ptr
存在销毁和取消分配它的情况。
解决方案:使用make_shared
。
这不再是问题:C ++ 17对这些操作的顺序施加了约束,在这种情况下,确保每次调用都new()
必须立即构造相应的智能指针,并且中间没有其他操作。这意味着,在new()
调用第二个对象时,可以确保第一个对象已经被包装在其智能指针中,从而防止在引发异常的情况下发生任何泄漏。
Barry 在另一个答案中提供了C ++ 17引入的新评估顺序的更详细说明。
感谢@Remy Lebeau指出,这在C ++ 17下仍然是一个问题(尽管更少):shared_ptr
构造函数可能无法分配其控制块并抛出,在这种情况下,传递给它的指针不会被删除。
解决方案:使用make_shared
。
new
成功仍然会发生泄漏,然后后续shared_ptr
构造也会失败。std::make_shared()
也会解决这一问题
shared_ptr
构造函数@Mehrdad 为存储共享指针和删除器的控制块分配内存,因此,是的,从理论上讲,它可以引发内存错误。仅复制,移动和别名构造函数是非抛出的。make_shared
分配控制块本身内的共享对象,所以只有1分配而不是2
在很大程度上,这是将自己的弱点提升为一般规则的人。使用运算符创建对象本身没有错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 ; }
在课堂上 没有它,即使您显然根本没有在堆上分配内存,您也会从堆中泄漏内存。
new
本身没有错,但是如果您查看注释所引用的原始代码,new
则会被滥用。代码的编写就像是Java或C#,new
实际上,将每个变量都放在堆栈上时,几乎所有变量都使用该代码。
new
。它表示,如果您可以在动态分配和自动存储之间进行选择,请使用自动存储。
new
,但是如果使用delete
,那就错了!
new()
不应该被用来作为小越好。应该尽可能小心地使用它。而且,应根据实用主义的需要尽可能多地使用它。
依靠对象的隐式破坏,将对象分配到堆栈上是一个简单的模型。如果对象的要求范围适合该模型,则无需使用new()
,关联delete()
和检查NULL指针。如果您在堆栈上分配了许多短期对象,则应减少堆碎片的问题。
但是,如果对象的生存期需要扩展到当前范围之外,那new()
是正确的答案。只要确保您注意调用的时间delete()
和方式以及NULL指针的可能性,使用删除的对象以及使用指针附带的所有其他陷阱即可。
const
ref或指针接受调用者范围的变量……?
make_shared/_unique
可用的地方)被调用方就不需要new
或不需要delete
。这个答案错过了真正的要点:(A)C ++提供了RVO,移动语义和输出参数之类的东西-这通常意味着通过返回动态分配的内存来处理对象创建和生命周期扩展变得不必要和粗心。(B)即使在需要动态分配的情况下,stdlib也提供RAII包装器,以减轻用户的丑陋内部细节。
使用new时,对象将分配给堆。通常在预期扩展时使用。当您声明诸如
Class var;
它放在堆栈上。
您将始终必须使用new对放置在堆上的对象调用destroy。这为内存泄漏打开了可能。放置在堆栈上的对象不容易发生内存泄漏!
std::string
或std::map
,是,敏锐的洞察力。我最初的反应是“但也通常将对象的生命周期与创建代码的作用域脱钩”,但实际上通过值返回或通过非const
引用或指针接受调用方作用域的值会更好,除非涉及到“扩展”太。这里也有一些其他声音的用途就像工厂方法虽然....
避免过度使用堆的一个显着原因是性能-特别是涉及C ++使用的默认内存管理机制的性能。虽然分配可以在琐碎的情况下是相当快的,做了很多的new
和delete
非均匀大小的物体没有严格的秩序,不仅导致内存碎片,但它也分配算法复杂化,并完全可以摧毁在某些情况下的性能。
这就是创建要解决的内存池的问题,它可以减轻传统堆实现的固有缺点,同时仍允许您根据需要使用堆。
不过,最好还是完全避免该问题。如果可以将其放在堆栈上,请这样做。
我认为发布者的意思是说You do not have to allocate everything on the
heap
而不是说stack
。
基本上,由于便宜的堆栈分配成本,对象是在堆栈上分配的(当然,如果对象大小允许的话),而不是基于堆的分配,这涉及分配器的大量工作,并且增加了冗长性,因为您必须管理在堆上分配的数据。
我倾向于不同意使用新的“太多”的想法。尽管原始发布者在系统类中使用new有点荒谬。(int *i; i = new int[9999];
真的吗?int i[9999];
要清楚得多。)我认为这就是引起评论者注意的地方。
当你与系统对象的工作,这是非常罕见的,你需要不止一个参考完全相同的对象。只要值相同,就很重要。系统对象通常不会占用太多内存。(每个字符一个字节,一个字符串)。如果这样做的话,库的设计应考虑到该内存管理(如果编写得当)。在这些情况下(除了代码中的一两个新闻,所有新闻都是毫无意义的),只会引起混乱和潜在的错误。
但是,当您使用自己的类/对象(例如,原始发布者的Line类)时,您必须开始自己考虑诸如内存占用,数据持久性等问题。在这一点上,允许多次引用相同的值是非常宝贵的-它允许使用链表,字典和图形之类的构造,其中多个变量不仅需要具有相同的值,而且还必须在内存中引用完全相同的对象。但是,Line类没有任何这些要求。因此,原始发布者的代码实际上绝对不需要new
。
When you're working with your own classes/objects
...您通常没有理由这样做!Q的一小部分与熟练的编码人员有关容器设计的细节有关。与之形成鲜明对比的是,令人沮丧的比例是不知道stdlib存在的新手的困惑-或在“编程”“课程”中积极地分配了艰巨的任务,导师要求他们毫无意义地重新发明轮子-在他们甚至了解了什么是轮子以及它为什么起作用。通过促进更多的抽象分配,C ++可以使我们摆脱C的无尽“带有链表的段错误”;拜托,让我们吧。
int *i; i = new int[9999];
真的吗?int i[9999];
要清晰得多。”)是的,虽然更清晰,但是扮演魔鬼的拥护者,类型不一定是一个不好的论点。对于9999个元素,我可以想象一个紧凑的嵌入式系统没有足够的堆栈来容纳9999个元素:9999x4字节为〜40 kB,x8〜80 kB。因此,假设这些系统使用替代内存来实现动态分配,则可能需要使用动态分配。不过,那只能证明动态分配是合理的,而不能new
;一个vector
是在这种情况下,真正的解决
std::make_unique<int[]>()
当然有)。
new
是新的goto
。
回想一下为什么goto
这么讨厌:尽管它是用于流控制的功能强大的低级工具,但是人们经常以不必要的复杂方式使用它,这使得代码难以遵循。此外,最有用和最容易阅读的模式是在结构化的编程语句(例如for
或while
)中编码的;最终的结果是,在哪里找到goto
合适的代码是非常罕见的,如果您很想编写代码,则goto
可能做得不好(除非您真的知道自己在做什么)。
new
相似-它通常用于使事情变得不必要地复杂和难以阅读,并且可以编码的最有用的用法模式已编码为各种类。此外,如果您需要使用尚没有标准类的任何新用法模式,则可以编写自己的对它们进行编码的类!
我甚至会认为new
是糟糕的比goto
,由于需要对new
与delete
报表。
就像goto
,如果您认为自己需要使用new
,那么您可能做错了事情-尤其是在类的实现之外这样做,而该类的实现的目的是封装您需要做的任何动态分配。
核心原因是堆上的对象总是比简单值难于使用和管理。编写易于阅读和维护的代码始终是任何认真的程序员的头等大事。
另一种情况是我们正在使用的库提供值语义,并且不需要动态分配。Std::string
是一个很好的例子。
但是,对于面向对象的代码,必须使用指针(这意味着new
必须先创建指针)。为了简化资源管理的复杂性,我们提供了数十种工具来使它尽可能简单,例如智能指针。基于对象的范式或泛型范式采用值语义,并且需要更少或不需要new
,就像其他地方的张贴者指出的那样。
传统的设计模式,尤其是GoF书中提到的模式,使用new
很多,因为它们是典型的OO代码。
For object oriented code, using a pointer [...] is a must
:废话。如果仅通过引用一个小的子集来贬低“ OO”,那么 多态性 - 也是废话:引用也可以。[pointer] means use new to create it beforehand
:尤其是废话:引用或指针可用于自动分配的对象,并且可以多态使用;看着我。[typical OO code] use new a lot
:也许在一本旧书中,但是谁在乎呢?任何依稀现代C ++避开new
/原始指针尽可能- &是在没有办法这样做的任何较少OO
还有一个以上所有正确答案的要点,这取决于您正在执行哪种编程。例如,在Windows中开发内核->堆栈受到严格限制,您可能无法像在用户模式下那样出现页面错误。
在这样的环境中,新的或类似C的API调用是首选的,甚至是必需的。
当然,这仅仅是规则的例外。