在结构上使用“新”是否将它分配到堆或栈上?
当用new
运算符创build一个类的实例时,内存将被分配到堆上。 当你用new
运算符创build一个结构体的实例时,内存被分配到哪里,在堆上还是在栈上?
好的,让我们看看我能否做得更清楚。
首先,Ash是正确的:问题不在于值typesvariables的分配。 这是一个不同的问题 – 答案不仅仅是“堆叠”。 这比这更复杂(而且由C#2变得更加复杂)。 我有一个关于这个话题的文章,如果需要的话,将会扩展它,但是让我们来处理这个new
操作符。
其次,这一切都取决于你在说什么水平。 我正在研究编译器在源代码上做了什么,就它创build的IL而言。 JIT编译器在优化掉相当多的“逻辑”分配方面会做很多巧妙的事情。
第三,我忽略了generics,主要是因为我实际上并不知道答案,一方面是因为这太复杂了。
最后,所有这些只是目前的实施。 C#规范没有详细说明 – 它实际上是一个实现细节。 有些人认为托pipe代码开发人员真的不应该在意。 我不确定我会走多远,但是值得一想的是,这个世界其实所有的局部variables都在堆上 – 这仍然符合规范。
new
运算符在值types上有两种不同的情况:您可以调用无参数构造函数(例如new Guid()
)或有参数的构造函数(例如new Guid(someString)
)。 这些产生显着不同的IL。 要理解为什么,您需要比较C#和CLI规范:根据C#,所有值types都有一个无参数的构造函数。 根据CLI规范, 没有值types具有无参数的构造函数。 (使用reflection获取值types的构造函数 – 您将不会find无参数的值)
C#将“用零初始化一个值”作为构造函数是有道理的,因为它保持了语言的一致性 – 您可以将new(...)
视为始终调用构造函数。 CLI有不同的想法是有道理的,因为没有真正的代码可以调用 – 当然也没有types特定的代码。
在初始化之后,您将如何处理值。 IL用于
Guid localVariable = new Guid(someString);
与IL所使用的不同:
myInstanceOrStaticVariable = new Guid(someString);
另外,如果该值被用作中间值,例如方法调用的参数,那么事情又会稍有不同。 为了显示所有这些差异,这里有一个简短的testing程序。 它没有显示静态variables和实例variables之间的差异:IL将在stfld
和stsfld
之间有所不同,但是全部。
using System; public class Test { static Guid field; static void Main() {} static void MethodTakingGuid(Guid guid) {} static void ParameterisedCtorAssignToField() { field = new Guid(""); } static void ParameterisedCtorAssignToLocal() { Guid local = new Guid(""); // Force the value to be used local.ToString(); } static void ParameterisedCtorCallMethod() { MethodTakingGuid(new Guid("")); } static void ParameterlessCtorAssignToField() { field = new Guid(); } static void ParameterlessCtorAssignToLocal() { Guid local = new Guid(); // Force the value to be used local.ToString(); } static void ParameterlessCtorCallMethod() { MethodTakingGuid(new Guid()); } }
下面是这个类的IL,排除不相关的比特(比如nops):
.class public auto ansi beforefieldinit Test extends [mscorlib]System.Object { // Removed Test's constructor, Main, and MethodTakingGuid. .method private hidebysig static void ParameterisedCtorAssignToField() cil managed { .maxstack 8 L_0001: ldstr "" L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string) L_000b: stsfld valuetype [mscorlib]System.Guid Test::field L_0010: ret } .method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed { .maxstack 2 .locals init ([0] valuetype [mscorlib]System.Guid guid) L_0001: ldloca.s guid L_0003: ldstr "" L_0008: call instance void [mscorlib]System.Guid::.ctor(string) // Removed ToString() call L_001c: ret } .method private hidebysig static void ParameterisedCtorCallMethod() cil managed { .maxstack 8 L_0001: ldstr "" L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string) L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid) L_0011: ret } .method private hidebysig static void ParameterlessCtorAssignToField() cil managed { .maxstack 8 L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field L_0006: initobj [mscorlib]System.Guid L_000c: ret } .method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed { .maxstack 1 .locals init ([0] valuetype [mscorlib]System.Guid guid) L_0001: ldloca.s guid L_0003: initobj [mscorlib]System.Guid // Removed ToString() call L_0017: ret } .method private hidebysig static void ParameterlessCtorCallMethod() cil managed { .maxstack 1 .locals init ([0] valuetype [mscorlib]System.Guid guid) L_0001: ldloca.s guid L_0003: initobj [mscorlib]System.Guid L_0009: ldloc.0 L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid) L_0010: ret } .field private static valuetype [mscorlib]System.Guid field }
正如你所看到的,有很多不同的指令用于调用构造函数:
-
newobj
:分配栈中的值,调用一个参数化的构造函数。 用于中间值,例如分配给字段或用作方法参数。 -
call instance
:使用已经分配的存储位置(不pipe是否在堆栈上)。 这在上面的代码中用于分配给局部variables。 如果使用几次new
调用将相同的局部variables多次分配一个值,则它只是初始化旧值的顶部的数据 – 它不会每次分配更多的堆栈空间。 -
initobj
:使用已经分配的存储位置,只擦除数据。 这用于我们所有的无参数构造函数调用,包括那些分配给局部variables的调用。 对于方法调用,有效地引入了一个中间局部variables,其值由initobj
。
我希望这个话题能够说明这个话题是多么复杂,同时又是一个亮点。 在一些概念的意义上,每一次new
调用都在堆栈中分配空间 – 但正如我们所看到的,即使在IL级别也不是真正发生的事情。 我想强调一个特例。 采取这种方法:
void HowManyStackAllocations() { Guid guid = new Guid(); // [...] Use guid guid = new Guid(someBytes); // [...] Use guid guid = new Guid(someString); // [...] Use guid }
这个“逻辑上”有4个堆栈分配 – 一个用于variables,一个用于三个new
调用 – 但事实上(对于特定的代码)堆栈只分配一次,然后重用相同的存储位置。
编辑:只是要清楚,这只是在某些情况下是正确的…尤其是, guid
的值将不可见如果Guid
构造函数抛出一个exception,这就是为什么C#编译器能够重用相同的堆栈插槽。 有关更多详细信息和不适用的情况,请参阅Eric Lippert 关于价值types构build的博客文章 。
我在写这个答案的过程中学到了很多,如果有任何不清楚的地方,请澄清一下。
包含struct字段的内存可以根据具体情况分配在堆栈或堆上。 如果struct-typevariables是某个匿名委托或迭代器类未捕获的局部variables或参数,则它将被分配到堆栈上。 如果variables是某个类的一部分,那么它将被分配到堆中的类中。
如果struct被分配在堆上,那么调用new运算符实际上不是分配内存所必需的。 唯一的目的是根据构造函数中的内容设置字段值。 如果构造函数没有被调用,那么所有的字段将得到它们的默认值(0或null)。
类似地,对于堆栈中分配的结构,除了C#要求所有局部variables在使用之前被设置为某个值之外,所以您必须调用自定义构造函数或默认构造函数(不带参数的构造函数始终可用于结构)。
简而言之,新结构是结构错误的一种,称新结构只是简单地称为构造函数。 结构的唯一存储位置是它所定义的位置。
如果它是一个成员variables,它将直接存储在它定义的任何variables中,如果它是一个局部variables或参数,它将被存储在堆栈中。
将其与类进行对比,类中有结构可能已被完整存储的地方,而引用指向堆的某处。 (成员,本地/堆栈参数)
这可能有助于看起来有点到C + +,其中没有真正的类/结构之间的区别。 (在语言中有类似的名字,但它们只是指默认的可访问性)当你调用new时,你得到一个指向堆的位置的指针,而如果你有一个非指针的引用,它将直接存储在堆栈上,或者在另一个对象中,C#中的ala结构。
与所有值types一样,结构总是在声明的位置 。
有关何时使用结构的更多详细信息,请参阅此处的问题。 这个问题在这里的结构更多的信息。
编辑:我错误地回答说,他们总是在堆栈中。 这是不正确的 。
我可能在这里错过了一些东西,但为什么我们关心分配?
值types是通过值传递的;)因此不能在与定义的范围不同的范围进行变异。 为了能够改变值,你必须添加[ref]关键字。
引用types通过引用传递,可以进行变异。
当然,不可变的引用typesstring是最stream行的。
数组布局/初始化:值types – >零内存[name,zip] [name,zip]引用types – >零内存 – > null [ref] [ref]
class
或struct
声明就像是用来在运行时创build实例或对象的蓝图。 如果您定义了一个名为Person的class
或struct
,那么Person就是该types的名称。 如果声明并初始化Persontypes的variablesp,则称p为Person的对象或实例。 可以创build同一个Persontypes的多个实例,并且每个实例在其properties
和fields
可以具有不同的值。
一个class
是一个引用types。 当class
一个对象被创build时,对象被赋值的variables只保存对该内存的引用。 当对象引用被分配给新variables时,新variables引用原始对象。 通过一个variables所做的更改反映在另一个variables中,因为它们都指向相同的数据。
struct
是一个值types。 当一个struct
被创build时, struct
被分配的variables保存结构的实际数据。 当struct
被分配给一个新的variables时,它被复制。 新variables和原始variables因此包含相同数据的两个单独的副本。 对一个副本所做的更改不会影响另一个副本。
通常, classes
用于build模更复杂的行为,或者在创buildclass
对象后打算修改的数据。 Structs
最适合于小数据结构,主要包含在创buildstruct
之后不打算修改的数据。
更多…
几乎所有被视为值types的结构都分配在堆栈上,而对象则分配在堆上,而对象引用(指针)则分配到堆栈上。
结构被分配到堆栈。 这是一个有用的解释:
结构
另外,在.NET中实例化的类在堆或.NET的保留内存空间上分配内存。 而由于堆栈上的分配,结构在实例化时会产生更高的效率。 此外,应该注意的是,在结构中传递参数是通过值来完成的。