微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

如何解决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 中的数组变量是指向 first 的地址的指针一样该数组中的元素,我觉得v1必须是指向SomeVal中第一个字段的地址(堆栈,当然不是堆)的指针,如果我的理解是正确的,那么{{ 1}} 也必须涉及指针解引用?如果不是,如果我们要访问结构中的随机字段,因为编译器需要生成该字段的偏移量,如何不涉及指针,仍然必须涉及指针?

解决方法

理论上,运行时不保证结构体的存储方式,它可以存储它,只要行为是相同的。

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

如果结构体有多个字段,编译器会简单地将多个偏移量加在一起,一个到结构体的开头,一个到实际字段。所有这些都是在编译时就知道的,所以编译器弄清楚这一点是没有问题的。

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

,

相关答案:
How does a struct instance's virtual method get located using its type object in heap?
How boxing a value type work internally in C#?
Is everything in .NET an object?

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

struct 的实例作为类的成员是用对象本身分配的,所以在堆中,但是在方法中声明的局部 struct 变量在堆栈中分配。

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

为了简化和理解,假设堆是整个房间,栈是这个房间的橱柜。

这个柜子是用于运行程序的局部值类型变量和引用,以及在方法之间传递数据并在这些方法是函数而不是过程时获取这些方法的结果:引用、值类型、整数类型、结构内容、匿名类型和委托作为临时容器在此橱柜中进行推送和弹出。

这个空间是给对象本身的(我们传递对象的引用),除了结构体本身不在对象中(我们传递所有结构体的内容,当我们传递一个在类中的结构体时也是如此,我们将整个结构作为副本传递)。

例如:

class MyClass
{ 
  MyStruct MyVar;
}

是在任何地方创建时在对象的头部创建的“不单独”的结构变量。

但是:

void MyMethod()
{ 
  MyStruct MyVar;
}

是在堆栈中创建的结构以及整数的本地“单独”实例。

因此,如果一个类有 10 个整数,则在调用方法时仅将引用推入堆栈(x32 上为 4 个字节,x64 上为 8 个字节)。但如果是结构体,则需要 PUSH 10 个整数(x32 和 x64 上为 40 个字节)。

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

说:

  • 堆中结构的成员(整数数字和引用指针“值”)通过使用 MOV 操作码和等效(虚拟或目标机器代码)的直接内存访问进行访问。

  • 使用堆栈寄存器基址+偏移量访问堆栈中结构的成员。

第一个比较慢,第二个比较快。

How would the memory look like for this object?

What and where are the stack and heap?

Stack and heap in c sharp

Memory allocation: Stack vs Heap?

Stack and Heap allocation

Stack and Heap memory

Why methods return just one kind of parameter in normal conditions?

List of CIL instructions

.NET OpCodes Class

Stack register

The Concept of Stack and Its Usage in Microprocessors

Introduction of Stack based CPU Organization

What is the role of stack in a microprocessor?

为了更好地理解并提高您的计算技能,您可能会发现研究什么是 assembly language 以及 CPU 的工作原理很有趣。您可以从 ILmodern Intel 开始,但从过去的 8086 to i386/i486 开始可能更简单、更具形成性和互补性。

,

你是对的 - 一个指向结构体的指针是涉及到的,但是结构体中字段的偏移量是在编译时计算的。

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

当然,这些 IL 指令由 JIT 编译器转换为汇编,这可能会应用许多优化,例如避免多次加载相同的指针,但因编译器版本和是否启用 DEBUG 而异或发布版本。

例如,考虑以下结构:

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. 使用 5
  3. 将常量 int32 值 ldc.i4.5 压入堆栈
  4. 使用 ConsoleApp1.SomeVal::x 将该 int32 值存储到由 stfld int32 ConsoleApp1.SomeVal::x 定义的恒定偏移量的字段中

在使用 x 将它们添加到一起之前,您可以看到用于加载 yadd 字段的类似 IL 代码。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。