13

我很想知道 C# 对象引用是如何在运行时(在 .NET CLR 中)在内存中表示的。想到的一些问题是:

  1. 对象引用占用多少内存?在类的范围与方法的范围中定义时是否有所不同?它所在的位置是否会根据此范围(堆栈与堆)而有所不同?

  2. 对象引用中维护的实际数据是什么?它只是一个指向它所指对象的内存地址还是更多?这是否根据它是否在类或方法的范围内定义而有所不同?

  3. 与上面相同的问题,但这次是在讨论对引用的引用时,例如当对象引用通过引用传递给方法时。1 和 2 的答案如何变化?

4

2 回答 2

14

.NET 堆和堆栈 这是对堆栈和堆如何工作的彻底处理。

C# 和许多其他使用堆的 OOP 语言在一般参考语言中使用句柄而不是指针在这种情况下进行引用(C# 也能够使用指针!)指针类比适用于一些一般概念,但这个概念模型分解为以下问题这。请参阅 Eric Lippert 关于此主题的出色文章句柄不是地址

说句柄是指针的大小是不恰当的。(尽管可能巧合地相同)句柄是对象的别名,并不要求它们是对象的正式地址。

在这种情况下,CLR 碰巧使用真实地址作为句柄:来自上面的链接:

... CLR 实际上确实将托管对象引用实现为垃圾收集器拥有的对象的地址,但这是一个实现细节。

所以是的,一个句柄在 32 位架构上可能是 4 个字节,在 64 字节架构上可能是 8 个字节,但这不是“肯定的”,也不是直接因为指针。值得注意的是,根据编译器实现和使用的地址范围,某些类型的指针的大小可能不同

在所有这些上下文中,您可能可以通过指针类比对其进行建模,但重要的是要意识到句柄不需要是地址。如果将来 CLR 愿意,CLR 可以选择更改这一点,而 CLR 的消费者不应该知道更多。

这个微妙点的最终驱动力:

这是一个 C# 指针:

int* myVariable;

这是一个 C# 句柄:

object myVariable;

它们是不相同的。

你可以在指针上做数学运算,而你不应该用 Handles 做这些事情。如果您的句柄恰好像指针一样实现,并且您将其用作指针,则您在某些方面滥用了句柄,这可能会给您带来麻烦。

于 2012-02-29T02:21:21.357 回答
13

如果您了解 C/C++ 指针,则此答案最容易理解。指针只是一些数据的内存地址。

  1. 对象引用应该是指针的大小,通常在 32 位 CPU 上为 4 个字节,在 64 位 CPU 上为 8 个字节。无论在哪里定义都是一样的。它生活在哪里取决于它被定义的地方。如果它是一个类的字段,它将驻留在它所属的对象的堆中。如果是静态字段,则位于堆中不受垃圾回收影响的特殊部分。如果它是一个局部变量,它就存在于堆栈中。

  2. 对象引用只是一个指针,可以将其可视化为包含对象在内存中的地址的 int 或 long。无论在哪里定义都是一样的。

  3. 这被实现为指向指针的指针。数据是一样的——只是一个内存地址。但是,在给定的内存地址处没有对象。取而代之的是另一个内存地址,它是对对象的原始引用。这就是允许修改参考参数的原因。通常,参数在其方法完成时消失。由于对对象的引用不是参数,因此对该引用的更改将保留。对引用的引用将消失,但引用不会消失。这是传递参考参数的目的。

您应该知道的一件事是,值类型存储在适当的位置(没有内存地址,而是直接存储在内存地址所在的位置 - 请参阅#1)。当它们被传递给一个方法时,会制作一个副本并在该方法中使用该副本。当它们通过引用传递时,将传递一个内存地址,该地址定位内存中的值类型,允许对其进行更改。

编辑:正如 dlev 指出的那样,这些答案不是硬性规定,因为没有规定必须如此。.NET 可以随意实现这些问题。这是最有可能实现它的方法,因为这就是 Intel CPU 在内部的工作方式,因此使用任何其他方法都可能效率低下。

希望我没有让您感到困惑,但请随时询问您是否需要澄清。

于 2012-02-29T00:42:49.470 回答