数组,堆和堆栈以及值类型

时间:2009-07-11 14:30:12

标签: c# arrays memory stack heap

int[] myIntegers;
myIntegers = new int[100];

在上面的代码中,是新的int [100]在堆上生成数组吗?从我通过c#读到的CLR,答案是肯定的。但我无法理解的是,数组中的实际int会发生什么。由于它们是值类型,我猜它们必须被装箱,因为我可以,例如,将myIntegers传递给程序的其他部分,如果它们一直留在堆栈上它会使堆栈混乱。还是我错了?我猜他们只是盒装了,并且只要数组存在就会活在堆上。

8 个答案:

答案 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个字节来存储其值。因此,RefTypeValType的值的内存如下所示:

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

现在,如果函数中有三个局部变量,类型为RefTypeValTypeint[],则如下所示:

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}的文章}

摘录:

  1. 每个局部变量(即在方法中声明的变量)都存储在堆栈中。这包括引用类型变量 - 变量本身位于堆栈上,但请记住,引用类型变量的值只是引用(或null),而不是对象本身。方法参数也计为局部变量,但如果使用ref修饰符声明它们,则它们不会获得自己的槽,而是与调用代码中使用的变量共享一个槽。有关详细信息,请参阅有关参数传递的文章。

  2. 引用类型的实例变量始终在堆上。这就是物体本身“存在”的地方。

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

  4. 每个静态变量都存储在堆上,无论它是在引用类型还是值类型中声明。无论创建多少个实例,总共只有一个插槽。 (不需要为该一个插槽创建任何实例。)确切存在于哪个堆中的详细信息很复杂,但在有关该主题的MSDN文章中有详细解释。

答案 7 :(得分:1)

这些插图描绘了@P Daddy的上述答案

enter image description here

enter image description here

我用我的风格说明了相应的内容。

enter image description here