我偶然发现了堆栈溢出问题使用std::string时内存泄漏std::list,其中一条评论是这样说的:
停止使用new这么多。我看不出你在任何地方使用new的任何理由你做到了。您可以在C++中按值创建对象,它是使用语言的巨大优势。你不必分配一切都在堆上。不要像Java程序员一样思考。
new
我不太确定他这么说是什么意思。
为什么要尽可能频繁地在C++中按值创建对象,它在内部有什么区别?我是不是误解了答案?
当您使用new时,对象被分配到堆中。它通常用于预期扩展时。当您声明一个对象时,例如,
Class var;
它被放置在堆栈上。
您将始终必须对您放置在堆上的带有new的对象调用销毁。这打开了内存泄漏的可能性。放置在堆栈上的对象不容易发生内存泄漏!
由new创建的对象最终必须是deleted,以免泄漏。析构函数不会被调用,内存不会被释放,整个位。由于C++没有垃圾回收机制,这是一个问题。
delete
由值创建的对象(即在堆栈上)在超出范围时自动死亡。析构函数调用由编译器插入,内存在函数返回时自动释放。
像unique_ptr、shared_ptr这样的智能指针解决了悬空引用问题,但它们需要编码纪律并存在其他潜在问题(可复制性、引用循环等)。
unique_ptr
shared_ptr
此外,在大量多线程的场景中,new是线程之间的争用点;过度使用new可能会影响性能。堆栈对象创建根据定义是线程本地的,因为每个线程都有自己的堆栈。
值对象的缺点是,一旦宿主函数返回,它们就会死亡——您无法将对这些对象的引用传递回调用者,只能通过按值复制、返回或移动。
在很大程度上,这是有人将自己的弱点提升到一般规则。使用new运算符创建对象没有错本身。有一些论点是,你必须遵守一些规则:如果你创建一个对象,你需要确保它会被销毁。
最简单的方法是在自动存储中创建对象,因此C++知道当它超出范围时将其销毁:
{File foo = File("foo.dat"); // do things }
现在,观察当你在结束大括号之后从该块脱落时,foo超出了范围。C++会自动为你调用它的dtor。与Java不同,你不需要等待GC找到它。
foo
你写的
{File * foo = new File("foo.dat");
您希望将其显式匹配为
delete foo;}
或者更好的是,将File *分配为“智能指针”。如果你不小心,它可能会导致泄漏。
File *
答案本身就做了一个错误的假设,即如果你不使用new,你就不会在堆上分配;事实上,在C++你不知道这一点。最多,你知道一小部分内存,比如一个指针,肯定会在堆栈上分配。然而,考虑文件的实现是否类似于
class File {private:FileImpl * fd;public:File(String fn){ fd = new FileImpl(fn);}
然后FileImpl将仍然分配到堆栈上。
FileImpl
是的,你最好确保有
~File(){ delete fd ; }
在类中也是如此;没有它,即使您根本没有在堆上分配显然,您也会从堆中泄漏内存。
new在堆上分配对象。否则,对象在堆栈上分配。查找两者之间的区别。
两个原因:
我认为海报的意思是说You do not have to allocate everything on theheap而不是stack。
You do not have to allocate everything on the
heap
stack
基本上,对象是在堆栈上分配的(当然,如果对象大小允许的话),因为堆栈分配的成本很低,而不是基于堆的分配,这涉及到分配器的大量工作,并增加了冗长性,因为你必须管理堆上分配的数据。
在C++中,只需一条指令即可在堆栈上为给定函数中的每个局部范围对象分配空间,并且不可能泄漏任何内存。该注释旨在(或应该旨在)说类似“使用堆栈而不是堆”。的内容
原因很复杂。
首先,C++不是垃圾回收。因此,对于每一个新的,必须有一个相应的删除。如果你没有把这个删除,那么你有一个内存泄漏。现在,对于这样一个简单的情况:
std::string *someString = new std::string(...);//Do stuffdelete someString;
这很简单。但是如果“做东西”抛出异常会发生什么?哎呀:内存泄漏。如果“做东西”问题return早期会发生什么?哎呀:内存泄漏。
return
这是针对最简单的情况的。如果你碰巧将该字符串返回给某人,现在他们必须删除它。如果他们将其作为参数传递,接收它的人需要删除它吗?他们应该什么时候删除它?
或者,你可以这样做:
std::string someString(...);//Do stuff
否delete。对象是在“堆栈”上创建的,一旦超出范围,它将被销毁。您甚至可以返回对象,从而将其内容转移到调用函数。您可以将对象传递给函数(通常作为引用或const-引用:void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)。等等。
void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)
全部没有new和delete。毫无疑问谁拥有内存或谁负责删除它。如果你这样做:
std::string someString(...);std::string otherString;otherString = someString;
据了解,otherString有someString的数据的副本。它不是指针;它是一个单独的对象。它们可能碰巧具有相同的内容,但您可以更改一个而不影响另一个:
otherString
someString
someString += "More text.";if(otherString == someString) { /*Will never get here */ }
看到这个想法了吗?
new()不应该被当作小来使用,而应该被当作仔细来使用,并且应该根据实用主义的需要尽可能频繁地使用。
new()
堆栈上对象的分配,依赖于它们的隐式销毁,是一个简单的模型。如果对象所需的范围符合该模型,那么就不需要使用new(),以及相关的delete()和检查NULL指针。如果你有很多短期对象,堆栈上的分配应该会减少堆碎片的问题。
delete()
但是,如果您的对象的生命周期需要扩展到当前范围之外,那么new()是正确的答案。只需确保您注意何时以及如何调用delete()以及NULL指针的可能性,使用已删除的对象以及使用指针带来的所有其他陷阱。
有两种广泛使用的内存分配技术:自动分配和动态分配。通常,每种技术都有一个相应的内存区域:堆栈和堆。
堆栈总是以顺序的方式分配内存。它可以这样做,因为它需要你以相反的顺序释放内存(先入后出:FILO)。这是许多编程语言中局部变量的内存分配技术。它非常非常快,因为它需要最少的簿记,并且下一个要分配的地址是隐式的。
在C++,这被称为自动存储,因为存储是在作用域末尾自动声明的。一旦当前代码块(使用{}分隔)的执行完成,该块中所有变量的内存将被自动收集。这也是调用析构函数清理资源的时刻。
{}
堆允许更灵活的内存分配模式。记账更复杂,分配更慢。因为没有隐式释放点,你必须使用delete或delete[](C中的free)手动释放内存。然而,没有隐式释放点是堆灵活性的关键。
delete[]
free
即使使用堆速度较慢并可能导致内存泄漏或内存碎片,动态分配也有非常好的用例,因为它的限制较少。
使用动态分配的两个关键原因:
你不知道编译时需要多少内存。例如,当将文本文件读入字符串时,你通常不知道文件有多大,所以在运行程序之前,你无法决定分配多少内存。
你想分配在离开当前块后仍然保留的内存。例如,你可能想写一个函数string readfile(string path)来返回文件的内容。在这种情况下,即使堆栈可以容纳整个文件内容,你也不能从函数返回并保留分配的内存块。
string readfile(string path)
C++有一个简洁的结构,称为析构函数。这种机制允许您通过将资源的生命周期与变量的生命周期对齐来管理资源。这种技术称为RAII,是C++的区别点。它将资源“包装”到对象中。std::string就是一个完美的例子。这个片段:
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实例将拥有自己的字符串实例,每个都有自己的内存,两者都将在程序结束时释放。
Line
由于上述所有原因,广泛使用RAII被认为是C++的最佳实践。然而,还有一个额外的好处并不是立即明显的。基本上,它比其各部分的总和要好。整个机制组成。它可以扩展。
如果您使用Line类作为构建块:
class Table{Line borders[4];};
然后
int main (){Table table;}
分配四个std::string实例、四个Line实例、一个Table实例以及字符串的所有内容和一切都是自动释放的。
Table
我倾向于不同意使用new“太多”的想法。尽管最初的海报在系统类中使用new有点荒谬。(int *i; i = new int[9999];?真的吗?int i[9999];更清楚。)我认为到是让评论者生气的原因。
int *i; i = new int[9999];
int i[9999];
当你使用系统对象时,很少需要对完全相同的对象进行多个引用。只要值相同,这就是最重要的。而且系统对象通常不会占用太多内存空间。(一个字符串中每个字符一个字节)。如果它们这样做了,库的设计应该考虑到内存管理(如果它们写得很好)。在这些情况下,(除了他代码中的一两个新闻之外),new实际上是毫无意义的,只会引入混淆和潜在的错误。
但是在使用自己的类/对象的时候(比如发起人的Line类),就得自己考虑内存占用、数据持久化等问题。在这一点上,允许多个引用同一个值是非常有价值的——它允许像链表、字典和图表这样的构造,在这些构造中,多个变量不仅需要有相同的值,还需要在内存中引用完全相同的对象。但是Line类没有这些要求。所以发起人的代码实际上完全不需要new。
避免过度使用堆的一个值得注意的原因是性能-特别是涉及C++使用的默认内存管理机制的性能。虽然在琐碎的情况下分配可以非常快,但在没有严格顺序的非均匀大小的对象上执行大量new和delete不仅会导致内存碎片,而且还会使分配算法复杂化,并且在某些情况下绝对会破坏性能。
这就是内存池要解决的问题,它允许减轻传统堆实现的固有缺点,同时仍然允许您根据需要使用堆。
不过,最好还是完全避免这个问题。如果你能把它放在堆栈上,那就这样做。
核心原因是堆上的对象总是比简单的值更难使用和管理。编写易于阅读和维护的代码始终是任何认真的程序员的首要任务。
另一个场景是我们使用的库提供了价值语义学,并且不需要动态分配。Std::string就是一个很好的例子。
Std::string
然而,对于面向对象的代码,使用指针——这意味着事先使用new来创建它——是必须的。为了简化资源管理的复杂性,我们有几十种工具来使其尽可能简单,例如智能指针。基于对象的范式或通用范式假定价值语义学,并且需要更少或不需要new,就像其他海报所述。
传统的设计模式,尤其是第1本书中提到的那些,经常使用new,因为它们是典型的OO代码。
考虑一个“谨慎”的用户,他记得用智能指针包装对象:
foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));
这段代码是危险的,因为有不能保证shared_ptr被构造之前T1或T2。因此,如果new T1()或new T2()中的一个在另一个成功后失败,那么第一个对象将被泄露,因为没有shared_ptr存在来销毁和释放它。
T1
T2
new T1()
new T2()
解决方案:使用make_shared。
make_shared
这不再是一个问题:C++17对这些操作的顺序施加了约束,在这种情况下,确保每次调用new()后必须立即构造相应的智能指针,中间没有其他操作。这意味着,当第二个new()被调用时,可以保证第一个对象已经包装在其智能指针中,从而防止引发异常时的任何泄漏。
巴里在另一个答案提供了C++17引入的新评估顺序的更详细解释。罢工>
感谢@陈志立指出这是仍然,这是C++17下的一个问题(尽管不那么严重):shared_ptr构造函数可能无法分配其控制块并抛出,在这种情况下,传递给它的指针不会被删除。
我看到了一些重要的原因,尽可能少做新的事情被遗漏了:
调用new可能会也可能不会导致操作系统为你的进程分配一个新的物理页,如果你经常这样做,这可能会很慢。或者它可能已经准备好了合适的内存位置,我们不知道。如果你的程序需要一致和可预测的执行时间(比如在实时系统或游戏/物理模拟中),你需要避免时间关键循环中的new。
是的,你听到了,你的操作系统需要确保你的页表是一致的,因此调用new将导致你的线程获得隐式互斥锁。如果你一直从许多线程调用new,你实际上是在序列化你的线程(我用32个CPU做过这件事,每个CPU都在new上获得几百个字节,哎哟!那是一个皇家p. i. t. a.调试)
其余的如速度慢、碎片化、容易出错等已经在其他答案中提到。
new是新的goto。
goto
回想一下为什么goto如此受人唾弃:虽然它是一个功能强大、低级的流程控制工具,但人们经常以不必要的复杂方式使用它,这使得代码难以理解。此外,最有用、最容易阅读的模式是编码在结构化编程语句中的(例如for或while);最终的结果是,以goto为合适方式的代码相当罕见,如果你想写goto,你可能做得很糟糕(除非你真的知道自己在做什么)。
for
while
new与之类似——它通常被用来使事情变得不必要的复杂和难以阅读,可以编码的最有用的使用模式已经被编码到各种类中。此外,如果你需要使用任何还没有标准类的新使用模式,你可以编写自己的类来编码它们!
我甚至认为new比goto是更糟,因为需要对new和delete语句进行配对。
就像goto,如果你认为你需要使用new,你可能做得很糟糕——特别是如果你在一个类的实现之外这样做,这个类的目的是封装你需要做的任何动态分配。
以上所有正确答案还有一点,这取决于你正在做什么样的编程。例如,在Windows中开发内核->堆栈受到严重限制,你可能无法像在用户模式下那样接受页面错误。
在这样的环境中,新的或类似C的API调用是首选的,甚至是必需的。
当然,这只是规则的一个例外。
许多答案已经进入了各种性能考虑。我想解决困扰OP的评论:
不要像Java程序员那样思考。
事实上,在Java,正如这个问题的答案所解释的那样,
首次显式创建对象时使用new关键字。
但是在C++中,类型T的对象是这样创建的:T{}(对于带参数的构造函数来说是T{ctor_argument1,ctor_arg2})。这就是为什么通常你没有理由想要使用new。
T
T{}
T{ctor_argument1,ctor_arg2}
那么,为什么要使用它呢?有两个原因:
现在,除了你引用的评论所暗示的之外,你应该注意到,即使是上面的两种情况也足够好,而不必自己“诉诸”使用new:
std::vector
因此,它是C++社区编码指南中的官方项目,以避免显式的new和delete:准则R.11。