Arrays, heap and stack and value types

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

In the above code, is new int[100] generating the array on the heap? From what I've read on CLR via c#, the answer is yes. But what I can't understand, is what happens to the actual int's inside the array. As they are value types, I'd guess they'd have to be boxed, as I can, for example, pass myIntegers to other parts of the program and it'd clutter up the stack if they were left on it all the time. Or am I wrong? I'd guess they'd just be boxed and would live on the heap for as long the array existed.

62720 次浏览

是的,数组将位于堆上。

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

比如说

不打拳击:

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

方格:

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

你可能也想看看 Eric 关于这个主题的文章:

在堆上分配一个整数数组,不多也不少。MyIntegers 引用分配 int 的部分的开头。该引用位于堆栈上。

如果您有一个引用类型对象的数组,比如 Object 类型,那么位于堆栈上的 myObjects []将引用引用对象本身的一组值。

总而言之,如果将 myIntegers 传递给某些函数,那么只需将引用传递到分配实际整数簇的位置。

示例代码中没有装箱。

值类型可以像在 int 数组中一样存在于堆中。该数组在堆上分配,并存储 int,这些 int 恰好是值类型。数组的内容被初始化为 default (int) ,它碰巧为零。

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


class HasAnInt
{
int i;
}


HasAnInt h = new HasAnInt();

变量 h 引用存在于堆上的 HasAnInt 实例。它只是碰巧包含一个值类型。这完全没有问题,“ i”只是碰巧存在于堆中,因为它包含在类中。在这个例子中也没有装箱。

为了理解正在发生的事情,这里有一些事实:

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

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

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

我认为你问题的核心在于对引用和价值类型的误解。这可能是每个。NET 和 Java 开发人员的努力。

数组只是一个值列表。如果它是一个引用类型的数组(比如说 string[]) ,那么该数组就是对堆中各种 string对象的引用列表,因为引用是引用类型的 价值。在内部,这些引用被实现为指向内存中某个地址的指针。如果您希望可视化它,那么这样的数组在内存中(在堆上)应该是这样的:

[ 00000000, 00000000, 00000000, F8AB56AA ]

这是一个包含4个对堆上 string对象的引用的 string数组(这里的数字是十六进制)。目前,只有最后一个 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(就像我说的,内存被初始化为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;

数组是在堆上分配的,整型数没有装箱。

造成混淆的原因可能是人们说过引用类型是在堆上分配的,值类型是在堆栈上分配的。这并不是一个完全准确的描述。

所有局部变量和参数都在堆栈上分配。这包括值类型和引用类型。两者之间的区别仅仅是变量中 储存的值。毫不奇怪,对于值类型,该类型的 价值直接存储在变量中,对于引用类型,该类型的值存储在堆中,对该值的 参考文献存储在变量中。

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

因此,给定以下类型:

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,即数组的地址,没有是数组的副本。

大家已经说得够多了,但是如果有人正在寻找关于堆、堆栈、本地变量和静态变量的清晰(但非官方)示例和文档,请参考 Jon Skeet 关于 内存在.NET-什么去哪里的完整文章

节选:

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

  2. 引用类型的实例变量总是在堆上,这是对象本身“存在”的地方。

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

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

这些插图描绘了上面@P 爸爸的回答

enter image description here

enter image description here

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

enter image description here