C++ / C# / Java中,new关键字是否总是在堆上分配内存?

16
我的理解一直以来都是,无论是C ++、C#还是Java,在使用new关键字创建对象时,它都会在堆上分配内存。我认为只有引用类型(类)才需要new,原始类型(int、bool、float等)永远不使用new并且总是存储在堆栈中(除非它们是使用new实例化的类的成员变量)。但是,我一直在阅读信息,这让我对这个长期存在的假设产生了怀疑,至少对于Java和C#来说是这样。
例如,我刚刚注意到,在C#中,new操作符可以用于初始化值类型,请参见此处。这是否是规则的例外,是语言的辅助功能,如果是的话,还有哪些例外情况?
请问可以有人澄清一下吗?

5
你能告诉我们,你正在阅读什么书或资料导致了这些疑惑吗? - Haris Hasan
我在原始问题中添加了一篇文章的引用。 - Alex
9个回答

30

我曾认为只有引用类型(类)才需要使用 new,而基本类型(int、bool、float等)从不使用 new。

C++ 中,如果你想的话,也可以在堆上分配基本类型:

int* p = new int(42);

如果你想要一个共享计数器,这很有用,例如在实现shared_ptr<T>时。

此外,在C++中使用类时并不强制使用new:

void function()
{
    MyClass myObject(1, 2, 3);
}

这将在堆栈上分配myObject。请注意,在现代C ++中很少使用new

此外,在C ++中,您可以重载operator new(全局或特定于类),因此即使您说new MyClass,对象也不一定会分配在堆上。


6
谢谢。在现代C++中,有什么替代"new"的方法? - Alex
9
在C++中,“new”有许多不同的用法... 你可以使用“std::string”代替“new char[n]”,使用“std::vector<MyClass>”代替“new MyClass[n]”。如果你想共享对象,可以使用“auto p = make_shared<Class>()”来代替“MyClass* p = new MyClass”等。 - fredoverflow
我认为在现代C++中delete很少使用。我们可能会在某些情况下使用shared_ptrunique_ptrnew - Deqing
2
@Deqing 为什么?我们有make_sharedmake_unique - fredoverflow
@fredoverflow 当您不希望 weak_ptr 保留已删除的共享计数时。 - Deqing

10

我对Java并不是十分了解(并且似乎很难找到有关它的文档)。

在C#中,new调用构造函数并返回一个新对象。如果它是值类型,则分配在堆栈上(例如局部变量)或堆上(例如装箱对象,引用类型对象的成员)。如果它是引用类型,则总是放在堆上,并由垃圾回收器管理。有关更多详细信息,请参见http://msdn.microsoft.com/en-us/library/fa0ab757(v=vs.80).aspx

在C ++中,"new表达式"返回具有动态存储持续时间的对象指针(即,您必须自己销毁该对象)。在C ++标准中,没有提到堆(具有此含义),获得这样的对象的机制是实现定义的。


4
在 C# 中,“new”并不总是返回指向堆上对象的引用,也不表示与 GC 有关。 - Marc Gravell
2
令人烦恼的是,在C#中你可以说 int i = new int(),但你不能说 new int(1) -- 这是一种无力的伪构造,对我来自C ++ 的人来说令人困惑... 我目前正在努力了解C#,但这些事情让我感觉并没有得到充分的考虑。 - Kerrek SB
@Kerrek SB:new int() 给你一个“自动存储”整数的事实真的很让我烦恼。这与任何东西都不一致。 - Alexandre C.
1
在解释C#的行为时,链接到MSDN会被视为不良做法,但是在解释C++的行为时,可以参考标准文档。如果你查看了C#标准,你就不会看到任何与new操作符相关联的“堆栈”提及。 - stakx - no longer contributing
@stakx:C#的事实上权威参考资料是msdn。由于缺乏共识实现,C++的权威参考资料是标准文档。但毫无疑问,你是正确的:微软无法适当地记录他们的语言。 - Alexandre C.
1
@Alex,我同意你的最后一句话。不过,在这种情况下,C#标准更有用(在我看来),因为它使用了正确的思考语言的方式;MSDN则建议了一种不恰当的思考方式。希望-1不会显得太严厉,这只是出于良好争论的精神。 - stakx - no longer contributing

8
我的理解一直以来都是,无论是C++、C#还是Java,在使用new关键字创建对象时,它会在堆上分配内存。
您的理解是错误的:
  • new在不同的编程语言中可能有不同的工作方式,即使这些语言表面上相似。不要让C#、C++和Java的相似语法误导您!

  • "堆"和"栈"这两个概念(在内部内存管理的上下文中理解)并不适用于所有编程语言。可以说,这两个概念更多地是实现细节,而不是编程语言的官方规范。

    (据我所知,至少对于C#和C++是正确的。我不知道Java。)

    它们是如此广泛的实现细节的事实并不意味着您应该依赖于这种区别,甚至不需要知道它!(然而,我承认我通常发现了解内部"工作原理"是有益的。)

我建议您不要过多地担心这些概念。你需要正确理解的重要事情是理解一种语言的语义;例如,对于C#或任何其他.NET语言,引用类型和值类型语义的区别。

示例:C#规范中关于运算符new的说明:

请注意,ECMA发布的C#规范(第4版)的以下部分没有提到任何"堆"或"栈":

14.5.10 new运算符

new运算符用于创建类型的新实例。[...]

new运算符意味着创建类型的一个实例,但不一定意味着动态分配内存。特别地,值类型的实例除了它们所在的变量之外不需要额外的内存,并且当使用new来创建值类型的实例时不会发生动态分配。

相反,它谈论了"动态分配内存",但那并不是同一回事:您可以在堆、栈或任何其他地方(例如硬盘驱动器)上动态分配内存。

然而,它确实说到值类型的实例是就地存储的,这正是值类型语义的全部内容:值类型实例在赋值期间被复制,而引用类型实例被引用/“别名”。这才是需要理解的重要事情,而不是"堆"或"栈"!


4
请将以下内容建议给问我C#中堆栈问题的面试官们:请向面试官建议这个想法:) - Alex

3
在C#中,一个class总是存在于堆上。一个struct可以存在于堆或栈上:
  • 变量(除了捕获和迭代器块)以及在栈上的自身结构体字段存储在栈上
  • 捕获、迭代器块、堆上的某些东西的字段以及数组中的值存储在堆上,"boxed"值也存储在堆上

1
愚蠢的问题:C#(语言)是否实际上有“堆栈”和“堆”的概念?我已经查看了标准(第一版),但没有找到任何相关参考,OP链接的微软网站上也没有。C++绝对不会规定任何特定的内存实现。 - Kerrek SB
3
Eric Lippert 的博客 中选取相关文章:The Stack Is An Implementation Detail, Part 1 和 *Part 2*。 - stakx - no longer contributing
1
@stakx:谢谢!Alex:这有点令人担忧。为什么微软要费尽力气推动标准,如果想法只局限于他们一个公司的话?难道没有讨论抽象的C#的价值吗?(我猜SO没有一个专门的“MS-C#”标签...) - Kerrek SB
2
@Alexandre,Eric Lippert(我相信他知道自己在说什么)在他的博客文章中提到,不幸的是MSDN经常明确地使用“堆栈”这些术语,因为(如果我没记错的话)相关规范实际上并没有要求这样做。它们应该被视为实现细节。请参见我链接的文章。 - stakx - no longer contributing
@Alex:如果我表达不清楚,我很抱歉。我认为这是一个非常遗憾的混淆,我完全同意Lippert的文章,更好地描述语言会更好!(事实上,正如他所说,并不是所有值类型都实际上放在堆栈上!) - Kerrek SB
显示剩余2条评论

2
(关于Java)您说的是正确的 - 原始类型被分配在堆栈上(有例外,例如闭包)。但是,您可能指的是诸如以下对象:
Integer n = new Integer(2);

这指的是一个整数对象,而不是原始的int类型。也许这是你困惑的根源?在这种情况下,n将被分配在堆上。也许你的困惑是由于自动装箱规则引起的?此外,为了更详细地了解自动装箱,请参阅此问题。请查看此答案的评论以了解原始类型在堆上分配的例外情况。

我知道Integer是引用类型,而int是原始类型,但还是谢谢你的帮助。 - Alex
我不知道关于C#,但在Java 6中,类似int myInt = new int()的语句将无法编译。 - Dhruv Gairola
在Java中,如果原始类型是字段,它们不会分配在堆栈上,只有当它们是局部变量时才会分配。在C#中,如果结构体是局部变量或方法参数,它们将分配在堆栈上,并且不会提升到lambda闭包中。我相信还有其他情况会导致值类型存储在堆上,但我现在想不起来了。 - configurator
在C#中,基元类型可以存储在堆上 - 我已经在我的答案中涵盖了这些情况。 - Marc Gravell
哦!迭代器块。它们也将所有本地变量提升到堆中。 - configurator
@Configuration 迭代器块,盒子,数组。 - Marc Gravell

2

Java 6实际上也具有逃逸分析。它在我认为的u14版本中作为一个选项被引入,并在后续更新中成为默认设置。甚至还有一个特定的性能构建(大约在u7左右),其中包含了逃逸分析。 - Arjan Tijms

2
关于c#,请阅读有关值类型的真相。您会发现值类型也可以放在堆上。
此外,这个问题表明引用类型可能会放在栈上。(但目前并未发生)

0
在Java和C#中,我们不需要在堆上分配基本类型。它们可以在栈上分配(并不是说它们只能在栈上分配)。而在C++中,我们可以将基本类型和用户定义的类型都分配到堆和栈上。

答案有什么问题吗?只是想知道。对于负评没有任何问题。 - Jagannath

0
在 C++ 中,除了常规的 new 操作符,还有一种额外的使用方式,那就是通过“定位 new”(placement new)来使用。你可以将其指向任何内存地址。
请参见 “定位 new”有哪些用途?

你可能指的是“放置new 表达式”,而不是运算符(实际上,运算符是无操作的)。新表达式的关键特征是调用构造函数。 - Kerrek SB

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