int[] myIntegers;
myIntegers = new int[100];
在上面的代码中,new int[100] 是在堆上生成数组吗?根据我通过 c# 在 CLR 上阅读的内容,答案是肯定的。但我不明白的是,数组内部的实际 int 会发生什么。由于它们是值类型,我猜它们必须被装箱,例如,我可以将 myIntegers 传递给程序的其他部分,如果它们一直留在堆栈上,它会弄乱堆栈. 还是我错了?我猜他们只是被装箱了,只要数组存在,它们就会一直存在于堆上。
int[] myIntegers;
myIntegers = new int[100];
在上面的代码中,new int[100] 是在堆上生成数组吗?根据我通过 c# 在 CLR 上阅读的内容,答案是肯定的。但我不明白的是,数组内部的实际 int 会发生什么。由于它们是值类型,我猜它们必须被装箱,例如,我可以将 myIntegers 传递给程序的其他部分,如果它们一直留在堆栈上,它会弄乱堆栈. 还是我错了?我猜他们只是被装箱了,只要数组存在,它们就会一直存在于堆上。
您的数组是在堆上分配的,并且整数没有装箱。
您混淆的根源可能是因为人们说引用类型是在堆上分配的,而值类型是在堆栈上分配的。这不是一个完全准确的表示。
所有局部变量和参数都在堆栈上分配。这包括值类型和引用类型。两者之间的区别仅在于变量中存储的内容。不出所料,对于值类型,该类型的值直接存储在变量中,而对于引用类型,该类型的值存储在堆上,对该值的引用就是存储在变量中的内容。
字段也是如此。class
当为聚合类型(a或 a )的实例分配内存时struct
,它必须包括其每个实例字段的存储空间。对于引用类型字段,此存储仅保存对值的引用,该值本身稍后会在堆上分配。对于值类型字段,此存储保存实际值。
因此,给定以下类型:
class RefType{
public int I;
public string S;
public long L;
}
struct ValType{
public int I;
public string S;
public long L;
}
每种类型的值都需要 16 字节的内存(假设字长为 32 位)。I
在每种情况下,字段需要 4 个字节来存储它的值,字段需要S
4 个字节来存储它的引用,而字段L
需要 8 个字节来存储它的值。因此两者的值的内存RefType
和ValType
看起来像这样:
0 ┌───────────────────┐ │ 我 │ 4 ├───────────────────┤ │ 小号 │ 8 ├───────────────────┤ │ L │ │ │ 16 └────────────────────┘
现在,如果您在一个函数中有三个局部变量,类型分别RefType
为ValType
、 和int[]
,如下所示:
RefType refType;
ValType valType;
int[] intArray;
那么您的堆栈可能如下所示:
0 ┌───────────────────┐ │ refType │ 4 ├───────────────────┤ │ valType │ │ │ │ │ │ │ 20 ├───────────────────┤ │ intArray │ 24 └────────────────────┘
如果您为这些局部变量赋值,如下所示:
refType = new RefType();
refType.I = 100;
refType.S = "refType.S";
refType.L = 0x0123456789ABCDEF;
valType = new ValType();
valType.I = 200;
valType.S = "valType.S";
valType.L = 0x0011223344556677;
intArray = new int[4];
intArray[0] = 300;
intArray[1] = 301;
intArray[2] = 302;
intArray[3] = 303;
那么你的堆栈可能看起来像这样:
0 ┌───────────────────┐ │ 0x4A963B68 │ -- `refType`的堆地址 4 ├───────────────────┤ │ 200 │ -- `valType.I` 的值 │ 0x4A984C10 │ -- `valType.S`的堆地址 │ 0x44556677 │ -- `valType.L` 的低 32 位 │ 0x00112233 │ -- `valType.L` 的高 32 位 20 ├───────────────────┤ │ 0x4AA4C288 │ -- `intArray` 的堆地址 24 └────────────────────┘
0x4A963B68
地址(的值)的内存refType
将类似于:
0 ┌───────────────────┐ │ 100 │ -- `refType.I` 的值 4 ├───────────────────┤ │ 0x4A984D88 │ -- `refType.S`的堆地址 8 ├───────────────────┤ │ 0x89ABCDEF │ -- `refType.L` 的低 32 位 │ 0x01234567 │ -- `refType.L` 的高 32 位 16 └────────────────────┘
0x4AA4C288
地址(的值)的内存intArray
将类似于:
0 ┌───────────────────┐ │ 4 │ -- 数组长度 4 ├───────────────────┤ │ 300 │ -- `intArray[0]` 8 ├───────────────────┤ │ 301 │ -- `intArray[1]` 12 ├───────────────────┤ │ 302 │ -- `intArray[2]` 16 ├────────────────────┤ │ 303 │ -- `intArray[3]` 20 └───────────────────┘
现在,如果您传递intArray
给另一个函数,则压入堆栈的值将是0x4AA4C288
数组的地址,而不是数组的副本。
是的,数组将位于堆上。
数组内的整数不会被装箱。仅仅因为堆上存在值类型,并不一定意味着它会被装箱。只有将值类型(例如 int)分配给类型对象的引用时,才会发生装箱。
例如
不装箱:
int i = 42;
myIntegers[0] = 42;
盒子:
object i = 42;
object[] arr = new object[10]; // no boxing here
arr[0] = 42;
您可能还想查看 Eric 关于此主题的帖子:
要了解正在发生的事情,这里有一些事实:
因此,如果您有一个整数数组,则该数组在堆上分配,并且它包含的整数是堆上数组对象的一部分。整数驻留在堆上的数组对象中,而不是作为单独的对象,因此它们没有被装箱。
如果你有一个字符串数组,它实际上是一个字符串引用数组。由于引用是值类型,它们将成为堆上数组对象的一部分。如果你把一个字符串对象放到数组中,其实就是把这个字符串对象的引用放到了数组中,而字符串就是堆上的一个单独的对象。
我认为您问题的核心在于对引用和值类型的误解。这可能是每个 .NET 和 Java 开发人员都在努力解决的问题。
数组只是值的列表。如果它是一个引用类型的数组(比如 a ),那么该数组是对堆上string[]
各种对象的引用列表,因为引用是引用类型的值。在内部,这些引用被实现为指向内存地址的指针。如果您希望对此进行可视化,这样的数组在内存中(在堆上)将如下所示:string
[ 00000000, 00000000, 00000000, F8AB56AA ]
这是一个string
包含 4 个对string
堆上对象的引用的数组(这里的数字是十六进制的)。目前,只有最后一个string
实际上指向任何东西(内存在分配时初始化为全零),这个数组基本上是 C# 中这段代码的结果:
string[] strings = new string[4];
strings[3] = "something"; // the string was allocated at 0xF8AB56AA by the CLR
上述数组将在 32 位程序中。在 64 位程序中,引用将是两倍大(F8AB56AA
将是00000000F8AB56AA
)。
如果您有一个值类型数组(例如 an int[]
),则该数组是整数列表,因为值类型的值就是值本身(因此得名)。这种数组的可视化是这样的:
[ 00000000, 45FF32BB, 00000000, 00000000 ]
这是一个由 4 个整数组成的数组,其中只有第二个 int 被分配了一个值(到 1174352571,这是该十六进制数的十进制表示),其余整数将为 0(就像我说的,内存被初始化为零十六进制的 00000000 是十进制的 0)。产生这个数组的代码是:
int[] integers = new int[4];
integers[1] = 1174352571; // integers[1] = 0x45FF32BB would be valid too
该int[]
数组也将存储在堆上。
作为另一个示例,short[4]
数组的内存如下所示:
[ 0000, 0000, 0000, 0000 ]
因为 a 的值是short
一个 2 字节的数字。
存储值类型的位置只是 Eric Lippert在这里很好解释的实现细节,而不是值和引用类型之间的差异(这是行为差异)所固有的。
当您将某些内容传递给方法(无论是引用类型还是值类型)时,该类型的值的副本实际上会传递给该方法。在引用类型的情况下,值是一个引用(将其视为指向一块内存的指针,尽管这也是一个实现细节),而在值类型的情况下,值就是事物本身。
// Calling this method creates a copy of the *reference* to the string
// and a copy of the int itself, so copies of the *values*
void SomeMethod(string s, int i){}
只有将值类型转换为引用类型时才会发生装箱。此代码框:
object o = 5;
每个人都说得够多了,但是如果有人正在寻找关于堆、堆栈、局部变量和静态变量的清晰(但非官方)示例和文档,请参阅完整的 Jon Skeet 的关于.NET 内存的文章 - 发生了什么在哪里
摘抄:
每个局部变量(即在方法中声明的变量)都存储在堆栈中。这包括引用类型变量——变量本身在堆栈上,但请记住,引用类型变量的值只是一个引用(或 null),而不是对象本身。方法参数也算作局部变量,但如果使用 ref 修饰符声明它们,它们不会获得自己的槽,而是与调用代码中使用的变量共享一个槽。有关详细信息,请参阅我关于参数传递的文章。
引用类型的实例变量总是在堆上。那就是对象本身“生活”的地方。
值类型的实例变量存储在与声明值类型的变量相同的上下文中。实例的内存槽有效地包含实例内每个字段的槽。这意味着(鉴于前两点)在方法中声明的结构变量将始终在堆栈上,而作为类的实例字段的结构变量将在堆上。
每个静态变量都存储在堆上,无论它是在引用类型还是值类型中声明的。无论创建多少实例,总共只有一个插槽。(虽然不需要为该插槽创建任何实例。)变量存在于哪个堆的详细信息很复杂,但在有关该主题的 MSDN 文章中进行了详细解释。
在堆上分配了一个整数数组,仅此而已。myIntegers 引用分配整数的部分的开头。该引用位于堆栈上。
如果您有一个引用类型对象数组,例如位于堆栈上的 Object 类型,myObjects[] 将引用一组引用对象本身的值。
总而言之,如果将 myIntegers 传递给某些函数,则只会将引用传递给分配真正的整数束的位置。
您的示例代码中没有装箱。
值类型可以像在 int 数组中一样存在于堆上。数组在堆上分配,它存储整数,它恰好是值类型。数组的内容被初始化为 default(int),恰好为零。
考虑一个包含值类型的类:
class HasAnInt
{
int i;
}
HasAnInt h = new HasAnInt();
变量 h 指的是位于堆上的 HasAnInt 实例。它恰好包含一个值类型。没关系,'i' 只是碰巧生活在堆上,因为它包含在一个类中。此示例中也没有装箱。