int[] myIntegers;
myIntegers = new int[100];
在上面的代码中,是新的int [100]在堆上生成数组吗?从我通过c#读到的CLR,答案是肯定的。但我无法理解的是,数组中的实际int会发生什么。由于它们是值类型,我猜它们必须被装箱,因为我可以,例如,将myIntegers传递给程序的其他部分,如果它们一直留在堆栈上它会使堆栈混乱。还是我错了?我猜他们只是盒装了,并且只要数组存在就会活在堆上。
答案 0 :(得分:266)
你的数组是在堆上分配的,而int不是盒装的。
您混淆的原因很可能是因为人们已经说过在堆上分配了引用类型,并且在堆栈上分配了值类型。这不完全准确。
所有局部变量和参数都在堆栈上分配。这包括值类型和引用类型。两者之间的区别仅在于变量中存储。不出所料,对于值类型,类型的值直接存储在变量中,对于引用类型,类型的值存储在堆上,引用< / em>将此值存储在变量中。
对于字段也是如此。为聚合类型(类或结构)的实例分配内存时,它必须包含每个实例字段的存储。对于引用类型字段,此存储仅包含对该值的引用,该值本身稍后将在堆上分配。对于值类型字段,此存储保存实际值。
因此,给出以下类型:
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 ┌───────────────────┐ │ I │ 4 ├───────────────────┤ │ S │ 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 │ -- heap address of `refType` 4 ├───────────────────┤ │ 200 │ -- value of `valType.I` │ 0x4A984C10 │ -- heap address of `valType.S` │ 0x44556677 │ -- low 32-bits of `valType.L` │ 0x00112233 │ -- high 32-bits of `valType.L` 20 ├───────────────────┤ │ 0x4AA4C288 │ -- heap address of `intArray` 24 └───────────────────┘
地址0x4A963B68(值refType
)的内存类似于:
0 ┌───────────────────┐ │ 100 │ -- value of `refType.I` 4 ├───────────────────┤ │ 0x4A984D88 │ -- heap address of `refType.S` 8 ├───────────────────┤ │ 0x89ABCDEF │ -- low 32-bits of `refType.L` │ 0x01234567 │ -- high 32-bits of `refType.L` 16 └───────────────────┘
地址0x4AA4C288(值intArray
)的内存类似于:
0 ┌───────────────────┐ │ 4 │ -- length of array 4 ├───────────────────┤ │ 300 │ -- `intArray[0]` 8 ├───────────────────┤ │ 301 │ -- `intArray[1]` 12 ├───────────────────┤ │ 302 │ -- `intArray[2]` 16 ├───────────────────┤ │ 303 │ -- `intArray[3]` 20 └───────────────────┘
现在,如果你将intArray
传递给另一个函数,那么推入堆栈的值将是0x4AA4C288,即数组的地址,不是数组的副本。
答案 1 :(得分:23)
是的,数组将位于堆上。
数组中的整数不会被装箱。仅仅因为堆上存在值类型,并不一定意味着它将被装箱。仅当将值类型(例如int)分配给object类型的引用时,才会发生Boxing。
例如
不包装盒:
int i = 42;
myIntegers[0] = 42;
箱:
object i = 42;
object[] arr = new object[10]; // no boxing here
arr[0] = 42;
您可能还想查看Eric关于此主题的帖子:
答案 2 :(得分:19)
要了解发生了什么,这里有一些事实:
因此,如果您有一个整数数组,则该数组将在堆上分配,并且它包含的整数是堆上数组对象的一部分。
表示整数位于堆上的数组对象内,而不是单独的对象。如果你有一个字符串数组,它实际上是一个字符串引用数组。由于引用是值类型,它们将成为堆上数组对象的一部分。如果将一个字符串对象放在数组中,实际上是将引用放在数组中的字符串对象中,而字符串是堆上的一个单独对象。
答案 3 :(得分:9)
我认为问题的核心在于对参考和价值类型的误解。这可能是每个.NET和Java开发人员都在努力解决的问题。
数组只是一个值列表。如果它是引用类型的数组(比如string[]
)那么数组是对堆上各种string
对象的引用列表,因为引用是值参考类型。在内部,这些引用实现为指向内存中地址的指针。如果你想想象这个,这样的数组在内存中(在堆上)看起来像这样:
[ 00000000, 00000000, 00000000, F8AB56AA ]
这是一个string
数组,其中包含对堆上string
个对象的4个引用(这里的数字是十六进制的)。目前,只有最后string
实际指向任何内容(内存在分配时初始化为所有零),此数组基本上是C#中此代码的结果:
string[] strings = new string[4];
strings[3] = "something"; // the string was allocated at 0xF8AB56AA by the CLR
上面的数组将是一个32位程序。在64位程序中,引用将是两倍大(F8AB56AA
将是00000000F8AB56AA
)。
如果你有一个值类型数组(比如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 ]
由于short
的值是2字节数。
存储值类型时,只是一个实现细节,正如Eric Lippert非常清楚地解释here,而不是值和引用类型之间的差异所固有的(这是行为上的差异)。
当您将某些内容传递给方法(是引用类型或值类型)时,该类型的值的副本实际上会传递给该方法。在引用类型的情况下,值是一个引用(将其视为指向一块内存的指针,尽管这也是一个实现细节),在值类型的情况下,价值就是事物本身。
// 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;
答案 4 :(得分:1)
在堆上分配一个整数数组,仅此而已。 myIntegers引用分配整数的部分的开头。该引用位于堆栈中。
如果你有一个引用类型对象数组,比如对象类型,位于堆栈上的myObjects []将引用引用它们自己的对象的一堆值。
总而言之,如果将myIntegers传递给某些函数,则只将引用传递给分配了真正整数串的位置。
答案 5 :(得分:1)
您的示例代码中没有装箱。
值类型可以像在int数组中那样存在于堆中。数组在堆上分配,它存储整数,这恰好是值类型。数组的内容初始化为default(int),恰好为零。
考虑一个包含值类型的类:
class HasAnInt
{
int i;
}
HasAnInt h = new HasAnInt();
变量h指的是生活在堆上的HasAnInt实例。它恰好包含一个值类型。那是完全可以的,'我'碰巧住在堆上,因为它包含在一个类中。在这个例子中也没有拳击。
答案 6 :(得分:1)
每个人都说过足够,但如果有人正在寻找关于堆,堆栈,局部变量和静态变量的清晰(但非官方)的示例和文档,请参阅完整的Jon Skeet关于{{3}的文章}
的摘录:强> 的
每个局部变量(即在方法中声明的变量)都存储在堆栈中。这包括引用类型变量 - 变量本身位于堆栈上,但请记住,引用类型变量的值只是引用(或null),而不是对象本身。方法参数也计为局部变量,但如果使用ref修饰符声明它们,则它们不会获得自己的槽,而是与调用代码中使用的变量共享一个槽。有关详细信息,请参阅有关参数传递的文章。
引用类型的实例变量始终在堆上。这就是物体本身“存在”的地方。
值类型的实例变量存储在与声明值类型的变量相同的上下文中。实例的内存插槽有效地包含实例中每个字段的插槽。这意味着(给出前两点)在方法中声明的结构变量将始终在堆栈上,而作为类的实例字段的结构变量将在堆上。
每个静态变量都存储在堆上,无论它是在引用类型还是值类型中声明。无论创建多少个实例,总共只有一个插槽。 (不需要为该一个插槽创建任何实例。)确切存在于哪个堆中的详细信息很复杂,但在有关该主题的MSDN文章中有详细解释。
答案 7 :(得分:1)