在结构上使用“ new”是否在堆或堆栈上分配它?


290

当您使用new运算符创建类的实例时,将在堆上分配内存。当您使用new运算符创建结构实例时,在堆或堆栈上分配内存的位置在哪里?

Answers:


305

好吧,让我们看看是否可以更清楚地说明这一点。

首先,Ash是正确的:问题在于值类型变量的分配位置。那是一个不同的问题-答案不只是“在堆栈上”。比这要复杂的多(并且在C#2中更加复杂)。我有一篇关于该主题文章,如果需要,将对其进行扩展,但让我们只讨论一下new运算符。

其次,所有这些实际上取决于您所谈论的级别。我正在根据其创建的IL来查看编译器对源代码的处理方式。JIT编译器很有可能会在优化大量“逻辑”分配方面做一些聪明的事情。

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

最后,所有这些仅与当前的实现有关。C#规范并未对此进行详细说明-实际上是实现细节。有些人认为托管代码开发人员确实不应该在乎。我不确定我会走的那么远,但是值得想象一个世界,实际上所有局部变量都存在于堆中-仍然符合规范。


new值类型的运算符有两种不同的情况:您可以调用无参数构造函数(例如new Guid())或有参数构造函数(例如new Guid(someString))。这些产生明显不同的IL。要了解原因,您需要比较C#和CLI规范:根据C#,所有值类型都具有无参数构造函数。根据CLI规范,没有值类型具有无参数构造函数。(一段时间后,通过反射来获取值类型的构造函数-您将找不到无参数的值。)

对于C#来说,将“用零初始化值”视为构造函数是有意义的,因为它使语言保持一致-您可以将其new(...)视为始终调用构造函数。对于CLI,以不同的方式考虑它是有意义的,因为没有要调用的实际代码-当然也没有特定于类型的代码。

初始化值后,对值的处理方式也会有所不同。用于的IL

Guid localVariable = new Guid(someString);

与用于以下方面的IL不同:

myInstanceOrStaticVariable = new Guid(someString);

另外,如果将该值用作中间值(例如,方法调用的参数),则情况会稍有不同。为了显示所有这些差异,这是一个简短的测试程序。它没有显示出静态变量和实例变量之间的区别: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:使用已分配的存储位置(无论是否在堆栈上)。这在上面的代码中用于分配给局部变量。如果使用多个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构造函数抛出异常,则w 的值将不可见,这就是C#编译器能够重用同一堆栈槽的原因。请参阅Eric Lippert 关于值类型构造博客文章,以了解更多详细信息和适用的情况。

我在编写此答案时学到了很多知识-如果不清楚,请要求澄清!


1
乔恩(Jon),HowManyStackAllocations示例代码很好。但是您可以将其更改为使用Struct代替Guid,还是添加一个新的Struct示例。我认为那将直接解决@kedar的原始问题。
灰烬”

9
Guid已经是一个结构。参见msdn.microsoft.com/zh-cn/library/system.guid.aspx 我不会为该问题选择参考类型:)
Jon Skeet

1
当您拥有List<Guid>并添加这3个元素时会发生什么?那将是3个分配(相同的IL)?但是他们一直
处在

1
@Ani:您缺少Eric的示例具有try / catch块的事实-因此,如果在结构的构造函数期间引发异常,则需要能够在构造函数之前看到该值。我的示例没有这种情况-如果构造函数因异常而失败,则的值guid仅被一半覆盖并不重要,因为无论如何它都不可见。
乔恩·斯基特

2
@Ani:实际上,Eric在他的帖子底部附近这样说:“现在,Wesner的观点如何?是的,实际上,如果声明了它是一个堆栈分配的局部变量(而不是闭包中的字段),在与构造函数调用相同的“尝试”嵌套级别上,那么我们就不用经历制作一个新的临时文件,初始化该临时文件并将其复制到本地的过程了,在这种特殊(和常见)的情况下,我们可以进行优化临时文件和副本的创建,因为C#程序无法观察到差异!”
乔恩·斯基特

40

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

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

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


13

简而言之,new是结构的误称,调用new只是调用构造函数。该结构的唯一存储位置是其定义的位置。

如果它是成员变量,则直接存储在定义的内容中;如果它是局部变量或参数,则存储在堆栈中。

将此与类进行比较,类将在结构完整存储的所有位置都有一个引用,而该引用指向堆上的某个位置。(成员在内部,本地/参数在堆栈上)

稍微看一下C ++可能会有所帮助,在C ++中,类/结构之间没有真正的区别。(该语言中有相似的名称,但它们仅引用事物的默认可访问性)当您调用new时,您将获得指向堆位置的指针,而如果您有非指针引用,则将其直接存储在堆栈中或在另一个对象中,C#中的ala结构。


5

与所有值类型一样,结构总是在声明它们的位置。

有关何时使用结构的更多详细信息,请参见此处的问题。这个问题在这里提供了有关结构的更多信息。

编辑:我含糊其词地回答说,他们总是在堆栈中。这是不正确的


“结构总是去声明它们的地方”,这有点令人迷惑。类中的struct字段总是放在“构造类型的实例时的动态内存中”-Jeff Richter。这可能是间接在堆上的,但根本不同于普通的引用类型。
灰烬

不,我认为这是完全正确的-尽管它与引用类型不同。变量的值存在于声明的位置。引用类型变量的值仅是引用,而不是实际数据。
乔恩·斯基特

总之,无论何时在方法中的任何地方创建(声明)值类型,它总是在堆栈上创建。
灰烬

2
琼恩,你想念我的意思。首先问这个问题的原因是,如果您使用new运算符创建结构,那么对于许多开发人员(直到我通过C#阅读CLR都包括在内)尚不清楚。说“结构总是去声明它们的地方”并不是一个明确的答案。
Ash Ash

1
@Ash:如果有时间,我会在上班时尝试写下答案。不过,这是一个太大的话题,无法尝试在火车
上讲

4

我可能在这里丢失了一些东西,但是为什么我们要关心分配?

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

引用类型通过引用传递,并且可以被更改。

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

数组布局/初始化:值类型->零内存[name,zip] [name,zip]引用类型->零内存-​​> null [ref] [ref]


3
引用类型不按引用传递-引用按值传递。那是完全不同的。
乔恩·斯基特

2

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

A class是引用类型。class创建的对象时,为其分配对象的变量仅保留对该内存的引用。当对象引用分配给新变量时,新变量引用原始对象。通过一个变量进行的更改会反映在另一个变量中,因为它们都引用相同的数据。

A struct是值类型。当struct被创建,以该变量struct被分配保持结构体的实际数据。将struct分配给新变量后,将对其进行复制。因此,新变量和原始变量包含相同数据的两个单独副本。对一个副本所做的更改不会影响另一副本。

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

更多...


1

几乎所有被认为是值类型的结构都分配在堆栈上,而对象则分配在堆上,而对象引用(指针)则分配在堆栈上。


1

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

结构

此外,在.NET中实例化的类在堆或.NET的保留内存空间上分配内存。而由于在堆栈上进行分配,结构在实例化时会产生更高的效率。此外,应注意,在结构中传递参数是通过值完成的。


5
当结构是类的一部分时,这并不涉及这种情况-此时,结构与对象的其余数据一起位于堆中。
乔恩·斯基特

1
是的,但实际上它专注于并回答所提出的问题。投票了。
灰烬

...同时仍然是错误和误导的。抱歉,这个问题没有简短的答案-杰弗里的答案是唯一完整的答案。
马克·Gravell
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.