浅析值类型与引用类型的内存分配
2009-02-05 22:53 宝宝合凤凰 阅读(270) 评论(0) 收藏 举报对象与实例的区别
这真是个无聊的话题,在之前我一直以为这是一件事情的两个叫法,到后来看 <<精通EJB>>一书,书中对这两个的概念明显是有些区别的,于是开始搜索一下。找了很多地方都没有答案,同时发现这个问题也不只是我一个人的问题,很多地方都在讨论。例如这些地方
http://forum.java.sun.com/thread.jspa?threadID=654144&messageID=3845005
http://www.geekinterview.com/question_details/17747
而且很多地方有对象实例 instance of object 的写法。真是让人迷惑啊。
查了一些资料,经过分析,现在觉得这个结论还是比较容易让人理解:
类-->对象-->实例
人类是类
某个人是对象
你是实例
实例本身也是对象。
表现出来是这样的
String 类
String str str是对象
String str = "abc"; "abc"是实例,也是对象.
这样也能解释instance of object这种说法 str的实例是"abc"
暂时先这么理解,希望有人能提出更好的理解方法和见解。。。
老外就是喜欢钻牛角尖,累不累呀!有什么意义吗?
下面是我的一点看法,希望这种无聊的讨论不要继续下去了。
例如:
Student是一个类,
语句
Student s; //只是声明了一个引用,并不关联到特定的实例
Student s = new Student();
上面语句实例化了一个Student对象,new Student()是调用类的构造函数产生实例。最后让引用和实例相关。
所以对象的引用和实例是不同的。s 是Student对象或实例的引用。对象和实例的概念是相同的。
如果仅用
Student s;
声明引用后,无法调用object的属性和方法,因为它并不和任何实例相关
===================================================
大家都知道要学好.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方互不相干;引用类型把引用复制给对方,从而双方都指向同一个堆中的实例,其中任何一方对实例做出修改,都会在另一方的操作中得到反映。最后我们通过复杂类型的内部成员的内存布局情况,进一步了解了值类型和引用类型的内存布局情况。
看以下代码:
public static void Main()
{
Int32 v = 5; //创建一个未装箱的值类型变量
Object o = v; //o引用一个已装箱的Int32,包含值5
v = 123; //将未装箱的值修改成123
Console.WriteLine(v + "," + (Int32)o);
}
以上代码发生了几次装箱,答案是3次。
首先在堆栈上创建一个Int32未装箱值类型的实例(v),并将其初始化为5,然后,创建一个Object类型的变量(o),并初始化它,让它指向v。但是,由于引用类型的变量必须指向堆中的对象,所以C#编译器会自动生成IL代码对v进行装箱,再将v的已装箱地址存储到o中。现在值123要放到未装箱的值类型实例v中,但这这个操作不会影响已装箱的Int32。
C#编译器会生成代码来调用String对象的静态方法Concat,该方法有几个重载的版本,所有的版本执行的操作都一样,只是参数数量不一样,根据上面情况,选用的是Concat的以下版本:(注:在VS中把光标放到String上,单击右键,转到定义即可看到该类型的定义)
public static string Concat(object arg0, object arg1, object arg2);
为第一个参数arg0传递的是v,但是v是一个未装箱的值参数,而arg0是一个Object,所以必须对v进行装箱,并将v的地址传给arg0。为arg1参数传递的是字符串“,”,它是对一个String对象的引用,最后,arg2参数o会拆箱转型为一个Int32,从而获得包含在已装箱的Int32中的未装箱的Int32的地址,这个未装箱的Int32必须再次装箱,并将新的已装箱的实例的内存地址传给Concat的arg2参数。
Concat方法调用指定的每个对象的ToString方法,并连接每个对象的字符串表示。从Concat传回的String对象随即传给WriteLine方法,显示最终结果。 如果以上代码写成这样效率会更高些:
Console.WriteLine(v + "," + o);
移除了o前面的一个拆箱操作,之所以这样是因为o已经是一个Object引用类型,它的地址可以直接传给Concat方法,所以这样就少了一个拆箱和一个装箱的操作,提高了性能。
还可以这样写:
Console.WriteLine(v.ToString() + "," + o);
在未装箱的值类型实例v上调用ToString()方法,返回一个String,String对象是引用类型,能直接传给Concat方法,不需要任何装箱操作。
再看以下代码:
public static void Main()
{
Int32 v = 5; //创建一个未装箱的值类型变量
Object o = v; //o引用一个已装箱的Int32,包含值5
v = 123; //将未装箱的值修改成123
Console.WriteLine(v);//显示123
v = (Int32)o; //拆箱并将o复制到v
Console.WriteLine(v);//显示5
}
上面代码只发生了一次装箱,因为Console.WriteLine()方法有一个重载版本只接受一个Int32值作为参数:
public static void WriteLine(Int32 value);
假如知道自己写的代码会进行反复对一个值类型进行装箱,那么手动方式对值类型装箱会有更好的效果,如下:
public static void Main()
{
Int32 v = 5; //创建一个未装箱的值类型变量
//编译这一行是,v会被装箱三次
Console.WriteLine("{0},{1},{2}", v, v, v);
//下面v只被装箱一次
Object o = v;
Console.WriteLine("{0},{1},{2}", o, o, o);
}
浙公网安备 33010602011771号