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

dev*_*ium 129 c# memory arrays heap stack

int[] myIntegers;
myIntegers = new int[100];
Run Code Online (Sandbox Code Playgroud)

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

P D*_*ddy 276

您的数组是在堆上分配的,而int不是盒装的.

您混淆的原因可能是因为人们已经说过在堆上分配了引用类型,并且在堆栈上分配了值类型.这不是一个完全准确的表示.

所有局部变量和参数都在堆栈上分配.这包括值类型和引用类型.两者之间的区别仅在于存储在变量中的内容.不出所料,对于一个值类型,则的类型的直接存储在变量,和用于参考的类型,该类型的值被存储在堆上,和一个参考于该值是什么被存储在变量中.

对于字段也是如此.为聚合类型(类或结构)的实例分配内存时,它必须包含每个实例字段的存储.对于引用类型字段,此存储仅包含对该值的引用,该值本身稍后将在堆上分配.对于值类型字段,此存储保存实际值.

因此,给定以下类型:

class RefType{
    public int    I;
    public string S;
    public long   L;
}

struct ValType{
    public int    I;
    public string S;
    public long   L;
}
Run Code Online (Sandbox Code Playgroud)

每种类型的值都需要16个字节的存储器(假设32位字大小).I每种情况下的字段需要4个字节来存储其值,该字段S需要4个字节来存储其引用,该字段L需要8个字节来存储其值.因此,对于两者的价值存储RefTypeValType看起来像这样:

 0 ?????????????????????
   ?        I          ?
 4 ?????????????????????
   ?        S          ?
 8 ?????????????????????
   ?        L          ?
   ?                   ?
16 ?????????????????????

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

RefType refType;
ValType valType;
int[]   intArray;
Run Code Online (Sandbox Code Playgroud)

然后你的堆栈可能如下所示:

 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;
Run Code Online (Sandbox Code Playgroud)

然后你的堆栈可能看起来像这样:

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

  • 我注意到所有局部变量都存储在堆栈中的语句是不准确的.作为匿名函数的外部变量的局部变量存储在堆上.迭代器块的局部变量存储在堆上.异步块的局部变量存储在堆上.已注册的本地变量既不存储在堆栈中,也不存储在堆中.省略的局部变量既不存储在堆栈中,也不存储在堆中. (49认同)
  • 显然,你对我作为"局部变量"意味着什么有不同的看法.您似乎相信"局部变量"的特点是其*实现细节*.在C#规范中我所知道的任何事情都没有证明这种信念.局部变量实际上是在块内声明的变量,其*name*在范围*中仅在与块关联的声明空间中.我向你保证,根据C#规则,作为实现细节的局部变量仍然是闭包类的字段,仍然是*局部变量*. (16认同)
  • 那就是说,你的答案当然很棒;*值*在概念上与*变量*不同的点是需要尽可能频繁和大声地制作的点,因为它是基础.但是很多人都相信关于他们的最奇怪的神话!如此善于打击好战斗. (15认同)
  • LOL,总是挑剔者,Lippert先生.:)我觉得有必要指出,除了后两种情况,所谓的"本地人"在编译时不再是本地人.实现将它们提升到类成员的状态,这是它们存储在堆上的唯一原因.所以它只是一个实现细节(窃笑).当然,寄存器存储是一个更低级别的实现细节,而elision不计算在内. (5认同)
  • 当然,我的整个帖子都是实现细节,但是,正如我相信你已经意识到的那样,它们都试图将**变量**和**值**的概念分开.变量(称为本地,字段,参数,等等)可以存储在堆栈,堆或其他一些实现定义的位置,但这并不是真正重要的.重要的是,该变量是直接存储它所代表的值,还是仅存储在其他位置的对该值的引用.这很重要,因为它会影响复制语义:复制该变量是复制其值还是其地址. (3认同)

Jar*_*Par 23

是的,数组将位于堆上.

数组中的int不会被装箱.仅仅因为堆上存在值类型,并不一定意味着它将被装箱.仅当将值类型(例如int)分配给object类型的引用时,才会发生Boxing.

例如

不包装盒:

int i = 42;
myIntegers[0] = 42;
Run Code Online (Sandbox Code Playgroud)

盒:

object i = 42;
object[] arr = new object[10];  // no boxing here 
arr[0] = 42;
Run Code Online (Sandbox Code Playgroud)

您可能还想查看Eric关于此主题的帖子:

  • @Jorge,没有引用类型包装器/容器的值类型将存在于堆栈中.但是,一旦它在引用类型容器中使用,它将存在于堆中.数组是引用类型,因此int的内存必须在堆中. (4认同)
  • @Jorge:引用类型只存在于堆中,永远不会存在于堆栈中.相反,不可能(在可验证的代码中)将指向堆栈位置的指针存储到引用类型的对象中. (2认同)

Guf*_*ffa 20

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

  • 始终在堆上分配对象.
  • 堆只包含对象.
  • 值类型要么在堆栈上分配,要么在堆上的对象的一部分上分配.
  • 数组是一个对象.
  • 数组只能包含值类型.
  • 对象引用是值类型.

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

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


Jul*_*anR 9

我认为问题的核心在于对参考和价值类型的误解.这可能是每个.NET和Java开发人员都在努力解决的问题.

数组只是一个值列表.如果它是引用类型的数组(比如说a 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
Run Code Online (Sandbox Code Playgroud)

上面的数组将是一个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
Run Code Online (Sandbox Code Playgroud)

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){}
Run Code Online (Sandbox Code Playgroud)

仅当您值类型转换为引用类型时才会出现限制.这个代码框:

object o = 5;
Run Code Online (Sandbox Code Playgroud)