int[] myIntegers;
myIntegers = new int[100];
在上面的代码中,new int[100] 是在堆上生成数组吗?从我在《CLR via C#》上阅读的内容来看,答案是肯定的。但我不理解的是,实际上数组内部的 int 元素会发生什么。由于它们是值类型,我猜想它们必须要进行装箱操作,因为例如我可以将 myIntegers 传递给程序的其他部分,如果它们一直留在栈上,就会导致栈溢出。或者我错了吗?我猜想它们只会被装箱,并且会随着数组的存在在堆上存活。int[] myIntegers;
myIntegers = new int[100];
在上面的代码中,new int[100] 是在堆上生成数组吗?从我在《CLR via C#》上阅读的内容来看,答案是肯定的。但我不理解的是,实际上数组内部的 int 元素会发生什么。由于它们是值类型,我猜想它们必须要进行装箱操作,因为例如我可以将 myIntegers 传递给程序的其他部分,如果它们一直留在栈上,就会导致栈溢出。或者我错了吗?我猜想它们只会被装箱,并且会随着数组的存在在堆上存活。你的数组是在堆上分配的,而int类型没有装箱(boxed)。
你感到困惑的原因可能是因为人们说引用类型(reference types)在堆上分配,值类型(value types)在栈上分配。这并不是完全准确的表述。
所有局部变量和参数都分配在栈上,包括值类型和引用类型。两者之间的区别只在于变量中存储的内容。对于值类型,类型的值直接存储在变量中,而对于引用类型,类型的值存储在堆上,变量中存储的是指向该值的引用。
字段也是同样的情况。当为聚合类型(an aggregate type,如class或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个字节来存储它的值。因此,RefType和ValType的值所需的内存如下:
0 ┌───────────────────┐ │ I │ 4 ├───────────────────┤ │ S │ 8 ├───────────────────┤ │ L │ │ │ 16 └───────────────────┘现在,如果您在函数中有三个本地变量,类型分别为RefType、ValType和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
пјҢеҚіж•°з»„зҡ„ең°еқҖпјҢиҖҢдёҚжҳҜж•°з»„зҡ„еүҜжң¬гҖӮ
是的,数组将位于堆上。
数组内部的整数不会被装箱。仅仅因为值类型存在于堆上,并不一定意味着它将被装箱。当一个值类型,比如 int,被分配给类型为 object 的引用时才会发生装箱。例如:
以下代码不会装箱:
int i = 42;
myIntegers[0] = 42;
盒子:
object i = 42;
object[] arr = new object[10]; // no boxing here
arr[0] = 42;
您可能还想查看Eric在这个主题上的帖子:
我认为你的问题的核心在于对引用类型和值类型的理解上存在误解。这是可能每个.NET和Java开发人员都会遇到的问题。
数组只是一组值,如果它是一个引用类型的数组(比如string[]
),那么数组是一个指向堆上各种string
对象的引用列表,因为引用就是引用类型的值。在内部,这些引用被实现为指向内存地址的指针。如果你想可视化这一点,在内存中(在堆上)这样一个数组看起来像这样:
[ 00000000,00000000,00000000,F8AB56AA ]
这是一个包含4个引用到堆上string
对象的string
数组(这里的数字是十六进制)。目前,只有最后一个string
实际指向任何东西(内存在分配时初始化为全0),这个数组基本上是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 个整数组成的数组,其中只有第二个整数被赋了一个值(为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;
你的示例代码中没有装箱。
值类型可以像 int 数组一样存在于堆上。该数组在堆上分配并存储 int,这些 int 恰好是值类型。数组的内容被初始化为 default(int),这恰好是零。
考虑一个包含值类型的类:
class HasAnInt
{
int i;
}
HasAnInt h = new HasAnInt();
变量'h'是指在堆上存在的HasAnInt实例。它恰好包含一个值类型。这完全没问题,因为'i'作为类中的一部分也恰好存储在堆上。在这个示例中没有装箱操作。
在堆上分配了一个整数数组,仅此而已。myIntegers 引用了分配整数的部分的开头。该引用位于堆栈上。
如果您有一组引用类型对象的数组,例如 Object 类型,myObjects[] 位于堆栈上,将引用一堆值,这些值引用了对象本身。
总之,如果您将 myIntegers 传递给某些函数,则只传递对实际整数堆的引用。