我偶然发现 Stack Overflow 问题 Memory leak with std::string when using std::list<std::string>,one of the comments 是这样说的:
停止使用新的这么多。我看不出你在任何地方使用 new 的任何原因。您可以在 C++ 中按值创建对象,这是使用该语言的巨大优势之一。您不必在堆上分配所有内容。不要像 Java 程序员那样思考。
我不太确定他的意思是什么。
为什么要在 C++ 中尽可能频繁地按值创建对象,它在内部有什么不同?我误解了答案吗?
new
和裸指针更安全的动态分配新方法。如果今天问这个问题,答案可能会有所不同。关于动态分配的讨论通常是不必要的,仍然是相关的。但是,大多数答案早于智能指针。
有两种广泛使用的内存分配技术:自动分配和动态分配。通常,每个都有一个相应的内存区域:堆栈和堆。
堆
堆栈总是按顺序分配内存。它可以这样做是因为它要求您以相反的顺序(先进后出:FILO)释放内存。这是许多编程语言中局部变量的内存分配技术。它非常非常快,因为它需要最少的簿记并且要分配的下一个地址是隐式的。
在 C++ 中,这称为 自动存储,因为存储在作用域结束时会自动声明。当前代码块(使用 {}
分隔)的执行完成后,将自动收集该块中所有变量的内存。这也是调用 析构函数 来清理资源的时刻。
堆
堆允许更灵活的内存分配模式。簿记更复杂,分配更慢。由于没有隐式释放点,您必须使用 delete
或 delete[]
(C 中的 free
)手动释放内存。然而,没有隐式释放点是堆灵活性的关键。
使用动态分配的原因
即使使用堆的速度较慢并且可能导致内存泄漏或内存碎片,动态分配也有非常好的用例,因为它的限制较少。
使用动态分配的两个主要原因:
您不知道编译时需要多少内存。例如,在将文本文件读入字符串时,您通常不知道文件的大小,因此在运行程序之前您无法决定分配多少内存。
您想要分配在离开当前块后将持续存在的内存。例如,您可能想要编写一个函数 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
实例都将拥有自己的字符串 instance,每个实例都有自己的内存,并且都将在程序结束时释放。
其他注意事项
由于上述所有原因,广泛使用 RAII 被认为是 C++ 中的最佳实践。但是,还有一个不是立即显而易见的额外好处。基本上,它比各个部分的总和要好。整个机制组成。它可以扩展。
如果您使用 Line
类作为构建块:
class Table
{
Line borders[4];
};
然后
int main ()
{
Table table;
}
分配四个 std::string
实例、四个 Line
实例、一个 Table
实例和所有字符串的内容,自动释放所有内容。
因为堆栈更快且防泄漏
在 C++ 中,只需要一条指令即可为给定函数中的每个局部范围对象分配空间(在堆栈上),并且不可能泄漏任何内存。该评论打算(或应该打算)说“使用堆栈而不是堆”之类的东西。
int x; return &x;
原因很复杂。
首先,C++ 不是垃圾收集器。因此,对于每一个新的,都必须有一个相应的删除。如果你没有把这个删除,那么你有内存泄漏。现在,对于这样一个简单的案例:
std::string *someString = new std::string(...);
//Do stuff
delete someString;
这很简单。但是如果“Do stuff”抛出异常会发生什么?糟糕:内存泄漏。如果“Do stuff”提前发出 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()
中动态分配的,在程序期间存在,由于这种情况,不能轻易在堆栈上创建,并且指向它的指针被传递给任何需要的函数访问它,这会在程序崩溃的情况下导致泄漏,还是安全?我会假设后者,因为释放所有程序内存的操作系统也应该在逻辑上释放它,但我不想在 new
方面做任何假设。
new
创建的对象必须最终delete
d,以免它们泄漏。析构函数不会被调用,内存不会被释放,整个位。由于 C++ 没有垃圾收集,所以这是一个问题。
按值创建的对象(即在堆栈上)在超出范围时会自动死亡。析构函数调用由编译器插入,内存在函数返回时自动释放。
像 unique_ptr
、shared_ptr
这样的智能指针解决了悬空引用问题,但它们需要编码纪律并且存在其他潜在问题(可复制性、引用循环等)。
此外,在多线程场景中,new
是线程之间的争用点;过度使用 new
可能会影响性能。根据定义,堆栈对象的创建是线程本地的,因为每个线程都有自己的堆栈。
值对象的缺点是一旦宿主函数返回它们就会死掉——你不能将引用传回给调用者,只能通过复制、返回或按值移动。
new
创建的对象必须最终delete
以免泄漏。” - 更糟糕的是,new[]
必须与 delete[]
匹配,如果您使用 delete
new[]
-ed 内存或 delete[]
new
-ed 内存,您会得到未定义的行为 - 很少有编译器会警告这一点(一些像 Cppcheck 这样的工具可以做到)。
C++ 本身不使用任何内存管理器。其他语言如 C#、Java 有垃圾收集器来处理内存
C++ 实现通常使用操作系统例程来分配内存,过多的新/删除可能会使可用内存碎片化
对于任何应用程序,如果经常使用内存,建议预先分配它并在不需要时释放它。
不正确的内存管理可能会导致内存泄漏,而且很难跟踪。所以在函数范围内使用栈对象是一种行之有效的技术
使用堆栈对象的缺点是,它会在返回、传递给函数等时创建多个对象副本。然而,智能编译器很清楚这些情况,并且已经针对性能进行了很好的优化
如果在两个不同的地方分配和释放内存,在 C++ 中真的很乏味。释放的责任始终是一个问题,我们主要依赖于一些常用的可访问指针、堆栈对象(最大可能)和 auto_ptr(RAII 对象)等技术
最好的事情是,您可以控制内存,最糟糕的是,如果我们对应用程序使用不正确的内存管理,您将无法控制内存。由于内存损坏导致的崩溃是最严重且难以追踪的。
malloc()
或其朋友来分配所需的内存。但是,堆栈不能释放堆栈中的任何项目,释放堆栈内存的唯一方法是从堆栈顶部展开。
我发现遗漏了一些尽可能少做新事物的重要原因:
运算符 new 具有不确定的执行时间
调用 new
可能会也可能不会导致操作系统为您的进程分配一个新的物理页面,如果您经常这样做,这可能会非常慢。或者它可能已经准备好合适的内存位置,我们不知道。如果您的程序需要具有一致且可预测的执行时间(例如在实时系统或游戏/物理模拟中),您需要在时间关键循环中避免 new
。
运算符 new 是隐式线程同步
是的,你听到了,你的操作系统需要确保你的页表是一致的,因此调用 new
会导致你的线程获取一个隐式互斥锁。如果您一直在从许多线程调用 new
,那么您实际上是在序列化您的线程(我已经使用 32 个 CPU 完成了此操作,每个 CPU 都在 new
上获得几百个字节,哎呀!这是一个要调试的皇家皮塔)
其他答案已经提到了其他答案,例如速度慢、碎片化、容易出错等。
mlock()
或类似的东西。这是因为系统可能内存不足,并且没有可用于堆栈的现成物理内存页面,因此操作系统可能需要交换或将一些缓存(清除脏内存)写入磁盘,然后才能继续执行。
C++17 之前:
因为即使您将结果包装在智能指针中,它也容易发生微妙的泄漏。
考虑一个“细心”的用户,他记得将对象包装在智能指针中:
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:
这不再是一个问题:C++17 对这些操作的顺序施加了约束,在这种情况下,确保每次调用 new()
之后必须立即构造相应的智能指针,没有之间的其他操作。这意味着,在调用第二个 new()
时,可以保证第一个对象已经被包装在其智能指针中,从而防止在引发异常时发生任何泄漏。 罢工>
Barry in another answer 提供了对 C++17 引入的新评估顺序的更详细说明。 罢工>
感谢 @Remy Lebeau 指出这仍然在 C++17 下是一个问题(尽管不太严重):shared_ptr
构造函数可能无法分配其控制块并抛出,在这种情况下传递给它的指针不会被删除。
解决方案:使用make_shared
。
new
成功并且随后的 shared_ptr
构造失败,仍然可能发生泄漏。 std::make_shared()
也可以解决这个问题
shared_ptr
构造函数为存储共享指针和删除器的控制块分配内存,所以是的,理论上它可以引发内存错误。只有复制、移动和别名构造函数是不抛出的。 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()
不应尽可能little 使用。应该尽可能小心使用它。并且应该根据实用主义的要求尽可能频繁地使用它。
在堆栈上分配对象,依赖于它们的隐式销毁,是一个简单的模型。如果对象的所需范围适合该模型,则无需使用 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
不仅会导致内存碎片,而且还会使分配算法复杂化,并且绝对会破坏在某些情况下的表现。
这就是创建 memory pools 来解决的问题,它可以减轻传统堆实现的固有缺点,同时仍允许您根据需要使用堆。
不过,最好还是完全避免这个问题。如果你可以把它放在堆栈上,那么就这样做。
我认为张贴者的意思是 You do not have to allocate everything on the
heap
而不是 stack
。
基本上,对象是在堆栈上分配的(当然,如果对象大小允许的话),因为堆栈分配的成本很低,而不是基于堆的分配,这涉及分配器的相当多的工作,并且增加了冗长,因为你必须管理在堆上分配的数据。
我倾向于不同意使用新的“太多”的想法。尽管原始海报将 new 与系统类一起使用有点荒谬。 (int *i; i = new int[9999];
?真的吗?int i[9999];
更清晰。)我认为 是引起评论者注意的原因。
当您使用系统对象时,很少需要对完全相同的对象进行多次引用。只要价值相同,那才是最重要的。并且系统对象通常不会占用太多内存空间。 (每个字符一个字节,在一个字符串中)。如果他们这样做了,库的设计应该考虑到内存管理(如果它们写得很好)。在这些情况下,(除了他的代码中的一两个新闻),new 实际上毫无意义,只会带来混乱和潜在的错误。
但是,当您使用自己的类/对象(例如原始海报的 Line 类)时,您必须自己开始考虑内存占用、数据持久性等问题。在这一点上,允许对同一个值的多次引用是无价的——它允许像链表、字典和图形这样的结构,其中多个变量不仅需要具有相同的值,而且还需要引用完全相同的对象 em> 在内存中。但是, Line 类没有任何这些要求。所以原始发帖人的代码实际上完全不需要new
。
When you're working with your own classes/objects
...您通常没有理由这样做!一小部分问题是关于熟练编码人员的容器设计细节。与此形成鲜明对比的是,令人沮丧的比例是关于不知道标准库存在的新手的困惑 - 或者在“编程”“课程”中积极分配糟糕的任务,导师要求他们毫无意义地重新发明轮子 - 在他们了解轮子是什么以及为什么它起作用之前。通过促进更抽象的分配,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[]>()
)。
两个原因:
在这种情况下是不必要的。您正在使您的代码不必要地变得更加复杂。它在堆上分配空间,这意味着你以后要记得删除它,否则会导致内存泄漏。
许多答案都涉及各种性能考虑。我想解决令 OP 感到困惑的评论:
不要像 Java 程序员那样思考。
实际上,在 Java 中,如 this question 的答案中所解释的,
首次显式创建对象时使用 new 关键字。
但在 C++ 中,T
类型的对象是这样创建的:T{}
(或 T{ctor_argument1,ctor_arg2}
用于带参数的构造函数)。这就是为什么您通常没有理由要使用 new
。
那么,为什么它一直被使用呢?嗯,有两个原因:
您需要创建许多值,其数量在编译时是未知的。由于通用机器上 C++ 实现的限制 - 通过分配太多空间以常规方式创建值来防止堆栈溢出。
现在,除了您引用的评论所暗示的内容之外,您应该注意,即使上面的这两种情况也得到了很好的覆盖,而您不必自己“求助”使用 new
:
您可以使用标准库中的容器类型,这些容器类型可以容纳运行时可变数量的元素(如 std::vector)。
您可以使用智能指针,它为您提供类似于 new 的指针,但要确保在“指针”超出范围时释放内存。
因此,避免显式 new
和 delete
:Guideline R.11 是 C++ 社区编码指南中的官方项目。
核心原因是堆上的对象总是比简单的值更难使用和管理。编写易于阅读和维护的代码始终是任何认真的程序员的首要任务。
另一种情况是我们使用的库提供了值语义并且不需要动态分配。 Std::string
就是一个很好的例子。
然而,对于面向对象的代码,使用指针——这意味着使用 new
预先创建它——是必须的。为了简化资源管理的复杂性,我们有几十种工具让它尽可能简单,比如智能指针。基于对象的范式或通用范式假定价值语义并且需要更少或不需要new
,正如其他地方的海报所述。
传统的设计模式,尤其是 GoF 书中提到的那些,大量使用 new
,因为它们是典型的 OO 代码。
For object oriented code, using a pointer [...] is a must
:废话。如果您仅通过引用一个小子集来贬低“OO”,那么 polymorphism - also 废话:引用也可以。 [pointer] means use new to create it beforehand
:尤其是废话:可以将引用或指针用于自动分配的对象和多态使用; 看着我。 [typical OO code] use new a lot
:也许在一些旧书中,但谁在乎?任何模糊的现代 C++ 都尽可能避开 new
/raw 指针 - & 绝不这样做会减少 OO
new
是新的 goto
。
回想一下为什么 goto
如此受辱:虽然它是一种功能强大的低级流控制工具,但人们经常以不必要的复杂方式使用它,从而使代码难以遵循。此外,最有用和最容易阅读的模式被编码在结构化编程语句中(例如 for
或 while
);最终的结果是 goto
是适当方式的代码相当少见,如果你想写 goto
,你可能做的很糟糕(除非你真的知道什么你正在做的)。
new
类似 - 它通常用于使事情变得不必要的复杂和难以阅读,并且可以将最有用的使用模式编码为各种类。此外,如果您需要使用任何还没有标准类的新使用模式,您可以编写自己的类来对它们进行编码!
我什至会争辩说,new
比 goto
更更糟糕,因为需要将 new
和 delete
语句配对。
与 goto
一样,如果您认为需要使用 new
,那么您可能做错了 — 尤其是如果您在一个类的实现之外这样做,该类的生活目的是封装您需要的任何动态分配做。
new
的构造示例可能会有所帮助。
以上所有正确答案的另一点,这取决于您正在执行哪种编程。例如,在 Windows 中开发内核 -> 堆栈受到严重限制,您可能无法像在用户模式下那样处理页面错误。
在这样的环境中,新的或类似 C 的 API 调用是首选,甚至是必需的。
当然,这只是规则的一个例外。
new
在堆上分配对象。否则,将在堆栈上分配对象。查找 the difference between the two。
std::vector
使用堆栈和堆内存)。您尚未回答实际提出的问题:为什么我们希望尽量减少 new
的使用。
Monster
死亡时,它会向World
吐出Treasure
。在它的Die()
方法中,它将宝藏添加到世界中。它必须使用world->Add(new Treasure(/*...*/))
来保存它死后的宝藏。替代方案是shared_ptr
(可能是过度杀伤)、auto_ptr
(所有权转移的语义不佳)、按值传递(浪费)和move
+unique_ptr
(尚未广泛实施)。