我偶然发现了一个Stack Overflow问题Memory leak with std::string when using std::list<std::string>,其中一条评论说:
不要过度使用
new
。我看不出你在任何地方为什么使用new
。在C++中,您可以通过值创建对象,这是使用该语言的巨大优势之一。您不必将所有东西都分配在堆上。停止像Java程序员一样思考。
我真的不确定他的意思是什么。
为什么应尽可能在C++中按值创建对象,内部有什么区别?我是否误解了答案?
我偶然发现了一个Stack Overflow问题Memory leak with std::string when using std::list<std::string>,其中一条评论说:
不要过度使用
new
。我看不出你在任何地方为什么使用new
。在C++中,您可以通过值创建对象,这是使用该语言的巨大优势之一。您不必将所有东西都分配在堆上。停止像Java程序员一样思考。
我真的不确定他的意思是什么。
为什么应尽可能在C++中按值创建对象,内部有什么区别?我是否误解了答案?
在编译时你不知道需要多少内存。例如,当将文本文件读入字符串时,通常不知道文件的大小,因此无法在运行程序之前决定要分配多少内存。
你想分配一个离开当前块后仍然存在的内存。例如,你可能想编写一个函数string readfile(string path)
,它返回文件的内容。在这种情况下,即使堆栈可以容纳整个文件内容,你也不能从函数返回并保留已分配的内存块。
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;
}
存在不必要的动态内存分配。这会增加程序员的打字工作量,并引入忘记释放内存的风险,而这没有任何明显的好处。
基本上,最后一段总结了它的优点。尽可能经常使用自动存储可以使您的程序:
在参考的问题中,还有其他问题需要考虑。特别是以下类:
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
实例将拥有自己的字符串实例,每个实例都有自己的内存,两者都将在程序结束时释放。
在C++中广泛使用RAII被认为是最佳实践,因为以上所有原因。然而,还有一个不太明显的额外好处。基本上,它比各个部分的总和更好。整个机制组成。它可以扩展。
如果您将Line
类用作构建块:
class Table
{
Line borders[4];
};
那么
int main ()
{
Table table;
}
分配四个std::string
实例,四个Line
实例,一个Table
实例和所有字符串的内容,一切都会自动释放。
Monster
死亡时,它会向World
喷出一枚Treasure
。在其Die()
方法中,它将宝藏添加到世界中。为了保留死亡后的财宝,必须使用world->Add(new Treasure(/*...*/))
。其他选择包括shared_ptr
(可能过于复杂)、auto_ptr
(所有权转移的语义不佳)、传值(浪费资源)和move
+ unique_ptr
(尚未被广泛实现)。 - kizzx2在C++中,为给定函数中的每个本地作用域对象分配空间只需一条指令即可,在栈上进行分配,并且无法泄漏任何内存。这个注释的意图(或应该有意)是表达类似于"使用栈而不是堆"的意思。
int x; return &x;
- peterchen原因很复杂。
首先,C++不是垃圾回收的。因此,对于每一个new操作,都必须有相应的delete操作。如果你没有写这个delete操作,那么就会出现内存泄漏的问题。现在来看这个简单的例子:
std::string *someString = new std::string(...);
//Do stuff
delete someString;
这很简单。但如果“Do stuff”抛出异常会发生什么?糟糕了:内存泄漏。如果“Do stuff”提前发出return
呢?糟糕了:内存泄漏。
而这只是最简单的情况。如果你把这个字符串返回给别人,他们现在必须删除它。如果他们把它作为参数传递,接收到它的人是否需要删除它?他们应该什么时候删除它?
或者,你可以这样做:
std::string someString(...);
//Do stuff
没有 delete
。该对象在“栈”上创建,并且一旦超出范围,它将被销毁。您甚至可以返回对象,从而将其内容传递给调用函数。您可以将该对象传递给函数(通常作为引用或const引用: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()
中动态分配,存在于程序的整个生命周期中,由于情况无法轻松地在堆栈上创建,并且指向它的指针被传递给需要访问它的任何函数,那么在程序崩溃的情况下,这会导致泄漏吗?还是安全的?我认为后者是安全的,因为操作系统释放程序的所有内存应该逻辑上也会释放它,但是当涉及到new
时,我不想做任何假设。 - Justin Time - Reinstate Monicanew
创建的对象必须最终使用delete
删除,否则它们会泄漏。析构函数不会被调用,内存也不会被释放,整个过程都会出问题。由于C++没有垃圾回收机制,这是一个问题。
按值创建的对象(例如在堆栈上)会在超出范围时自动销毁。编译器会插入析构函数调用,并且在函数返回时自动释放内存。
像unique_ptr
,shared_ptr
这样的智能指针解决了悬空引用问题,但需要编码纪律并且存在其他潜在问题(可复制性、引用循环等)。
此外,在高度多线程的情况下,new
是线程之间争夺的一点;过度使用new
可能会对性能产生影响。堆栈对象的创建定义为线程本地,因为每个线程都有自己的堆栈。
按值对象的缺点是一旦主机函数返回,它们就会死亡-你不能将这些对象的引用传递回调用者,只能通过复制、返回或移动来实现。
new
must be eventually delete
d lest they leak." 这个说法,更糟糕的是,使用 new[]
创建的对象必须使用 delete[]
进行匹配,如果您删除了使用 new[]
分配的内存或者使用 delete[]
删除使用 new
分配的内存,那么会导致未定义的行为。很少有编译器会对此发出警告(某些工具如 Cppcheck 可以在可能的情况下发出警告)。 - Tony Delroymalloc()
或其相关函数来分配所需的内存。然而,堆栈无法释放堆栈中的任何项,唯一释放堆栈内存的方法是从堆栈顶部展开。 - Mikko Rantalainennew
的原因如下:
new
操作符执行时间不确定调用new
可能会导致操作系统为进程分配新的物理页面,也可能已经有合适的内存位置可用,我们无法得知。如果您经常这样做,速度会非常慢。或者它可能已经有一个合适的内存位置; 我们不知道。如果您的程序需要具有一致和可预测的执行时间(例如实时系统或游戏/物理模拟),则需要避免在时间关键循环中使用new
。
new
操作符是隐式线程同步是的,你没听错。您的操作系统需要确保您的页表是一致的,因此调用new
将导致您的线程获取隐式互斥锁。如果您从许多线程一致地调用new
,则实际上是在串行化您的线程(我曾在32个CPU上这样做,每个CPU都会命中new
以获得几百个字节,痛苦!那是一个王牌p.i.t.a.调试)。
其他问题,例如速度慢、碎片化、容易出错等,已经在其他答案中提到。
mlock()
或类似的函数。这是因为系统可能会出现内存不足的情况,没有可用于堆栈的物理内存页面,因此操作系统可能需要交换或写入一些缓存(清除脏内存)到磁盘,然后才能继续执行。 - Mikko Rantalainen考虑一个“小心谨慎”的用户,他记得将对象封装在智能指针中:
foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));
这段代码存在危险,因为无法保证T1
或T2
之前是否已经构建了shared_ptr
,所以如果new T1()
或 new T2()
中的一个在另一个成功之后失败,那么第一个对象将被泄漏,因为不存在shared_ptr
来销毁和释放它。
解决方法:使用make_shared
。
这不再是问题:C++17对这些操作的顺序施加了约束,即确保每次调用new()
必须紧接着构造相应的智能指针,中间不能有其他操作。这意味着,在调用第二个new()
时,可以保证第一个对象已经被包装在其智能指针中,从而防止在抛出异常时发生任何泄漏。
Barry in another answer提供了关于C++17引入的新评估顺序的更详细说明。
感谢@Remy Lebeau指出这仍然是C++17下的一个问题(虽然不那么严重):`shared_ptr`构造函数可能无法分配其控制块并抛出异常,在这种情况下,传递给它的指针将不被删除。new
成功分配内存,但后续的shared_ptr
构造失败,仍可能发生内存泄漏。使用std::make_shared()
也可以解决这个问题。 - Remy Lebeaushared_ptr
构造函数分配内存用于存储共享指针和删除器的控制块,因此它理论上可能会抛出内存错误。只有复制、移动和别名构造函数是不抛出异常的。make_shared
在控制块本身内分配共享对象,因此只有1次分配而不是2次。 - Remy Lebeaunew
运算符创建对象本身没有什么问题。 {
File foo = File("foo.dat");
// Do things
}
现在,注意当你在结尾括号后从那个块中掉落时,foo
超出了作用域。C++会自动为您调用其析构函数。与Java不同,您无需等待垃圾回收来找到它。
如果你写成这样
{
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
,当实际上很多时候使用栈更加合理。 - lukenew
。它的意思是,如果你可以在动态分配和自动存储之间做出选择的话,使用自动存储。 - André Caronnew
жІЎжңүй—®йўҳпјҢдҪҶеҰӮжһңдҪ дҪҝз”Ёdelete
пјҢйӮЈе°ұй”ҷдәҶпјҒ - Matthieu M.new()
应该尽可能谨慎地使用,而不是尽可能少用它。在实际情况下,需要根据实用主义的指导来尽可能经常地使用它。
将对象分配到堆栈上并依赖于它们的隐式销毁是一种简单的模型。如果对象所需的范围适合该模型,则无需使用new()
,也就不需要使用相关的delete()
和检查空指针的操作。在有许多短生命周期对象的情况下,分配到堆栈上可以减少堆碎片的问题。
但是,如果您的对象的生命周期需要超出当前作用域,则new()
是正确的选择。只需确保注意何时以及如何调用delete()
和空指针的可能性,以及使用已删除对象和所有其他指针使用中可能遇到的问题。
make_shared/_unique
可用的情况下)调用方都不需要new
或delete
。该答案忽略了真正的要点:(A)C++提供了像RVO、移动语义和输出参数这样的东西——这通常意味着通过返回动态分配的内存处理对象创建和生存期扩展变得不必要和粗心。(B)即使在需要动态分配的情况下,stdlib也提供了RAII包装器,减轻了用户的丑陋内部细节。 - underscore_d当你使用 new
时,对象将被分配到堆中。通常在预期需要扩展对象时使用。例如,当你声明一个对象时:
Class var;
如果你使用了 new 在堆上创建了一个对象,你总是需要调用 destroy 来销毁它。否则就可能会存在内存泄漏的风险。而放在栈上的对象则不容易出现内存泄漏的问题!
std::string
或 std::map
追加,确实很敏锐。我的最初反应是“但也经常用于将对象的生命周期与创建代码的作用域分离”,但实际上通过值返回或通过非 const
引用或指针接受调用者作用域的值更好,除非也涉及到“扩展”。还有一些其他合理的用途,比如工厂方法... - Tony Delroy
new
和裸指针更安全的动态分配方法。如果今天提出这个问题,答案可能会有所不同。有关动态分配通常是不必要的讨论仍然相关,但大多数答案都早于智能指针的出现。 - thomasrutter