数组,堆和堆栈和值types
int[] myIntegers; myIntegers = new int[100];
在上面的代码中,是新的int [100]在堆上生成数组? 从我读过的CLR通过C#,答案是肯定的。 但是我不能理解的是,在数组里面发生了什么。 因为它们是值types,所以我想它们必须被装箱,例如,将myInteger传递给程序的其他部分,并且如果它们始终保留在堆栈上,就会混乱堆栈。 还是我错了? 我想他们只是装盒子,只要arrays存在就会活在堆上。
您的数组分配在堆上,并且整数不是装箱的。
混淆的根源很可能是因为人们说引用types是在堆上分配的,值types是在堆栈上分配的。 这不是一个完全准确的表示。
所有本地variables和参数都分配在堆栈上。 这包括值types和引用types。 两者之间的区别只是存储在variables中。 不出所料,对于一个值types,types的值直接存储在variables中,而对于引用types,types的值存储在堆中,并且对该值的引用是存储在variables中的。
领域也是如此。 当为一个聚合types(一个类或一个结构)的实例分配内存时,它必须包含每个实例字段的存储。 对于引用types的字段,这个存储只包含一个对这个值的引用,这个值本身将会在堆上被分配。 对于值types字段,此存储保存实际值。
所以,给以下几种types:
class RefType{ public int I; public string S; public long L; } struct ValType{ public int I; public string S; public long L; }
每个这些types的值都需要16个字节的内存(假设32位字的大小)。 每个字段I
需要4个字节来存储它的值,字段S
需要4个字节来存储它的引用,而字段L
需要8个字节来存储它的值。 因此, RefType
和ValType
的值的内存如下所示:
0┌───────────────────┐ │我│ 4├───────────────────┤ │S│ 8├───────────────────┤ │L│ ││ 16└────────────────────┘
现在,如果在函数中有三个局部variables,types为RefType
, ValType
和int[]
,如下所示:
RefType refType; ValType valType; int[] intArray;
那么你的堆栈可能看起来像这样:
0┌───────────────────┐ │refType│ 4├───────────────────┤ │valType│ ││ ││ ││ 20├───────────────────┤ │intArray│ 24└────────────────────┘
如果您为这些局部variables赋值,如下所示:
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的值)的intArray
将如下所示:
0┌───────────────────┐ │4│ - arrays的长度 4├───────────────────┤ │300│ - intArray [0]` 8├───────────────────┤ │301│ - `intArray [1]` 12├───────────────────┤ │302│ - `intArray [2]` 16├───────────────────┤ │303│ - `intArray [3]` 20└───────────────────┘
现在,如果将intArray
传递给另一个函数,则推入堆栈的值将是0x4AA4C288,即数组的地址, 而不是数组的副本。
是的,数组将位于堆上。
arrays内的整数将不会被装箱。 仅仅因为堆中存在一个值types,并不一定意味着它将被装箱。 只有当一个值types(比如int)被分配给一个对象types的引用时,才会发生装箱。
例如
不包括:
int i = 42; myIntegers[0] = 42;
盒:
object i = 42; object[] arr = new object[10]; // no boxing here arr[0] = 42;
您可能还想查看Eric的这个主题的post:
为了理解发生了什么,下面是一些事实:
- 对象始终分配在堆上。
- 堆只包含对象。
- 值types可以分配在堆栈上,也可以分配给堆上的对象的一部分。
- 一个数组是一个对象。
- 数组只能包含值types。
- 对象引用是一个值types。
所以,如果你有一个整数数组,那么这个数组就被分配在堆上,它所包含的整数就是堆上的数组对象的一部分。 整数驻留在堆上的数组对象内,而不是单独的对象,所以它们没有被装箱。
如果你有一个string数组,它实际上是一个string引用数组。 作为引用是值types,它们将成为堆上的数组对象的一部分。 如果在数组中放置一个string对象,则实际上将对string对象的引用放在数组中,而string是堆上的单独对象。
我认为在你的问题的核心是对参考和价值types的误解。 这可能是每个.NET和Java开发人员都在苦苦挣扎的东西。
一个数组只是一个值列表。 如果它是一个引用types的数组(比如说一个string[]
),那么这个数组是一个引用到堆上各个string
对象的列表,因为引用是引用types的值 。 在内部,这些引用被实现为指向内存地址的指针。 如果你想看到这个,这样的数组在内存中(在堆上)看起来像这样:
[ 00000000, 00000000, 00000000, F8AB56AA ]
这是一个string
数组,其中包含对堆上string
对象的4个引用(这里的数字是hex的)。 目前,只有最后一个string
指向任何东西(分配时内存被初始化为全零),这个数组基本上就是C#代码的结果:
string[] strings = new string[4]; strings[3] = "something"; // the string was allocated at 0xF8AB56AA by the CLR
上面的数组将在32位程序中。 在一个64位的程序中,引用将是两倍大( F8AB56AA
将是00000000F8AB56AA
)。
如果你有一个值types的数组(比如一个int[]
),那么这个数组就是一个整数列表,因为值types的值就是它本身的值(因此也就是名字)。 这样一个数组的可视化将是这样的:
[ 00000000, 45FF32BB, 00000000, 00000000 ]
这是一个4整数的数组,其中只有第二个int被分配了一个值(1174352571,这是hex数的十进制表示),其余的整数是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字节的数字。
在存储值types的地方,只是Eric Lippert 在这里解释得非常好的一个实现细节,并不是由于值和引用types(这是行为上的差异)之间的差异而固有的。
当你将一些东西传递给一个方法(即引用types或值types)时,types值的副本实际上被传递给方法。 在引用types的情况下,这个值是一个引用(把它看作是指向一段内存的指针,尽pipe这也是一个实现细节),在值types的情况下,这个值本身就是事物。
// 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){}
只有在将值types转换为引用types时,才会出现拳击。 这个代码框:
object o = 5;
一堆整数被分配在堆上,没有什么比这更多的了。 myIntegers引用分配整数部分的开始。 该引用位于堆栈上。
如果您有一个引用types对象的数组,如位于堆栈上的对象typesmyObjects [],则会引用引用对象本身的一堆值。
总而言之,如果您将myInteger传递给某些函数,则只会将该引用传递给真正的一堆整数所分配的位置。
您的示例代码中没有装箱。
值types可以像堆栈中的数组一样存在于堆中。 数组在堆上分配,并存储ints,它们恰好是值types。 数组的内容被初始化为默认(int),恰好为零。
考虑一个包含值types的类:
class HasAnInt { int i; } HasAnInt h = new HasAnInt();
variablesh是指存在于堆上的HasAnInt的一个实例。 它恰好包含一个值types。 这完全没问题,'我只是碰巧住在一堆,因为它包含在一个类。 在这个例子中也没有拳击。
大家已经说过了,但是如果有人正在寻找一个关于堆,堆栈,局部variables和静态variables的清晰(但非官方)的示例和文档,请参考完整的Jon Skeet关于.NET中的内存的文章-哪里
摘抄:
-
每个本地variables(即在方法中声明的一个variables)都存储在堆栈中。 这包括引用typesvariables – variables本身在堆栈上,但请记住引用typesvariables的值只是引用(或null),而不是对象本身。 方法参数也作为局部variables计数,但是如果用ref修饰符声明它们,它们不会获得它们自己的插槽,而是与调用代码中使用的variables共享一个插槽。 查看我的关于parameter passing的文章以获取更多细节
-
引用types的实例variables始终在堆上。 这就是对象本身“生活”的地方。
-
值types的实例variables与声明值types的variables存储在相同的上下文中。 实例的内存插槽有效地包含实例中每个字段的插槽。 这意味着(给出前两点)在方法中声明的结构variables将始终在堆栈上,而作为类的实例字段的结构variables将在堆上。
-
无论是在引用types还是值types中声明,每个静态variables都存储在堆中。 总共只有一个插槽,无论创build多less个实例。 (虽然不需要为这个槽创build任何实例)。variables生存的具体堆的细节很复杂,但在有关这个主题的MSDN文章中有详细解释。