值类型的变量一定义之后就马上可用。比如定义“int i;”之后,变量i 即可使用。引用类型的变量定义之后,还必须用new 关键字创建对象后才可以使用。
二、值类型变量与引用类型变量的内存分配模型也不一样
值类型变量与引用类型变量的内存分配模型也不一样。为了理解清楚这个问题,读者首先必须区分两种不同类型的内存区域:线程堆栈(Thread Stack)和托管堆(Managed Heap)。
每个正在运行的程序都对应着一个进程(process),在一个进程内部,可以有一个或多个线程(thread),每个线程都拥有一块“自留地”,称为“线程堆栈”,大小为1M,用于保
存自身的一些数据,比如函数中定义的局部变量、函数调用时传送的参数值等,这部分内存区域的分配与回收不需要程序员干涉。
所有值类型的变量都是在线程堆栈中分配的。
另一块内存区域称为“堆(heap)”,在.NET 这种托管环境下,堆由CLR 进行管理,所以又称为“托管堆(managed heap)”。
用new 关键字创建的类的对象时,分配给对象的内存单元就位于托管堆中。在程序中我们可以随意地使用new 关键字创建多个对象,因此,托管堆中的内存资源
是可以动态申请并使用的,当然用完了必须归还。
打个比方更易理解:托管堆相当于一个旅馆,其中的房间相当于托管堆中所拥有的内存单元。当程序员用new 方法创建对象时,相当于游客向旅馆预订房间,旅馆管理员会先看
一下有没有合适的空房间,有的话,就可以将此房间提供给游客住宿。当游客旅途结束,要办理退房手续,房间又可以为其他旅客提供服务了。
从表 1 可以看到,引用类型共有四种:类类型、接口类型、数组类型和委托类型。所有引用类型变量所引用的对象,其内存都是在托管堆中分配的。严格地说,我们常说的“对象变量”其实是类类型的引用变量。但在实际中人们经常将引用类型的变量简称为“对象变量”,用它来指代所有四种类型的引用变量。在不致于引起混淆的情况下,本书也采用了这种惯例。
在了解了对象内存模型之后,对象变量之间的相互赋值的含义也就清楚了。请看以下代码(示例项目ReferenceVariableForCS):
01 class A
02 {
03 public int i;
04 }
05 class Program
06 {
07 static void Main(string[] args)
08 {
09 A a ;
10 a= new A();
11 a.i = 100;
12 A b=null;
13 b = a; //对象变量的相互赋值
14 Console.WriteLine("b.i=" + b.i); //b.i=?
15 }
16 }
注意第12 和13 句。
程序的运行结果是:
b.i=100;
请读者思索一下:两个对象变量的相互赋值意味着什么?
事实上,两个对象变量的相互赋值意味着赋值后两个对象变量所占用的内存单元其内容是相同的。讲得详细一些:第10 句创建对象以后,其首地址(假设为“1234 5678”)被放入到变量a 自身的4 个字节的内存单元中。第12 句又定义了一个对象变量b,其值最初为null(即对应的4 个字节内存单元中为“0000 0000”)。第13 句执行以后,a 变量的值被复制到b 的内存单元中,现在,b 内存单元中的值也为“1234 5678”。
根据前面介绍的对象内存模型,我们知道现在变量a 和b 都指向同一个实例对象。如果通过b.i 修改字段i 的值,a.i 也会同步变化,因为a.i 与b.i 其实代表同一对象的同一字段。
整个过程可以用图 9 来说明:
由此得到一个重要结论:
对象变量的相互赋值不会导致对象自身被复制,其结果是两个对象变量指向同一对象。另外,由于对象变量本身是一个局部变量,因此,对象变量本身是位于线程堆栈中的。
严格区分对象变量与对象变量所引用的对象,是面向对象编程的关键之一。
由于对象变量类似于一个对象指针,这就产生了“判断两个对象变量是否引用同一对象”的问题。
C#使用“==”运算符比对两个对象变量是否引用同一对象,“!=”比对两个对象变量是否引用不同的对象。参看以下代码:
//a1与a2引用不同的对象
A a1= new A();
A a2= new A();
Console.WriteLine(a1 == a2);//输出:false
a2 = a1;//a1与a2引用相同的对象
Console.WriteLine(a1 == a2);//输出:true
需要注意的是,如果“==”被用在值类型的变量之间,则比对的是变量的内容:
int i = 0;
int j = 100;
if (i == j)
{
Console.WriteLine("i与j的值相等");
}
理解值类型与引用类型的区别在面向对象编程中非常关键。