ChatGPT解决这个技术问题 Extra ChatGPT

什么时候应该在 C++ 中使用 new 关键字?

我使用 C++ 有一段时间了,我一直想知道 new 关键字。简单地说,我应该使用它,还是不使用它?

使用新关键字...

    MyClass* myClass = new MyClass();
    myClass->MyField = "Hello world!";

如果没有 new 关键字...

    MyClass myClass;
    myClass.MyField = "Hello world!";

从实现的角度来看,它们似乎没有什么不同(但我确信它们是不同的)......但是,我的主要语言是 C#,当然第一种方法是我习惯的。

困难似乎在于方法 1 更难与 std C++ 类一起使用。

我应该使用哪种方法?

更新1:

我最近将new 关键字用于heap 内存(或free store)用于超出范围(即从函数返回)的大型数组。在我使用堆栈之前,这会导致一半元素在范围之外损坏,切换到堆使用可确保元素完好无损。耶!

更新 2:

我的一个朋友最近告诉我,使用 new 关键字有一个简单的规则;每次输入 new 时,输入 delete

    Foobar *foobar = new Foobar();
    delete foobar; // TODO: Move this to the right place.

这有助于防止内存泄漏,因为您总是必须将删除放在某处(即,当您将其剪切并粘贴到析构函数或其他位置时)。

简短的回答是,当你可以摆脱它时,使用简短的版本。 :)
比总是编写相应的删除更好的技术 - 使用 STL 容器和智能指针,如 std::vectorstd::shared_ptr。它们为您包装了对 newdelete 的调用,因此您更不可能泄漏内存。例如,问问自己:您是否总是记得在可能引发异常的任何地方都放置相应的 delete?手动放入 delete 比您想象的要难。
@nbolton 回复:更新 1 - C++ 的优点之一是它允许您将用户定义的类型存储在堆栈上,而像 C# 这样的垃圾收集语言会强制您将数据存储在堆上。在堆上存储数据比在堆栈上存储数据消耗更多的资源,因此您应该更喜欢堆栈而不是堆,除非您的 UDT 需要大量内存来存储其数据。 (这也意味着默认情况下对象是按值传递的)。解决您的问题的更好方法是通过引用将数组传递给函数。

D
Daniel LeCheminant

方法 1(使用 new

为空闲存储上的对象分配内存(这通常与堆相同)

要求您稍后显式删除您的对象。 (如果你不删除它,你可能会造成内存泄漏)

内存保持分配状态,直到您将其删除。 (即,您可以返回使用 new 创建的对象)

除非删除指针,否则问题中的示例将泄漏内存;并且无论采用哪个控制路径或是否引发异常,都应始终将其删除。

方法 2(不使用 new

为堆栈上的对象分配内存(所有局部变量所在的地方)堆栈可用的内存通常较少;如果你分配了太多的对象,你就有堆栈溢出的风险。

您以后不需要删除它。

超出范围时不再分配内存。 (即您不应该返回指向堆栈上对象的指针)

至于使用哪一个;鉴于上述限制,您选择最适合您的方法。

一些简单的案例:

如果您不想担心调用 delete(以及可能导致内存泄漏),则不应使用 new。

如果你想从一个函数返回一个指向你的对象的指针,你必须使用 new


一个挑剔——我相信 new 运算符从“自由存储”分配内存,而 malloc 从“堆”分配。尽管在实践中它们通常是相同的,但不能保证它们是相同的。请参阅gotw.ca/gotw/009.htm
我认为您的答案可能会更清楚地使用哪个。 (99% 的情况下,选择很简单。在构造函数/析构函数中调用 new/delete 的包装对象上使用方法 2)
@jalf:方法 2 是不使用新方法的方法 :-/ 在任何情况下,使用方法 2(没有新方法的方法)您的代码会更简单(例如处理错误情况)
另一个挑剔...您应该更清楚地表明尼克的第一个示例泄漏内存,而他的第二个示例没有,即使面对异常也是如此。
@Fred,Arafangion:感谢您的洞察力;我已将您的评论纳入答案。
P
Paul

两者之间有一个重要的区别。

未使用 new 分配的所有内容的行为与 C# 中的值类型非常相似(人们经常说这些对象是在堆栈上分配的,这可能是最常见/最明显的情况,但并非总是如此)。更准确地说,不使用 new 分配的对象具有自动存储持续时间 使用 new 分配的所有内容都在堆上分配,并返回指向它的指针,就像 C# 中的引用类型一样。

在堆栈上分配的任何东西都必须有一个常量大小,在编译时确定(编译器必须正确设置堆栈指针,或者如果对象是另一个类的成员,它必须调整另一个类的大小) .这就是 C# 中的数组是引用类型的原因。它们必须是,因为使用引用类型,我们可以在运行时决定请求多少内存。这同样适用于这里。只有具有恒定大小(可以在编译时确定的大小)的数组才能分配自动存储持续时间(在堆栈上)。必须通过调用 new 在堆上分配动态大小的数组。

(这就是与 C# 的任何相似性停止的地方)

现在,在堆栈上分配的任何东西都有“自动”存储持续时间(您实际上可以将变量声明为 auto,但如果没有指定其他存储类型,则这是默认值,因此实际上并未真正使用该关键字,但这是它的来源)

自动存储持续时间意味着它听起来像,变量的持续时间是自动处理的。相比之下,在堆上分配的任何东西都必须由您手动删除。这是一个例子:

void foo() {
  bar b;
  bar* b2 = new bar();
}

此函数创建三个值得考虑的值:

在第 1 行,它在堆栈上声明了一个 bar 类型的变量 b(自动持续时间)。

在第 2 行,它在堆栈上声明了一个 bar 指针 b2(自动持续时间),调用 new,在堆上分配一个 bar 对象。 (动态持续时间)

当函数返回时,会发生以下情况:首先,b2 超出范围(破坏顺序总是与构造顺序相反)。但是 b2 只是一个指针,所以什么也没有发生,它占用的内存被简单地释放了。重要的是,它指向的内存(堆上的 bar 实例)没有被触及。只有指针被释放,因为只有指针具有自动持续时间。其次,b 超出范围,因此由于它具有自动持续时间,因此调用其析构函数并释放内存。

bar堆上的实例呢?它可能还在那里。没有人愿意删除它,所以我们泄露了内存。

从这个例子中,我们可以看到任何具有自动持续时间的东西都保证在超出范围时调用其析构函数。这很有用。但是在堆上分配的任何东西只要我们需要它就可以持续,并且可以动态调整大小,就像数组一样。这也很有用。我们可以使用它来管理我们的内存分配。如果 Foo 类在其构造函数中在堆上分配一些内存,并在其析构函数中删除该内存会怎样。然后我们可以两全其美,保证再次释放的安全内存分配,但没有强制所有内容都在堆栈上的限制。

这几乎就是大多数 C++ 代码的工作方式。以标准库的 std::vector 为例。这通常在堆栈上分配,但可以动态调整大小和调整大小。它通过在必要时在堆上内部分配内存来做到这一点。该类的用户永远不会看到这一点,因此不会泄漏内存或忘记清理您分配的内容。

这个原理叫做RAII(Resource Acquisition is Initialization),它可以扩展到任何必须被获取和释放的资源。 (网络套接字、文件、数据库连接、同步锁)。它们都可以在构造函数中获取,并在析构函数中释放,因此您可以保证您获得的所有资源都会再次被释放。

作为一般规则,永远不要直接从您的高级代码中使用 new/delete。始终将它包装在一个可以为您管理内存的类中,这将确保它再次被释放。 (是的,这条规则可能有例外。特别是,智能指针要求您直接调用 new,并将指针传递给其构造函数,然后由构造函数接管并确保正确调用 delete。但这仍然是一个非常重要的经验法则)


“所有未分配新的东西都放在堆栈上”不在我工作过的系统中......通常初始化(和 uninit。)全局(静态)数据放在它们自己的段中。例如,.data、.bss 等...链接器段。书呆子,我知道...
当然,你是对的。我并没有真正考虑静态数据。我的坏,当然。 :)
为什么在堆栈上分配的任何东西都必须具有恒定的大小?
并非总是如此,有几种方法可以绕过它,但在一般情况下它确实如此,因为它在堆栈上。如果它在堆栈的顶部,那么它可能会调整它的大小,但是一旦其他东西被推到它上面,它就会被“围起来”,被两边的物体包围,所以它不能真正调整大小.是的,说它总是必须有一个固定的大小有点简化,但它传达了基本思想(我不建议搞乱 C 函数,这会让你在堆栈分配方面过于有创意)
b
bitmask

简短的回答是:如果您是 C++ 的初学者,您应该永远自己使用 newdelete

相反,您应该使用智能指针,例如 std::unique_ptrstd::make_unique(或者较少见的 std::shared_ptrstd::make_shared)。这样,您几乎不必担心内存泄漏。即使您更高级,最佳实践通常是将您使用 newdelete 的自定义方式封装到一个专门用于对象生命周期问题的小类(例如自定义智能指针)中.

当然,在幕后,这些智能指针仍在执行动态分配和释放,因此使用它们的代码仍然会产生相关的运行时开销。这里的其他答案已经涵盖了这些问题,以及如何就何时使用智能指针而不是仅在堆栈上创建对象或将它们合并为对象的直接成员做出设计决策,足够好,我不会重复它们。但我的执行摘要是:在某些事情迫使您使用之前,不要使用智能指针或动态分配。


有趣的是,随着时间的推移,答案会如何变化;)
d
dirkgently

我应该使用哪种方法?

这几乎从来不是由您的打字偏好决定的,而是由上下文决定的。如果您需要将对象保留在几个堆栈中,或者如果它对于堆栈来说太重,您可以将它分配到免费存储中。此外,由于您正在分配一个对象,因此您还负责释放内存。查找 delete 运算符。

为了减轻使用免费商店管理的负担,人们发明了 auto_ptrunique_ptr 之类的东西。我强烈建议你看看这些。它们甚至可能对您的打字问题有所帮助;-)


Z
Zan Lynx

如果您使用 C++ 编写代码,您可能是为了性能而编写代码。使用 new 和 free 存储比使用堆栈慢得多(尤其是在使用线程时),所以只在需要时使用它。

正如其他人所说,当您的对象需要位于函数或对象范围之外,对象非常大或者您在编译时不知道数组的大小时,您需要 new 。

另外,尽量避免使用删除。将你的 new 包装成一个智能指针。让智能指针为你调用 delete。

在某些情况下,智能指针不智能。永远不要将 std::auto_ptr<> 存储在 STL 容器中。由于容器内的复制操作,它将过早删除指针。另一种情况是当您有一个非常大的 STL 对象指针容器时。 boost::shared_ptr<> 会产生大量的速度开销,因为它会上下颠簸引用计数。在这种情况下,更好的方法是将 STL 容器放入另一个对象中,并为该对象提供一个析构函数,该析构函数将对容器中的每个指针调用 delete。


v
vartec

如果没有 new 关键字,您会将其存储在 call stack 上。在堆栈上存储过大的变量会导致 stack overflow


M
Matt Davis

如果您的变量仅在单个函数的上下文中使用,则最好使用堆栈变量,即选项 2。正如其他人所说,您不必管理堆栈变量的生命周期 - 它们是构造和自动销毁。此外,相比之下,在堆上分配/释放变量很慢。如果您的函数被调用得足够频繁,那么如果使用堆栈变量与堆变量相比,您将看到巨大的性能提升。

也就是说,有几个明显的情况是堆栈变量不足。

如果堆栈变量具有较大的内存占用,那么您将面临堆栈溢出的风险。默认情况下,the stack size of each thread is 1 MB 在 Windows 上。您不太可能创建大小为 1 MB 的堆栈变量,但您必须记住堆栈利用率是累积的。如果您的函数调用一个函数,该函数调用另一个函数,该函数调用另一个函数......,所有这些函数中的堆栈变量都会占用同一个堆栈上的空间。递归函数会很快遇到这个问题,这取决于递归的深度。如果这是一个问题,您可以增加堆栈的大小(不推荐)或使用 new 运算符在堆上分配变量(推荐)。

另一个更可能的情况是您的变量需要“生存”在您的函数范围之外。在这种情况下,您将在堆上分配变量,以便可以在任何给定函数的范围之外访问它。


T
Timo Geusch

简单的答案是肯定的 - new() 在堆上创建一个对象(不幸的副作用是您必须管理它的生命周期(通过显式调用它的 delete),而第二种形式在当前堆栈中创建一个对象范围,并且该对象将在超出范围时被销毁。


i
itsmatt

您是将 myClass 从函数中传递出来,还是期望它存在于该函数之外?正如其他人所说,当您不在堆上分配时,这完全与范围有关。当您离开该功能时,它会消失(最终)。初学者犯的经典错误之一是试图在函数中创建某个类的本地对象并返回它而不在堆上分配它。我记得在我早期使用 C++ 时调试过这种事情。


e
einpoklum

C++ 核心指南 R.11:避免显式使用 new 和 delete。

自从写了这个问题的大多数答案以来,情况发生了很大变化。具体来说,C++ 作为一种语言已经进化,标准库现在更加丰富。为什么这很重要?由于两个因素的结合:

使用 new 和 delete 有潜在的危险:如果你没有严格遵守删除分配的所有不再使用时分配的所有内容,那么内存可能会泄漏;并且永远不要删除当前未分配的内容。

标准库现在提供了封装新调用和删除调用的智能指针,因此您不必自己管理空闲存储/堆上的分配。标准库和其他地方的其他容器也是如此。

如链接文档所示,这已演变成 C++ 社区编写更好的 C++ 代码的“核心指南”之一。当然,这条规则也有例外:有人需要编写那些确实使用 newdelete 的封装类;但那个人很少是你自己。

添加到@DanielSchepler 的有效答案:


g
greyfade

第二种方法在堆栈上创建实例,以及声明的内容 int 和传递给函数的参数列表。

第一种方法为堆栈上的 指针 腾出空间,您已将其设置为内存中已在堆上分配新 MyClass 的位置 - 或空闲存储。

第一种方法还要求您delete 使用 new 创建的内容,而在第二种方法中,当类超出范围时(通常是下一个右大括号),它会自动销毁并释放。


R
RAGNO

简短的回答是肯定的,“new”关键字非常重要,因为当您使用它时,对象数据存储在堆上而不是堆栈上,这是最重要的!