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


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

在上面的代码中,是否是new int [100]在堆上生成数组?根据我通过c#进行的CLR阅读,答案是肯定的。但是我不明白的是,数组内部的实际int发生了什么。因为它们是值类型,所以我想必须将它们装箱,例如,我可以将myIntegers传递给程序的其他部分,如果一直将它们留在程序中,则会使堆栈混乱。还是我错了?我猜想它们会被装箱,并且只要数组存在就可以在堆中生存。

Answers:


289

您的数组分配在堆上,并且没有将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个字节存储其值。因此,对于两者的价值存储RefTypeValType看起来像这样:

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

现在,如果你在一个函数中有三个局部变量,类型RefTypeValTypeint[],就像这样:

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数组的地址,而不是数组的副本。


52
我注意到所有本地变量都存储在堆栈中的说法是不准确的。作为匿名函数的外部变量的局部变量存储在堆中。迭代器块的局部变量存储在堆中。异步块的局部变量存储在堆中。已注册的局部变量既不存储在堆栈中,也不存储在堆中。被忽略的局部变量既不存储在堆栈中,也不存储在堆中。
埃里克·利珀特

5
大声笑,永远是挑剔的人,利珀特先生。:)我不得不指出,除后两种情况外,所谓的“本地”在编译时不再是本地。该实现将它们提升为类成员的状态,这是将它们存储在堆中的唯一原因。因此,这只是一个实现细节(更精简)。当然,寄存器存储是一个更低层的实现细节,省略并不重要。
P Daddy

3
当然,我的整个文章都是实现细节,但是,正如您确定的那样,我相信这都是试图将变量的概念分开。可以将变量(称为局部变量,字段,参数等)存储在堆栈,堆或其他实现定义的位置上,但这并不重要。重要的是该变量是直接存储它表示的值,还是仅存储到其他位置的对该值的引用。这很重要,因为它会影响复制语义:复制该变量会复制其值还是其地址。
P爸爸

16
显然,您对“局部变量”的含义与我有所不同。您似乎相信“局部变量”的特征在于其实现细节。我在C#规范中知道的任何内容都不能证明这种信念。局部变量实际上是一个块,其内声明的变量在范围上仅与各地块相关联的声明空间。我向您保证,作为实现细节的局部变量,根据C#的规则,这些局部变量仍然是局部变量,仍然是局部变量
埃里克·利珀特

15
话虽如此,您的回答当然总的来说是很好的;在概念上与变量不同的观点是这一点,因为它是基本的,因此需要尽可能频繁地大声提出。但是很多人相信关于他们的最奇怪的神话!你们为打好仗而高兴。
埃里克·利珀特

23

是的,数组将位于堆上。

数组内部的整数将不被装箱。仅仅因为值类型存在于堆中,并不一定意味着它将被装箱。仅当将值类型(例如int)分配给类型对象的引用时,才会进行装箱。

例如

不装箱:

int i = 42;
myIntegers[0] = 42;

包装盒:

object i = 42;
object[] arr = new object[10];  // no boxing here 
arr[0] = 42;

您可能还需要查看Eric关于此主题的文章:


1
但是我不明白。不应在堆栈上分配值类型吗?还是可以同时在堆或堆栈上分配值和引用类型,仅仅是它们通常只存储在一个地方或另一个地方?
吞噬了极乐世界09年

4
@Jorge,没有引用类型包装器/容器的值类型将存在于堆栈中。但是,一旦在引用类型的容器中使用它,它将驻留在堆中。数组是引用类型,因此int的内存必须在堆中。
JaredPar

2
@Jorge:引用类型仅存在于堆中,而不存在于堆栈中。相反,不可能(以可验证的代码)将指向堆栈位置的指针存储到引用类型的对象中。
安东·泰克(09年

1
我认为您打算将i分配给arr [0]。常量分配仍将导致装箱“ 42”,但是您创建了i,因此您也可以使用它;-)
Marcus Griep 09年

@AntonTykhyy:我知道说CLR不能进行转义分析是没有规则的。如果它检测到在创建该对象的函数的生命周期内将永不引用该对象,则在堆栈上构造该对象是完全合法的,甚至是更可取的,无论该对象是否为值类型。“值类型”和“引用类型”基本上描述了变量所占用的内存内容,而不是关于对象所在位置的严格规则。
cHao 2013年

21

要了解发生了什么,这里有一些事实:

  • 对象总是分配在堆上。
  • 堆仅包含对象。
  • 值类型要么在堆栈上分配,要么在堆上作为对象的一部分。
  • 数组是一个对象。
  • 数组只能包含值类型。
  • 对象引用是一种值类型。

因此,如果您有一个整数数组,则该数组将在堆上分配,并且它包含的整数是堆上数组对象的一部分。整数位于堆上的数组对象内部,而不是作为单独的对象,因此不会装箱。

如果您有一个字符串数组,则实际上是一个字符串引用数组。由于引用是值类型,因此它们将成为堆上数组对象的一部分。如果将字符串对象放入数组中,则实际上是将对字符串对象的引用放入数组中,并且该字符串是堆上的单独对象。


是的,引用的行为与值类型完全一样,但我注意到它们通常不是那样调用的,也不包含在值类型中。例如,请参阅(但还有更多类似的内容)msdn.microsoft.com/en-us/library/s1ax56ch.aspx
Henk Holterman,2009年

@Henk:是的,没错,引用没有在值类型变量中列出,但是当涉及到如何为它们分配内存时,它们在各个方面都是值类型,这对于理解内存分配方式非常有用。都适合。:)
Guffa

我怀疑第五点,“数组只能包含值类型”。字符串数组呢?string []字符串=新的string [4];
Sunil Purushothaman

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位程序,引用是两倍大(F8AB56AA00000000F8AB56AA)。

如果您有值类型的数组(说的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;

我相信“实施细节”应为font-size:50px。;)
sisve

2

这些是@P Daddy的上述答案的插图

在此处输入图片说明

在此处输入图片说明

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

在此处输入图片说明


@P爸爸,我做了插图。请检查是否有错误的部分。我还有其他问题。1.当我创建4个长度为int的类型数组时,长度信息(4)是否也总是存储在内存中?
YoungMin Park

2.在第二个插图中,复制的数组地址存储在哪里?intArray地址存储在同一堆栈区域中吗?是其他堆栈,而是相同类型的堆栈吗?它是另一种类型的堆栈吗?3.低32位/高32位是什么意思?4.使用new关键字在堆栈上分配值类型(在本示例中为结构)时,返回值是多少?它也是地址吗?当我通过该语句Console.WriteLine(valType)检查时,它将显示完全限定名称,例如ConsoleApp.ValType之类的对象。
YoungMin Park

5. valType.I = 200; 此语句是否表示我获得了valType的地址,通过该地址我访问了I,然后在那里我存储了200,但“在堆栈上”。
YoungMin Park

1

整数数组分配在堆上,仅此而已。myIntegers引用分配int的部分的开头。该引用位于堆栈上。

如果您有一个引用类型对象的数组,例如对象类型,则位于堆栈上的myObjects []将引用一堆值,这些值引用了它们自己的对象。

总而言之,如果将myIntegers传递给某些函数,则仅将引用传递给分配了真正的整数串的位置。


1

您的示例代码中没有装箱。

值类型可以像在整数数组中那样生活在堆中。数组在堆上分配,并存储整数,它们恰好是值类型。数组的内容被初始化为default(int),它恰好为零。

考虑一个包含值类型的类:


    class HasAnInt
    {
        int i;
    }

    HasAnInt h = new HasAnInt();

变量h指向驻留在堆上的HasAnInt的实例。它恰好包含一个值类型。完全可以,“ i”恰好生活在堆中,因为它包含在类中。此示例中也没有拳击。


1

每个人都说够了,但是如果有人在寻找有关堆,堆栈,局部变量和静态变量的清晰(但非官方)示例和文档,请参阅完整的Jon Skeet关于.NET中的内存的文章-会发生什么哪里

摘抄:

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

  2. 引用类型的实例变量始终在堆上。那就是对象本身“生存”的地方。

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.