Answers:
您的数组分配在堆上,并且没有将int装箱。
造成混淆的原因很可能是因为人们曾说过,引用类型是在堆上分配的,而值类型是在堆栈上分配的。这不是一个完全准确的表示。
所有局部变量和参数都分配在堆栈上。这包括值类型和引用类型。两者之间的区别仅在于变量中存储的内容。毫不奇怪,对于值类型,该类型的值直接存储在变量中,对于引用类型,该类型的值存储在堆中,对该值的引用就是存储在变量中的值。
字段也是如此。当为聚合类型(a class
或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├─────────────┤ │S│ 8├──────────────┤ │L│ ││ 16└──────────────┘
现在,如果你在一个函数中有三个局部变量,类型RefType
,ValType
和int[]
,就像这样:
RefType refType;
ValType valType;
int[] intArray;
那么您的堆栈可能看起来像这样:
0┌──────────────┐ │引用类型│ 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开发人员都在努力的事情。
数组只是值列表。如果它是引用类型的数组(例如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 在这里很好地解释的那样,不是值和引用类型之间的差异(行为上的差异)固有的。
当您将某些内容传递给方法(引用类型或值类型)时,该类型的值的副本实际上会传递给该方法。在引用类型的情况下,值是引用(将其视为指向内存的指针,尽管这也是实现细节),在值类型的情况下,值本身就是事物。
// 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文章中有详细说明。