数组,堆和堆栈和值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个字节来存储它的值。 因此, RefTypeValType的值的内存如下所示:

  0┌───────────────────┐
    │我│
  4├───────────────────┤
    │S│
  8├───────────────────┤
    │L│
    ││
 16└────────────────────┘

现在,如果在函数中有三个局部variables,types为RefTypeValTypeint[] ,如下所示:

 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中的内存的文章-哪里

摘抄:

  1. 每个本地variables(即在方法中声明的一个variables)都存储在堆栈中。 这包括引用typesvariables – variables本身在堆栈上,但请记住引用typesvariables的值只是引用(或null),而不是对象本身。 方法参数也作为局部variables计数,但是如果用ref修饰符声明它们,它们不会获得它们自己的插槽,而是与调用代码中使用的variables共享一个插槽。 查看我的关于parameter passing的文章以获取更多细节

  2. 引用types的实例variables始终在堆上。 这就是对象本身“生活”的地方。

  3. 值types的实例variables与声明值types的variables存储在相同的上下文中。 实例的内存插槽有效地包含实例中每个字段的插槽。 这意味着(给出前两点)在方法中声明的结构variables将始终在堆栈上,而作为类的实例字段的结构variables将在堆上。

  4. 无论是在引用types还是值types中声明,每个静态variables都存储在堆中。 总共只有一个插槽,无论创build多less个实例。 (虽然不需要为这个槽创build任何实例)。variables生存的具体堆的细节很复杂,但在有关这个主题的MSDN文章中有详细解释。