大家都知道要学好.NET,深入了解值类型和引用类型是必不可少的。在这里我给大家简单分析一下它们内存分配的区别和联系。
在分析之前,我们先行构造出一个最简单的类引用类型:
public class MyClass
{
}
u 局部变量的声明
在我们使用类型时,代码里面必然少不了变量的声明,我们先看一下方法内的局部变量的声明,请看如下代码:
private static void Main()
{
int i;
MyClass mc;
i = 5;
mc = new MyClass();
}
当一个局部变量声明之后,就会在栈的内存中分配一块内存给这个变量,至于这块内存多大,里面存放什么东西,就要看这个变量是值类型还是引用类型了。
l 值类型
如果是值类型,为变量分配这块内存的大小就是值类型定义的大小,存放值类型自身的值(内容)。比如,对于上面的整型变量i,这块内存的大小就是4个字节(一个int型定义的大小),如果执行i = 5;这行代码,则这块内存的内容就是5(如图-1)。
对于任何值类型,无论是读取还是写入操作,可以一步到位,因为值类型变量本身所占的内存就存放着值。
l 引用类型
如果是引用类型,为变量分配的这块内存的大小,就是一个内存指针(实例引用、对象引用)的大小(在32位系统上为4字节,在64位系统上为8字节)。因为所有引用类型的实例(对象、值)都是创建在堆上的,而这个为变量分配的内存就存放变量对应在堆上的实例(对象、值)的内存首地址(内存指针),也叫实例(对象)的引用。以图形化的方式展现仿佛是变量有一条线指向着它在堆中的实例(有如图-2),而如果变量的类型还没有被实例化,则为零地址(null、空引用)。
以下为执行mc = new MyClass();代码后,内存中的示例:
由图-2可知,变量mc中存放的是MyClass实例(对象)的对象引用,如果需要访问mc实例,系统需要首先从mc变量中得到实例的引用(在堆中的地址),然后用这个引用(地址)找到堆中的实例,再进行访问。需要至少2步操作才可以完成实例访问。
u 类型赋值
另一个常见的操作就是类型的赋值操作,即变量之间的赋值。由于值类型和引用类型的变量内部存放的内容不同,导致在变量赋值的时候,会有相同的行为而有不同的结果。
l 值类型
请看如下代码:
private void SomeMethod()
{
int i, j;
i = 5;
j = i;
j = 10;
}
相信大家一定都知道最后的结果是i:5,j:10。不过在.NET中,int类型也是一个结构,不但可以存放整数值,还有一系列的方法和属性可以使用,而非我们以前学C语言时的那种单纯int存放一个整数的概念。所以我们现在看针对int的代码,其实也是在看针对struct类型的代码。
对于值类型的赋值语句“j = i”,请看图-3:
在执行j = i;语句时,变量i中的内容被复制了一份,然后放到了变量j中,此时变量i和j都有一个值为5,同时也可以看出,i和j的值现在互不相干,完全独立,所以任意修改其中的某个变量的值,不会影响到另外一个。
l 引用类型
请看如下代码:
private void SomeMethod()
{
MyClass x, y;
x = new MyClass();
y = x;
}
代码中先对x进行了实例化,然后将x赋值到y,这段代码的结果请看图-4:
当执行y = x;代码时,变量x中的内容同样复制了一份,然后放到了变量y之中,但是因为变量x中存放是一个类型实例(对象)的引用,因此这次赋值操作等同于把这个引用传递给了变量y,结果就是x和y中的引用指向堆中同一个类型的实例(对象)。
你可以使用x的引用去修改MyClass实例(对象),然后用y的引用得到修改后的MyClass实例(对象),反之亦可,因为x和y引用的是同一个实例(对象)。
u 复杂类型的内存布局概述
以上内容是以值类型或者引用类型为一个整体叙述值类型和引用类型的变量声明和赋值的情况。下面我们看看值类型和引用类型内部含有其他类型成员变量(一般称为字段)的情况。虽然看起来情况似乎复杂了一点,但是只要我们可以把握住值类型的值存放在值类型变量内部,而引用类型的值在堆中存放,引用类型的变量只存放对它实例(对象)的引用这个原则,就可以很清晰的做出分析。
l 值类型
且看下面的类型定义代码:
public struct MyStruct
{
/* 注意:作为结构,内部字段是不能象下面所写那样,在声明时直接初始化的。
* 但这里为了节省篇幅,从表达语义的角度,直接在声明时初始化了
* 此结构的代码无法通过编译的 */
public int i = 5; //值类型
public System.Exception ex = new Exception(); //引用类型
}
在MyStruct结构中,有2个字段,一个是值类型的i变量,一个是引用类型的ex变量。这种情况下,内存中应该是一个什么模样呢?
首先,变量i和ex作为MyStruct的成员,必然存放在MyStruct实例的内部,而变量i作为值类型,其值就存放在自身;ex作为引用类型,变量内只存放实例(对象)的引用,而实例(对象)则在堆上创建,因此就有如图-5所示:
l 引用类型
且看下面的类型定义代码:
public class MyClass
{
MyStruct ms = new MyStruct(); //上面所述的MyStruct结构
System.Random r = new Random(); //引用类型
}
在MyClass中,有2个字段成员,一个是我们上面的所定义的MyStruct结构值类型ms,另外一个是Random类类型r。
这里我们把情况再变得复杂一些了,因为MyStruct内部还有值类型和引用类型的字段,这时候内存中是一幅什么景象呢?我们要记住,不管情况多么复杂,把握住值类型和引用类型的特点,慢慢分析,总会得到正确的结果,正如图-6所示:
作为引用类型的实例(对象),无论什么情况,都是在堆中的。而MyStruct结构作为MyClass的成员,它也在MyClass实例所占的堆内存中,而且因为值类型的值是在自身存放的,所以就是图-6中看到的结果。整个图-6,所有的值类型和引用类型的布局,都完全负责值类型和引用类型的特点,没有例外。
u 总结
以前在问起值类型和引用类型有什么区别的时候,经常听到同学说“值类型存放在栈上,引用类型存放在堆上”。其实这么说并不严谨,因为当值类型作为引用类型的一个成员的时候,它的值是内嵌在引用类型实例内部在堆上存放的。我认为,正确的说法应该是:值类型变量的值存放在变量内部,而引用类型变量的值存放在堆上,变量本身存放一个指向堆中的值的引用。同时我们也可以看到在2个变量赋值的时候,值类型和引用类型的差别,值类型将自身的值复制给对方,之后,2方互不相干;引用类型把引用复制给对方,从而双方都指向同一个堆中的实例,其中任何一方对实例做出修改,都会在另一方的操作中得到反映。最后我们通过复杂类型的内部成员的内存布局情况,进一步了解了值类型和引用类型的内存布局情况。