这些对象的引用在栈上还是堆上?

14

如果有人能告诉我是否理解得很好,我将不胜感激:

class X
{
   A a1=new A(); // reference on the stack, object value on the heap
   a1.VarA=5;    // on the stack - value type
   A a2=a1;      // reference on the stack, object value on the heap
   a2.VarA=10;   // on the stack - value type         
}

同时 a1a2 引用都在堆栈上,而它们所指向的 "对象" 值则存储在堆上。但是对于变量 VarA,它仍然是纯值类型吗?

class A
{
   int VarA;
}

3
由于这段代码无法编译,很难描述运行时如何处理它。所有这些语句是否都打算放在方法体内?它们是字段声明还是局部变量声明? - Eric Lippert
8个回答

28
您正在询问关于“实现细节”的问题,因此答案将取决于具体的实现。让我们考虑一个实际编译的程序版本:
class A { public int VarA; }
class X
{
    static void Main(string[] args)
    {
        A a1 = new A();
        a1.VarA = 5;
        A a2 = a1;
        a2.VarA = 10;
    }
}

在 Microsoft 的 CLR 4.0 上,以 C# 4.0 运行 Debug 模式时会发生以下情况。

此时堆栈帧指针已经被复制到寄存器 ebp 中:

这里我们为新对象分配堆内存。

A a1 = new A();
mov         ecx,382518h 
call        FFE6FD30 

该代码返回eax中一个堆对象的引用。我们将引用存储在ebp-48的堆栈槽中,它是一个临时槽,没有与任何名称相关联。请记住,a1尚未初始化。

mov         dword ptr [ebp-48h],eax 

现在我们将刚刚存储在堆栈中的引用复制到ecx寄存器中,这将被用作调用构造函数的"this"指针。

mov         ecx,dword ptr [ebp-48h] 

现在我们调用构造函数。

call        FFE8A518 

现在我们将存储在临时堆栈插槽中的引用再次复制到寄存器eax中。

mov         eax,dword ptr [ebp-48h] 

现在我们将eax中的引用复制到堆栈插槽ebp-40,即a1。

mov         dword ptr [ebp-40h],eax 

现在我们必须将a1获取到eax寄存器中:

a1.VarA = 5;
mov         eax,dword ptr [ebp-40h] 

记住,eax现在是a1引用的指向堆分配数据的地址。该对象的VarA字段位于对象中的第四个字节,因此我们将5存储到其中:

mov         dword ptr [eax+4],5 

现在我们将引用a1的副本复制到eax寄存器中,然后将其复制到a2的栈槽中,即ebp-44。

A a2 = a1;
mov         eax,dword ptr [ebp-40h] 
mov         dword ptr [ebp-44h],eax 

现在正如你所预期的那样,我们将a2放入eax中,然后将参考值转换为四个字节,并将0x0A写入VarA:

a2.VarA = 10;
mov         eax,dword ptr [ebp-44h] 
mov         dword ptr [eax+4],0Ah

那么,回答你的问题,对象的引用存储在栈中的三个位置:ebp-44、ebp-48和ebp-40。它们存储在eax和ecx寄存器中。对象的内存,包括其字段,存储在托管堆上。这全部是基于Microsoft CLR v4.0的x86调试版本。如果你想知道如何在其他配置中将东西存储在栈、堆和寄存器中,可能完全不同。引用可以全部存储在堆中,或全部存储在寄存器中;可能根本没有栈。这完全取决于jit编译器的作者决定如何实现IL语义。


这也取决于C#编译器的作者决定如何实现C#语义。局部变量(a1a2)可以作为托管类型中的字段实现,每个堆栈帧中只留下一个引用。我意识到在您的帖子评论中提出这个问题会引起一些不必要的争议,但我还是想提一下 :) - Jon Skeet
@Jon:确实如此。在编译器的IL生成阶段,我们产生的错误非常少;其中之一是“局部变量过多”——我不记得限制是多少,但大概是一个方法中不能有超过32K或64K个局部变量或临时变量。(显然,真正的代码不会出现这个问题,但机器生成的代码可能会出现。)我经常想,在这种情况下,我们应该开始将它们提升为字段,而不是产生错误。但这种情况太难以理解了,无法证明编写和测试代码的成本。 - Eric Lippert

10

严格来说,这是依赖于具体实现的。通常情况下,.NET开发者不需要关心这些事情。就我所知,微软的.NET实现中,值类型的变量被存储在栈上(当它们在方法内部声明时),而引用类型对象的数据则分配在托管堆上。但请记住,当一个值类型是类的字段时,包括所有值类型字段在内的类数据本身存储在堆上。 因此,请勿混淆语义(值类型vs引用类型)与分配规则。这些事情可能有关联,也可能没有。


2
class X 
{ 
    A a1=new A(); // reference on the stack, object value on the heap 
    a1.VarA=5;    // on the Heap- value type (Since it is inside a reference type)
    A a2=a1;      // reference on the stack, object value on the heap 
    a2.VarA=10;   // on the Heap - value type (Since it is inside a reference type)
}

2

我认为你可能有一些误解...

通常情况下,引用类型存储在堆上,值类型/局部变量我相信(可能是错误的)存储在栈上。然而,你的A1.VarA和A2.VarA示例是指向引用类型的字段 - 它与对象一起存储在堆上...


是的,但该字段的值是int类型,因此是值类型,对吧? - Petr
@Petr,所有字段都包含在引用类型A中,该类型位于堆上。 - Alex Budovski

2
在这种情况下,a1.VarA将在堆上,因为当您执行A a1 = new A()时,已分配了其空间。
如果您只是在函数中执行int i = 5;,它将放在堆栈上,但由于您明确声明a1应该分配在堆上,与其相关的所有值类型都将放置在堆上。

0

阅读Jeff Richter的CLR via C#,以完全理解此主题。


0

还记得在《C#深度剖析》中读到过:只有局部变量(在方法内声明的变量)和方法参数存储在堆栈中。像上面例子中的varA这样的实例变量存储在堆中。


3
请注意,作为 lambda 表达式或匿名方法的闭合本地变量以及位于迭代器块中的本地变量,在 Microsoft 实现的 C# 中不会存储在堆栈上。 - Eric Lippert

0

我也是新手,对C#不太熟悉。你的问题非常重要,我也曾思考过。所有文档都说,值类型存储在栈中,引用类型存储在堆中,但正如上面的人所说,这仅适用于方法内部的代码。在学习的过程中,我意识到所有程序代码都始于属于堆的实例的方法内部。因此,在概念上,栈并不等同于堆,就像所有文档混淆人们一样。栈机制仅存在于方法中...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接