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

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

4

8 回答 8

311

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

您混淆的根源可能是因为人们说引用类型是在堆上分配的,而值类型是在堆栈上分配的。这不是一个完全准确的表示。

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

字段也是如此。class当为聚合类型(a或 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 个字节来存储它的值,字段需要S4 个字节来存储它的引用,而字段L需要 8 个字节来存储它的值。因此两者的值的内存RefTypeValType看起来像这样:

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

现在,如果您在一个函数中有三个局部变量,类型分别RefTypeValType、 和int[],如下所示:

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

于 2009-07-11T17:03:56.113 回答
24

是的,数组将位于堆上。

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

例如

不装箱:

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

盒子:

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

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

于 2009-07-11T14:35:40.027 回答
22

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

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

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

如果你有一个字符串数组,它实际上是一个字符串引用数组。由于引用是值类型,它们将成为堆上数组对象的一部分。如果你把一个字符串对象放到数组中,其实就是把这个字符串对象的引用放到了数组中,而字符串就是堆上的一个单独的对象。

于 2009-07-11T15:49:06.963 回答
11

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

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

[ 00000000, 00000000, 00000000, F8AB56AA ]

这是一个string包含 4 个对string堆上对象的引用的数组(这里的数字是十六进制的)。目前,只有最后一个string实际上指向任何东西(内存在分配时初始化为全零),这个数组基本上是 C# 中这段代码的结果:

string[] strings = new string[4];
strings[3] = "something"; // the string was allocated at 0xF8AB56AA by the CLR

上述数组将在 32 位程序中。在 64 位程序中,引用将是两倍大(F8AB56AA将是00000000F8AB56AA)。

如果您有一个值类型数组(例如 an 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 ]

因为 a 的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;
于 2009-07-11T16:57:11.243 回答
3

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

在此处输入图像描述

在此处输入图像描述

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

在此处输入图像描述

于 2017-12-06T01:00:11.257 回答
2

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

摘抄:

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

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

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

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

于 2013-04-09T07:43:44.233 回答
1

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

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

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

于 2009-07-11T14:38:02.813 回答
1

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

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

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


    class HasAnInt
    {
        int i;
    }

    HasAnInt h = new HasAnInt();

变量 h 指的是位于堆上的 HasAnInt 实例。它恰好包含一个值类型。没关系,'i' 只是碰巧生活在堆上,因为它包含在一个类中。此示例中也没有装箱。

于 2009-07-11T15:07:46.277 回答