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


272

我使用C ++已有很短的时间了,而且我一直在想new关键字。简而言之,我应该使用它吗?

1)使用关键字...

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

2)没有关键字...

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

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

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

我应该使用哪种方法?

更新1:

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

更新2:

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

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

这有助于防止内存泄漏,因为您始终必须将删除项放在某个位置(例如,将其剪切并粘贴到析构函数或其他方法时)。


6
简短的答案是,当您可以使用它时,请使用简短的版本。:)
杰夫

11
与总是编写相应的删除使用STL容器和智能指针(如std::vector和)相比,这是一种更好的技术std::shared_ptr。这些包装给您的呼叫new以及delete为您服务的呼叫,因此您泄漏内存的可能性更低。例如,问问自己:您是否始终记得在delete可能引发异常的地方放置对应的内容?delete手工放入s比您想象的要难。
AshleysBrain

@nbolton回复:更新1-C ++的美丽之处之一是它允许您将用户定义类型存储在堆栈上,而C#之类的垃圾收集lang则迫使您将数据存储在堆上在堆上存储数据比在堆栈上存储数据要消耗更多的资源,因此,除了UDT需要大量内存来存储其数据之外,您应该更喜欢使用堆栈而不是。(这也意味着默认情况下按值传递对象)。一个更好的解决方案是将数组通过引用传递给函数
查尔斯·亚迪斯

Answers:


303

方法1(使用new

  • 免费存储区中为对象分配内存 (这通常与相同)
  • 要求您delete稍后显式显示您的对象。(如果不删除它,可能会造成内存泄漏)
  • 内存一直分配到您分配delete。(即您可以return使用创建的对象new
  • 问题中的示例将泄漏内存,除非指针为deleted;否则,将不执行任何操作。并且应该始终删除它,无论采用哪个控制路径,或者是否引发异常。

方法2(不使用new

  • 堆栈上的对象分配内存(所有局部变量都存放在该堆栈中)通常,堆栈可用的内存较少;如果分配的对象过多,则可能会导致堆栈溢出。
  • delete以后不需要。
  • 超出范围时不再分配内存。(即您不应该return指向堆栈上的对象的指针)

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

一些简单的情况:

  • 如果您不想担心调用delete(以及引起内存泄漏的可能性),则不应该使用new
  • 如果要从函数返回指向对象的指针,则必须使用 new

4
一个nitpick -我相信new运算符从“免费存储”中分配内存,而malloc从“堆”中分配。尽管实际上它们通常是相同的,但不能保证它们是同一件事。参见gotw.ca/gotw/009.htm
Fred Larson

4
我认为您的答案可能会更清楚。(99%的时间,选择很简单。在包装对象上使用方法2,该包装对象在构造函数/析构函数中调用new / delete)
jalf

4
@jalf:方法2是不使用新方法的一种方法:-/在任何情况下,使用方法2(没有使用新方法的一种方法),您编写的代码很多时候会更简单(例如,处理错误情况)
Daniel LeCheminant 2009年

另一个挑剔...您应该更加清楚,即使面对异常,尼克的第一个示例也会泄漏内存,而第二个示例不会泄漏内存。
Arafangion

4
@ Fred,Arafangion:感谢您的见解;我已将您的评论纳入答案。
Daniel LeCheminant 09年

118

两者之间有重要区别。

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

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

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

现在,在堆栈上分配的所有内容都具有“自动”存储持续时间(您实际上可以将变量声明为auto,但是如果未指定其他存储类型,则这是默认设置,因此实际上并没有使用该关键字,但是在这里来自)

自动存储持续时间意味着确切地说,变量的持续时间是自动处理的。相比之下,堆上分配的所有内容都必须由您手动删除。这是一个例子:

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

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

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

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

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

bar在堆上实例?它可能仍然存在。没有人愿意删除它,因此我们泄漏了内存。

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

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

该原理称为RAII(资源获取就是初始化),它可以扩展到必须获取和释放的任何资源。(网络套接字,文件,数据库连接,同步锁)。所有这些都可以在构造函数中获取,然后在析构函数中释放,因此可以确保您获取的所有资源将再次释放。

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


2
“没有分配给new的所有东西都放在堆栈上”不在我使用的系统中...通常将初始化(和取消初始化)的全局(静态)数据放在自己的段中。例如,.data,.bss等...链接器段。迂腐,我知道...

当然,您是对的。我不是真的在考虑静态数据。我的坏,当然。:)
杰夫

2
为什么在堆栈上分配的任何内容都必须具有恒定的大小?
user541686

并非总是如此,有几种方法可以规避它,但是在一般情况下可以,因为它在堆栈上。如果它位于堆栈的顶部,则可以调整其大小,但是一旦将其他内容推入堆栈的顶部,它就会被“围墙”,两侧被对象包围,因此无法真正调整其大小。是的,说它总是必须有一个固定的大小有点简化,但是它传达了基本思想(我不建议您使用C函数,因为它会使您在堆栈分配方面过于创新)
jalf

14

我应该使用哪种方法?

这几乎永远不会取决于您的键入首选项,而是取决于上下文。如果需要将对象放置在几个堆栈中,或者如果该对象对于堆栈而言太重,则可以在免费存储区中分配它。另外,由于您正在分配对象,因此您还负责释放内存。查找delete运算符。

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


10

如果您使用C ++编写,则可能是出于性能方面的考虑。使用new存储区和免费存储区比使用堆栈要慢得多(尤其是在使用线程时),因此仅在需要时才使用它。

就像其他人所说的,当您的对象需要驻留在函数或对象范围之外,对象确实很大或在编译时不知道数组的大小时,您需要新的对象。

另外,请尽量避免使用Delete。而是将新包装为智能指针。让智能指针调用为您删除。

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


10

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

相反,您应该使用智能指针,例如std::unique_ptrstd::make_unique(或更不经常使用std::shared_ptrand std::make_shared)。这样,您不必担心内存泄漏。而且,即使你是更先进的,最好的做法通常是封装你使用自定义的方式newdelete成小类(如自定义智能指针),专门只是对象生命周期的问题。

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


有趣的是,随着时间的流逝,答案可能会如何变化;)
Wolf


2

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


1

如果仅在单个函数的上下文中使用变量,则最好使用堆栈变量,即选项2。正如其他人所说,您不必管理堆栈变量的生存期-它们是构造的,自动销毁。而且,相比之下,在堆上分配/取消分配变量的速度很慢。如果经常调用函数,那么使用堆栈变量与堆变量相比,您将获得巨大的性能提升。

就是说,有几个明显的例子,其中堆栈变量不足。

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

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


1

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


0

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

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

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


-1

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.