在结构体上使用 "new" 关键字会在堆上还是栈上分配它?

14 浏览
0 Comments

在结构体上使用 "new" 关键字会在堆上还是栈上分配它?

当使用new运算符创建一个类的实例时,内存会在堆上分配。当使用new运算符创建一个结构体的实例时,内存会分配在堆上还是栈上?

0
0 Comments

使用"new"关键字在一个结构体上是否会在堆上分配内存还是栈上分配内存的问题,出现的原因是因为对于结构体的存储位置有一些混淆和误解。解决方法是了解结构体的存储规则以及与类的区别。

首先,对于结构体而言,"new"关键字只是调用构造函数,它并不会在堆上分配内存。结构体的唯一存储位置取决于它的定义方式。如果它是一个成员变量,它会直接存储在定义它的对象中;如果它是一个局部变量或参数,它会存储在栈上。

与之相反,对于类而言,类的引用指向堆上的位置,而不是存储在堆上的整个类对象。类的成员变量在类的内部存储,局部变量或参数存储在栈上。

可能有助于了解一下C++,在C++中,类和结构体之间没有真正的区别(语言中有类似的名称,但它们只是指代事物的默认可访问性)。当你调用"new"关键字时,你会得到一个指向堆上位置的指针,而如果你有一个非指针引用,它会直接存储在栈上或其他对象内部,类似于C#中的结构体。

所以,结构体是否在堆上分配内存取决于它的使用方式和定义位置,并不是"new"关键字的使用与否决定的。

0
0 Comments

使用"new"关键字在结构体上是否会将其分配在堆上还是栈上,这个问题的出现是因为结构体的字段所在的内存可以根据情况分配在堆上或栈上。

如果结构体类型的变量是一个局部变量或参数,且没有被某个匿名委托或迭代器类捕获,则它将在栈上分配。如果变量是某个类的一部分,则它将在堆上分配。

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

对于在栈上分配的结构体也是类似的,但是C#要求在使用局部变量之前将其设置为某个值,所以必须调用自定义构造函数或默认构造函数(始终提供不带参数的构造函数给结构体)。

解决方法:根据上述内容,我们可以得出使用"new"关键字在结构体上是否会将其分配在堆上还是栈上的结论。如果结构体是局部变量或参数,则分配在栈上;如果结构体是某个类的一部分,则分配在堆上。在堆上分配结构体时,"new"操作符并不是必需的,只有在需要根据构造函数设置字段值时才需要使用。在栈上分配结构体时,必须调用构造函数来初始化局部变量。

0
0 Comments

使用"new"关键字在结构体上会将其分配到堆还是栈上?

这个问题的出现主要是因为对于值类型变量的分配位置的疑问。这是一个不同的问题,答案不仅仅是“在栈上”。这个问题更加复杂,特别是在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`的值将不可见,这就是为什么C#编译器能够重用相同的栈槽的原因。有关更多详细信息和一个不适用的情况,请参阅Eric Lippert的有关值类型构造的博客文章。

在写这个答案的过程中,我学到了很多东西 - 如果有任何不清楚的地方,请提出澄清。

0