我正在读一本书,上面写着:
表示结构实例的变量不包含指向实例的指针;变量包含实例本身的字段。因为变量包含实例的字段,所以不必取消引用指针来操作实例的字段。以下代码演示了引用类型和值类型如何区别
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
注意:
- 使用
ldloca.s V_0
将结构的地址推送到堆栈上 - 使用
ldc.i4.5
将常量int32值5
推送到堆栈上 - 将该int32值存储到由
ConsoleApp1.SomeVal::x
使用stfld int32 ConsoleApp1.SomeVal::x
定义的常量偏移处的字段中
您可以看到类似的IL代码,用于在使用add
将x
和y
字段相加之前加载它们。
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目标,代码在概念上与此处显示的代码类似。