使用“;new"在结构上分配它在堆上还是堆栈上?

当使用new操作符创建类的实例时,将在堆上分配内存。当你用new操作符创建一个结构体实例时,内存分配在哪里,在堆上还是在堆栈上?

90592 次浏览

与所有值类型一样,结构体始终位于宣布所在的位置。

有关何时使用结构体的更多细节,请参阅这个问题在这里。 这个问题在这里是关于struct的更多信息

我错误地回答他们总是去堆栈。这是不正确的

结构体被分配给堆栈。下面是一个有用的解释:

Structs

此外,在.NET中实例化的类在。net中分配内存 堆或。net的预留内存空间。而结构体的产量更高 由于在堆栈上的分配,实例化时的效率。 此外,应该注意在结构中传递参数

包含结构的字段的内存可以根据情况分配到堆栈或堆上。如果结构类型变量是未被某个匿名委托或迭代器类捕获的局部变量或形参,则它将被分配到堆栈上。如果变量是某个类的一部分,那么它将在堆上的类中分配。

如果结构体是在堆上分配的,那么实际上不需要调用new操作符来分配内存。唯一的目的是根据构造函数中的内容设置字段值。如果未调用构造函数,则所有字段将获得默认值(0或null)。

同样,对于在堆栈上分配的结构,除了c#要求在使用所有局部变量之前将它们设置为某个值,因此您必须调用自定义构造函数或默认构造函数(不带参数的构造函数总是可用于结构)。

很多被认为是值类型的结构体都是在堆栈上分配的,而对象是在堆上分配的,而对象引用(指针)是在堆栈上分配的。

好吧,看看我能不能说得更清楚些。

首先,Ash是对的:问题是,关于值类型变量分配在哪里。这是一个不同的问题——这个问题的答案不仅仅是“在堆栈上”。它比这更复杂(c# 2使它更加复杂)。我有一个关于此主题的文章,如果需要,将对它展开,但让我们只处理new操作符。

其次,所有这些都取决于你所谈论的水平。我正在查看编译器对源代码做了什么,根据它创建的IL。JIT编译器很有可能在优化大量“逻辑”分配方面做一些聪明的事情。

第三,我忽略了泛型,主要是因为我实际上不知道答案,部分原因是它会使事情变得太复杂。

最后,所有这些都只是当前的实现。c#规范并没有详细说明这一点——它实际上只是一个实现细节。有些人认为托管代码开发人员真的不应该关心。我不确定我会走得那么远,但值得想象的是,实际上所有局部变量都位于堆上——这仍然符合规范。


在值类型上使用new操作符有两种不同的情况:既可以调用无参数构造函数(例如new Guid()),也可以调用参数构造函数(例如new Guid(someString))。为了理解其中的原因,你需要比较c#和CLI规范:根据c#,所有的值类型都有一个无参数的构造函数。根据CLI规范,没有值类型具有无参数构造函数。(在某些时候获取带有反射的值类型的构造函数-你不会找到一个无参数的构造函数。)

对于c#来说,将“用0初始化一个值”作为构造函数是有意义的,因为它保持了语言的一致性——你可以把new(...)看作调用构造函数的总是。对于CLI来说,从不同的角度考虑它是有意义的,因为没有真正的代码要调用——当然也没有特定于类型的代码。

在初始化后对值的处理也会有所不同。用于

Guid localVariable = new Guid(someString);

不同于用于:

myInstanceOrStaticVariable = new Guid(someString);

此外,如果该值用作中间值,例如方法调用的参数,情况又略有不同。为了显示所有这些差异,这里有一个简短的测试程序。它没有显示静态变量和实例变量之间的区别:IL会在stfldstsfld之间有所不同,但仅此而已。

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:使用一个已经分配的存储位置(无论是否在堆栈上)。这在上面的代码中用于给局部变量赋值。如果使用多次new调用给同一个局部变量赋值,它只是在旧值之上初始化数据——它每次分配更多的堆栈空间。
  • initobj:使用一个已经分配的存储位置,只是擦除数据。这用于所有的无参数构造函数调用,包括赋值给局部变量的调用。对于方法调用,有效地引入了一个中间局部变量,其值被initobj擦除。

我希望这能显示出这个话题有多复杂,同时也能让人对它有所了解。在一些概念上,对new的每次调用都在堆栈上分配空间——但正如我们所看到的,即使在IL级别上也不是这样。我想强调一个特殊的案例。试试这个方法:

void HowManyStackAllocations()
{
Guid guid = new Guid();
// [...] Use guid
guid = new Guid(someBytes);
// [...] Use guid
guid = new Guid(someString);
// [...] Use guid
}

“逻辑上”有4个堆栈分配-一个用于变量,一个用于三个new调用中的每个调用-但实际上(对于特定的代码)堆栈只分配一次,然后重复使用相同的存储位置。

编辑:澄清一下,这只是在某些情况下是正确的……特别是,如果Guid构造函数抛出异常,guid的值将不可见,这就是c#编译器能够重用相同堆栈槽的原因。参见Eric Lippert的关于价值类型建设的博客文章获取更多细节和适用的情况。

我在写这个答案中学到了很多——如果有任何不清楚的地方,请澄清!

简单地说,new是struct的用词不当,调用new只是调用构造函数。结构体的唯一存储位置是定义它的位置。

如果它是成员变量,它将直接存储在定义它的任何地方,如果它是局部变量或参数,它将存储在堆栈中。

与此形成对比的是类,类在结构完整存储的任何位置都有引用,而引用点则位于堆上的某个位置。(Member within, local/parameter on stack)

研究一下c++可能会有所帮助,在c++中,类/结构之间没有真正的区别。(在语言中有类似的名字,但它们只指事物的默认可访问性)当你调用new时,你会得到一个指向堆位置的指针,而如果你有一个非指针引用,它会直接存储在堆栈上或其他对象中,就像c#中的结构体一样。

我可能遗漏了一些东西但是我们为什么要关心分配呢?

值类型是通过Value;)传递的,因此不能在不同于定义它们的范围内进行突变。为了能够改变值,您必须添加[ref]关键字。

引用类型是通过引用传递的,可以进行突变。

当然有不可变的引用类型字符串是最流行的。

< p >阵列布局/初始化: 值类型->零内存[name,zip][name,zip] 引用类型-> 0内存-> null [ref][ref]

classstruct声明就像一个蓝图,用于在运行时创建实例或对象。如果定义名为Person的classstruct,则Person是类型的名称。如果声明并初始化一个Person类型的变量p,则p是Person的对象或实例。可以创建同一Person类型的多个实例,并且每个实例的propertiesfields中可以有不同的值。

class是一个引用类型。当class的对象被创建时,赋给该对象的变量仅保存对该内存的引用。当对象引用被赋值给一个新变量时,新变量引用原始对象。通过一个变量所做的更改会反映在另一个变量中,因为它们都引用相同的数据。

struct是一个值类型。当struct被创建时,赋值给struct的变量保存结构的实际数据。当struct被赋值给一个新变量时,它会被复制。因此,新变量和原始变量包含相同数据的两个独立副本。对一个副本所做的更改不会影响另一个副本。

一般来说,classes用于建模更复杂的行为,或在创建class对象后要修改的数据。Structs最适合用于小型数据结构,其中主要包含在struct创建后不打算修改的数据。

for more