为什么需要使用new和delete?

6

我刚接触C++,我想知道为什么我需要使用new和delete?这可能会引起问题(内存泄漏),我不明白为什么我不能只是使用初始化变量而不用new运算符。有人能向我解释一下吗?我很难在Google上找到针对这个特定问题的答案。


11
Stack Overflow是答案(双关语)。在C++中,你的栈比堆要小得多。了解栈和堆内存之间的区别。 - tgmath
1
@tgmath:你可以使用标准容器来存储对象,它们不会占用太多的堆栈空间。此外,你还可以使用全局容器来管理对象。 - 6502
我不明白为什么我不能只是使用初始化变量而不使用new运算符。在你有充分的理由使用堆之前,你可以并且应该使用栈内存:"int i=5;" 是完全有效的。 - Adrian Maire
@tgmath解释了你的问题是为什么使用动态内存。有人可能将你的问题理解为为什么使用手动动态内存。在现代C++中,你可以使用智能指针等等方式,再也不需要使用new或delete了,但仍然能够使用动态内存。 - Neil Kirk
但这是完全错误的。最常见的情况是动态分配的生命周期由程序逻辑确定,并且没有智能指针可以处理它(也不需要)。 - James Kanze
显示剩余2条评论
8个回答

6

出于历史和效率原因,C++(以及C)的内存管理是显式和手动的。

有时候,您可能会在调用堆栈上分配内存(例如使用VLAalloca(3))。然而,这并不总是可能的,因为

  1. 堆栈大小有限(取决于平台,几千字节或几兆字节)。
  2. 内存需求并不总是FIFOLIFO。有时候您需要分配内存,这些内存在执行期间很快就会被释放或变得无用,特别是因为它可能是某个函数的结果(调用者 - 或其调用者 - 会释放该内存)。
你绝对应该了解垃圾回收动态内存分配。在一些语言(Java,Ocaml,Haskell,Lisp等)或系统中,提供了GC,负责释放无用(更准确地说是不可达)数据的内存。还要了解弱引用。请注意,大多数GCs需要扫描调用堆栈以获取本地指针。 请注意,拥有相当高效的垃圾收集器是可能的,但很困难(通常不适用于C ++)。对于某些程序,使用具有生成复制GC的Ocaml比使用显式内存管理的等效C ++代码更快。 显式管理内存的优点(在C ++中很重要)是您不必为不需要的东西付费。它的缺点是给程序员带来更多负担。
在C或C++中,有时您可能考虑使用Boehm的保守垃圾收集器。在C++中,您可能需要使用自己的分配器,而不是默认的std::allocator。还应了解智能指针引用计数std::shared_ptrstd::unique_ptrstd::weak_ptr以及RAII惯用语和三法则(在C++中成为五法则)。最近的建议是避免显式使用newdelete(例如通过使用标准容器和智能指针)。

请注意,在管理内存时最困难的情况是任意的、可能是循环的引用图。

在Linux和一些其他系统上,valgrind是一个有用的工具,可以用来查找内存泄漏


1
垃圾回收绝对是一个有用的工具,而且在C++中也可以使用(Boehm收集器)。然而,与您提到的其他语言不同,C++具有值语义,因此动态分配内存的使用受到限制,通常仅用于寿命取决于程序逻辑的对象。并且垃圾回收不能解决生命周期问题(即使在具有垃圾回收的语言中,您仍然经常需要一个特殊的“dispose”函数或类似的东西,必须显式调用)。在C++中,垃圾回收的主要动机是安全性。 - James Kanze

3
使用栈分配内存的替代方法会带来麻烦,因为栈大小通常限制在Mb数量级,并且您将获得大量的值副本。您还将遇到在函数调用之间共享栈分配数据的问题。
有一些替代方法:使用std::shared_ptr(C++11及以上版本)将在共享指针不再使用时为您执行删除操作。共享指针实现利用了称为RAII的可怕缩写技术。我明确提到它,因为大多数资源清理惯用语都是基于RAII的。您还可以利用C++标准模板库中提供的全面数据结构,这些结构消除了与显式内存管理过度接触的需要。
但是,正式地说,每个new必须与一个delete相平衡。同样适用于new[]和delete[]。

对于std::shared_ptr,它在许多情况下(但不是所有情况)使生活变得更加轻松。我也发现std::unique_ptr非常有用。 - jlh
1
std :: unique_ptr不同,std :: shared_ptr并不真正实现RAII;它实现了一种非常特定的生命周期管理策略,这只有在某些情况下才是合适的。 - James Kanze
1
@jiandingzhe 它只是一种工具,就像其他任何工具一样。我不讨厌它;在适当的时候我会使用它。我讨厌那些不知道是否适合使用它,却无论什么事情都建议使用它的人。 - James Kanze

2

在许多情况下,其实不需要使用newdelete,可以只使用标准容器,把分配/释放管理交给它们。

你可能需要显式地使用分配的原因之一是对于需要重视身份的对象(即它们不仅仅是可以复制的值)。

例如,如果你有一个 GUI “窗口”对象,那么复制它可能没有意义,因此你基本上排除了所有标准容器(它们设计用于可以复制和赋值的对象)。在这种情况下,如果对象需要在创建它的函数之后继续存在,则最简单的解决方案可能就是将其显式地分配到堆上,可能使用智能指针以避免泄漏或删除后使用。

在其他情况下,可能很重要避免复制,不是因为复制是非法的,而是因为效率不高(大对象),显式处理实例生命周期可能是更好(更快)的解决方案。

另一种可能最佳的显式分配/释放选项是复杂数据结构,它们无法由标准库表示(例如每个节点也是双向链表的树)。


2
现代C++风格通常不鼓励在专门的资源管理代码之外显式调用newdelete,这并不是因为堆栈/自动存储足够使用,而是因为RAII智能资源所有者(无论是容器、共享指针还是其他什么)几乎使所有直接内存操作都变得不必要。由于内存管理问题往往容易出错,这使得你的代码更加健壮、易于阅读,并且有时会更快(因为高级资源所有者可以使用你可能不会在所有地方都费心使用的技术)。
这体现在零规则上:不编写析构函数、复制/移动分配、复制/移动构造函数。在智能存储中存储状态,并让它为你处理。
当你自己编写智能内存拥有类时,以上内容都不适用。然而,这是一件很少需要做的事情。它还需要C++14(对于make_unique)来摆脱调用new的倒数第二个借口。
现在,自由存储仍然被使用,只是在上述风格下不直接使用。自由存储(也称为堆)是必需的,因为自动存储(也称为堆栈)仅支持真正简单的对象生存期规则(基于作用域、编译时确定的大小和计数、FILO顺序)。由于运行时大小和计数的数据很常见,而且对象生存期通常并不那么简单,大多数程序都使用自由存储。有时在堆栈上复制一个对象就足以使简单的生命周期问题减少,但在其他情况下,标识很重要。
最后一个原因是堆栈溢出。在某些C++实现中,堆栈/自动存储的大小受到严格限制。更重要的是,在将太多东西放入其中时,很少有可靠的故障模式。通过将大型数据存储在自由存储中,我们可以减少堆栈溢出的可能性。

1
第一句话绝对是错误的。无论动态分配发生在何处或如何发生,任何C++风格都会(或应该)不赞成不必要的动态分配。对于其生命周期取决于程序逻辑的对象(动态分配最常见的原因),显式的newdelete(通常是delete this,因为对象本身会对事件做出反应)确实是唯一的方法。 - James Kanze
@jamezkanze 将数据存储在 unique_ptr 中。不再需要时使用 .reset()。这清楚地表明了哪个指针拥有,使泄漏几乎不可能(包含对象必须泄漏才能泄漏资源,如果递归应用,则包含对象未泄漏),并且几乎没有开销(仅在销毁和清零时可能进行冗余检查,如果编译器无法证明它是冗余的)。这可能不是您的风格,但我并没有声称每种风格都是正确的。 - Yakk - Adam Nevraumont
@jameskanze 你能在本地证明没有泄漏吗?你可以获得关于指针拥有的类型强制文档吗?如果你从未泄漏过任何东西,而且在修改代码时也从未泄漏过任何东西,那么它就毫无意义。实际上,泄漏是很常见的,而这种风格几乎不增加复杂性,同时消除了整个类别的错误。 - Yakk - Adam Nevraumont
显然,你无法“局部地”证明什么,因为“局部地”不包括所需的生命周期。但是,使用通常的智能指针也并不能真正证明任何事情;我发现shared_ptr经常是泄漏的源头(因为存在循环引用)。而且_没有_指针是所有权的。否则:泄漏非常非常罕见,因为我们只动态分配需要程序逻辑定义生命周期的东西。在很大程度上,动态分配的对象是自包含的,并且根本没有“所有者”,除非是暂时的(例如通过交易)。 - James Kanze
@james 我在谈论 unique_ptr,而不是 shared_ptr。它们都是智能指针,因为它们像指针一样运作并管理生命周期,但是它们在其他方面非常不同。shared_ptr 有开销,并且通常使得关于生命周期的推理更加困难。unique_ptr 几乎没有开销,并且使得关于生命周期的推理更加容易。混淆这两者意味着你应该更多地尝试使用 unique_ptr:我永远不会推荐全面地使用 shared_ptr - Yakk - Adam Nevraumont
显示剩余2条评论

2

首先,如果你不需要动态分配内存,就不要使用它。

最常需要动态分配内存的原因是对象的生命周期由程序逻辑而非词法作用域决定。 newdelete 运算符旨在支持显式管理的生命周期。

另一个常见原因是“对象”的大小或结构在运行时确定。 对于简单情况(如数组等),有标准类(std::vector)可以为您处理,但对于更复杂的结构(例如图和树),您必须自己处理。(通常的技术是创建一个表示图或树的类,并让它管理内存。)

还有一种情况,即对象必须是多态的,实际类型直到运行时才知道。(在最简单的情况下,有一些棘手的方法可以处理此问题,而无需动态分配内存,但通常情况下,您需要动态分配内存。)在这种情况下,可能需要使用 std::unique_ptr 来处理 delete,或者如果对象必须共享,则使用 std::shared_ptr(尽管通常必须共享的对象属于上述第一类,因此智能指针并不合适)。

可能还有其他原因,但这些是我最常遇到的三个原因。


1

只有在简单的程序中,您才能预先知道需要使用多少内存。通常情况下,您无法预见需要使用多少内存。

然而,使用现代C++11,您通常依赖于标准库,如vectormap进行内存分配,并且使用智能指针可以帮助您避免内存泄漏,因此您不需要手动显式地使用newdelete


1
使用 new 时,对象存储在 Heap 中,直到手动删除为止。但是如果不使用 new,则对象将进入 Stack,并在超出范围时自动销毁。 Stack 的大小是固定的,因此如果没有可用块来分配新对象,则会导致 Stack Overflow。当大量嵌套函数被调用或存在无限递归调用时,通常会发生这种情况。如果当前 heap 的大小过小而不能容纳新的内存,则操作系统可以向堆添加更多内存。

1
另一个原因可能是您正在使用具有 C 风格接口的外部库或 API 进行显式调用。在这种情况下设置回调通常意味着必须在回调中提供和返回上下文数据,并且这样的接口通常仅提供“简单”的 void* 或 int*。为此类操作分配新对象或结构体是合适的(如果需要,您可以在回调中稍后删除它)。

我见过很多人用static对象来解决这个问题。但是,通过void*传递的方式需要小心谨慎。常见的做法是将回调函数强制转换为Base*,以便调用虚成员函数;如果被调用者直接将new Derived传递给void*,则会导致未定义的行为。 - James Kanze
静态变量不是线程安全的。同时设置多个回调也很常见。 - Martin James
我并没有说这是一个好主意;我只是说我见过很多人这样做。例如,在pthread_create的情况下,这显然会带来问题。(实际上,你可以通过使用pthread_cond_t来解决这个问题,只有在子线程将值复制到本地变量后才能在起始线程中前进。例如,Boost线程就是这样做的,以确保您可能已经传递给构造函数的任何临时对象的生命周期。) - James Kanze

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接