堆和堆栈是如何为C#中结构的实例和成员工作的



我正在读一本书,上面写着:

表示结构实例的变量不包含指向实例的指针;变量包含实例本身的字段。因为变量包含实例的字段,所以不必取消引用指针来操作实例的字段。以下代码演示了引用类型和值类型如何区别

class SomeRef { public Int32 x; }
struct SomeVal { public Int32 x; }
static void ValueTypeDemo() {
SomeRef r1 = new SomeRef();        // Allocated in heap
SomeVal v1 = new SomeVal();        // Allocated on stack
r1.x = 5;                          // Pointer dereference
v1.x = 5;                          // Changed on stack
}

我来自C背景,对结构变量v1有点困惑,我觉得v1.x = 5;仍然涉及指针解引用,就像C中的数组变量是指向该数组中第一个元素地址的指针一样,我认为v1必须是指向SomeVal中第一个字段地址(当然是堆栈,而不是堆)的指针,如果我的理解正确的话,那么CCD_ 5也必须涉及指针去引用?如果不是,如果我们想访问结构中的随机字段,因为编译器需要生成字段的偏移量,那么指针怎么不涉及呢?

理论上,运行时不能保证结构的存储方式,只要行为相同,它就可以随心所欲地存储。

在实践中,您的示例将作为方法堆栈框架的一部分存储。因此v1将保留结构的空间,即4个字节。对结构的字段的访问将被简单地转换为相应的字段,就像直接使用int32一样。

如果结构有多个字段,编译器只需将多个偏移量加在一起,一个偏移量到结构的开头,一个到实际字段。所有这些在编译时都是已知的,所以编译器解决这个问题是没有问题的。

注意,虽然CIL使用基于堆栈的模型,但抖动可能会优化要存储在寄存器中的变量。还有ref关键字,它允许引用值类型,有点类似于指针。

相关答案:
结构实例如何';s的虚拟方法在堆中使用其类型对象定位
装箱值类型在C#中是如何在内部工作的
.NET中的所有内容都是对象吗?

正如@Damien_The_Unbeliever所说,以下内容仅适用于当前的计算技术,因为.NET是一个虚拟平台。事实上,自从CPU开始和堆栈寄存器的发明以来,在类似英特尔的微处理器(x86、x32、x64等)上,行为就是这样。但在未来,量子等任何其他技术都可能会有所不同

作为类成员的结构实例与对象本身一起分配,因此在堆中也是如此,但在方法中声明的局部结构变量在堆栈中分配。

此外,作为方法参数传递的变量总是使用堆栈:引用以及结构的内容都是PUSHed和POPed,因此建议不要过度使用结构和匿名类型的限制,也不要太大。

为了简化和理解,想象一下堆是一个完整的房间,堆是这个房间里的一个橱柜。

此文件柜用于本地值类型变量和用于运行程序的引用,以及在方法之间传递数据,以及在这些方法是函数而非过程时获取这些方法的结果:引用、值类型、积分类型、结构内容、匿名类型和委托作为临时容器被推送和弹出到此文件柜。

房间是为对象本身准备的(我们传递对象的引用),除了不在对象中的单独结构(我们传递所有结构内容,当我们传递类中的结构时也是一样的,我们将整个结构作为副本传递)。

例如:

class MyClass
{ 
MyStruct MyVar;
}

是一个结构变量"并非唯一";当在任何位置创建时,在头部中与对象一起创建。

但是:

void MyMethod()
{ 
MyStruct MyVar;
}

是当地的";单独";在堆栈中创建的结构的实例以及整数。

因此,如果一个类有10个整数,那么在调用方法时,堆栈中只有引用被推送(x32上为4字节,x64上为8字节)。但如果它是一个结构,它需要PUSH 10个整数(x32和x64上的40个字节)。

换句话说,正如您所写:因此,单独的结构实例(因此,分配给结构类型的局部变量)不会存储在堆中。但是类的成员(因此,分配给结构类型的字段)存储在堆中。

也就是说:

堆中结构的成员(整数和引用指针"值")使用MOV操作码和等效代码(虚拟或目标机器代码)使用直接内存访问进行访问。
  • 堆栈中结构的成员使用堆栈寄存器基+偏移量进行访问。

  • 第一个是慢的,第二个是快的。

    这个对象的内存会是什么样子?

    堆栈和堆在什么地方?

    在c sharp 中堆叠

    内存分配:堆栈与堆?

    堆栈和堆分配

    堆栈和堆存储器

    为什么方法在正常情况下只返回一种参数?

    CIL指令清单

    .NET OpCodes类

    堆栈寄存器

    堆栈的概念及其在微处理器中的应用

    基于堆栈的CPU组织介绍

    堆栈在微处理器中的作用是什么?

    为了更好地理解并提高你的计算技能,你可能会发现研究什么是汇编语言以及CPU是如何工作的很有趣。你可以从IL和现代英特尔开始,但从过去的8086到i386/i486可能会更简单、更具形成性和互补性。

    您是正确的-指向所涉及的结构IS的指针,但指向结构内字段的偏移量是在编译时计算的。

    用于在字段中存储(非引用)值的IL指令是stfld,用于从字段加载(非参考)值的指令是ldfield

    当然,这些IL指令由JIT编译器转换为程序集,JIT编译器可能会应用许多优化,例如避免多次加载同一指针,但这取决于编译器版本以及您是否启用了DEBUG或RELEASE版本。

    例如,考虑以下结构:

    struct SomeVal
    {
    public Int32 x; 
    public Int32 y;
    }
    

    代码:

    SomeVal v1 = new SomeVal();
    v1.x = 5;
    v1.y = 6;
    Console.WriteLine(v1.x + v1.y);
    

    为RELEASE生成的IL是:

    .entrypoint
    .locals init (
    [0] valuetype ConsoleApp1.SomeVal V_0
    )
    IL_0000: ldloca.s V_0
    IL_0002: initobj ConsoleApp1.SomeVal
    IL_0008: ldloca.s V_0
    IL_000a: ldc.i4.5
    IL_000b: stfld int32 ConsoleApp1.SomeVal::x
    IL_0010: ldloca.s V_0
    IL_0012: ldc.i4.6
    IL_0013: stfld int32 ConsoleApp1.SomeVal::y
    IL_0018: ldloc.0
    IL_0019: ldfld int32 ConsoleApp1.SomeVal::x
    IL_001e: ldloc.0
    IL_001f: ldfld int32 ConsoleApp1.SomeVal::y
    IL_0024: add
    IL_0025: call void [mscorlib]System.Console::WriteLine(int32)
    IL_002a: ret
    

    v1.x = 5的IL为:

    IL_0008: ldloca.s V_0
    IL_000a: ldc.i4.5
    IL_000b: stfld int32 ConsoleApp1.SomeVal::x
    

    注意:

    1. 使用ldloca.s V_0将结构的地址推送到堆栈上
    2. 使用ldc.i4.5将常量int32值5推送到堆栈上
    3. 将该int32值存储到由ConsoleApp1.SomeVal::x使用stfld int32 ConsoleApp1.SomeVal::x定义的常量偏移处的字段中

    您可以看到类似的IL代码,用于在使用addxy字段相加之前加载它们。

    OP问题:

    如果不是,如果我们想访问随机字段,因为编译器需要生成字段,仍然需要涉及指针?

    来自C背景,你会理解一个变量,它是一个指针,例如int* p;

    对于堆栈上的结构,通常会涉及一个指针,而不是像上面提到的*p那样您知道的指针。

    例如,编译器/抖动可以生成使用指针来保存堆栈基址的代码。从那里,堆栈上变量的偏移量被生成为常量,这些常量可以添加到堆栈指针中,以访问堆栈中变量的值。一些CPU有一个名为SP(堆栈指针)的寄存器,用于跟踪堆栈的基址。

    对于您的示例,生成的代码可以在伪汇编代码中显示为:

    // Suppose SP = 0x1004
    // SP = 0x1000 after v1 is allocated (SP decremented by 4)
    // SP is now the pointer that you were wondering about
    // v1.x = 5, Place 32-bit constant value 5 at address 0x1000
    mov 5, [SP] 
    

    请注意,SP寄存器是您想知道的指针。还要注意,由于结构只包含一个变量,因此结构的地址与变量的地址相同。这个伪程序集也可能是这样的:

    // Suppose SP = 0x1004
    // v1.x = 5, push constant value 5 on the stack
    push #5   
    // SP = 0x1000 (again, SP decremented by 4)
    

    因此,指针,但它用于编译器/抖动生成的较低级别代码中,并且在源代码级别,不需要显式指针变量。为了读取v1.x,较低级别生成的代码可以使用相同的SP(指针)。例如:

    // int a = v1.x;
    // pseudo-assembly of generated code
    // A CPU register R0 is used for variable a.
    // Place the value at address SP into R0.
    mov [SP], R0
    

    对于您的案例,C#,MSIL代码将生成,这是一个很好的答案。在MSIL编译之后,例如,对于x64 CPU目标,代码在概念上与此处显示的代码类似。

    最新更新