.Net Framemwork 之 值类型和引用类型的存储
C#把数据类型分为两种:值类型 和 引用类型。值类型存储在堆栈中,而引用类型存储在托管堆上。
一、值类型和引用类型变量的存储
首先,变量是存储信息的基本单元,而对于计算机内部来说,变量就相当于一块内存空间。
C#中的变量数据类型有两种:
[1] 值类型:简单类型、结构类型、枚举类型
[2] 引用类型:类、代表、数组、接口。
1、值类型和引用类型内存分配
值类型是在栈中操作,而引用类型则在堆中分配存储单元。
栈在编译的时候就分配好内存空间,在代码中有栈的明确定义,而堆是程序运行中动态分配的内存空间,可以根据程序的运行情况动态地分配内存的大小。因此,值类型总是在内存中占用一个预定义的字节数(比如,int占用4个字节,即32位)。当声明一个值类型变量时,会在栈中自动分配此变量类型占用的内存空间,并存储这个变量所包含的值。.NET会自动维护一个栈指针,它包含栈中下一个可用内存空间的地址。栈是先入后出的,栈中最上面的变量总是比下面的变量先离开作用域。当一个变量离开作用域时,栈指针向下移动被释放变量所占用的字节数,仍指向下一个可用地址。
注意,值类型的变量在使用时必须初始化。
引用类型的变量则在栈中分配一个内存空间,这个内存空间包含的是对另一个内存位置的引用,这个位置是托管堆中的一个地址,即存放此变量实际值的地方。.NET也自动维护一个堆指针,它包含堆中下一个可用内存空间的地址,但堆不是先入后出的,堆中的对象不会在程序的一个预定义点离开作用域,为了在不使用堆中分存的内存时将它释放,.NET将定期执行垃圾收集。垃圾收集器递归地检查应用程序中所有的对象引用,当发现引用不再有效的对象使用的内存无法从程序中访问时,该内存就可以回收(除了fixed关键字固定在内存中的对象外)。
但值类型在栈上分配内存,而引用类型在托管堆上分配内存,却只是一种笼统的说法。更详细准确地描述是:
[1] 对于值类型的实例,如果做为方法中的局部变量,则被创建在线程栈上;如果该实例做为类型的成员,则作为类型成员的一部分,连同其他类型字段存放在托管堆上,
[2] 引用类型的实例创建在托管堆上,如果其字节小于85000byte,则直接创建在托管堆上,否则创建在LOH(Large Objet Heap)上。
比如一下代码段:
public class Test { private int i; //作为Test实例的一部分,与Test的实例一起被创建在GC堆上 public Test() { int j = 0; //作为局部实量,j的实例被创建在执行这段代码的线程栈上 } }
2、嵌套结构的内存分配
所谓嵌套结构,就是引用类型中嵌套有值类型,或值类型中嵌套有引用类型。
引用类型嵌套值类型是最常见的,上面的例子就是典型的例子,此时值类型是内联在引用类型中。
值类型嵌套引用类型时,该引用类型作为值类型成员的变量,将在堆栈上保留关引用类型的引用,但引用类型还是要在堆中分配内存的。
3、关于数组内存的分配
考虑当数组成员是值类型和引用类型时的情形:
成员是值类型:比如int[] arr = new int[5]。arr将保存一个指向托管堆中4*5byte(int占用4字节)的地址的引用,同时将所有元素赋值为0;
引用类型:myClass[] arr = new myClass[5]。arr在线程的堆栈中创建一个指向托管堆的引用。所有元素被置为null。
二、值类型和引用类型在传递参数时的影响
由于值类型直接将它们的数据存放在栈中,当一个值类型的参数传递给一个方法时,该值的一个新的拷贝被创建并被传递,对参数所做的任何修改都不会导致传递给方法的变量被修改。而引用类型它只是包含引用,不包含实际的值,所以当方法体内参数所做的任何修改都将影响传递给方法调用的引用类型的变量。
下面程序证明了这一点:
class Program { /// <summary> /// 应用程序的主入口点。 /// </summary> [STAThread] static void Main(string[] args) { int i = 0; int[] intArr = new int[5]; Class1.SetValues(i,intArr); //输出的结果将是:i=0,intArr[0]=10 Console.WriteLine("i={0},intArr[0]={1}",i,intArr[0]); Console.Read(); } public static void SetValues(int i,int[] intArr) { i = 10; for (int j = 0; j < intArr.Length; j++) { intArr[j] = i; } } }
三、装箱和拆箱
装箱:将一个值类型转换为一个对象类型(object);
拆箱:将一个对象类型显式转换为一个值类型。
对于装箱而言,它是将被装箱的值类型复制一个副本来转换,而对于拆箱而言,需要注意类型的兼容性,比如,不能将一个值为“a”的object类型转换为int的类型。
可以用以下程序来说明:
static void Main(string[] args) { int i = 10; //装箱 object o = i; //对象类型 if (o is int) { //说明已经被装箱 Console.WriteLine("i已经被装箱"); } i = 20; //改变i的值 //拆箱 int j = (int)o; //输出的结果是20,10,10 Console.WriteLine("i={0};o={1};j={2}",i,o,j); Console.ReadLine(); }
四、关于string
string是引用类型,但却与其他引用类型有着一点差别。可以从以下两个方面来看:
[1] String类继承自object类。而不是System.ValueType。
[2] String本质上是一个char[],而Array是引用类型,同样是在托管的堆中分配内存。
但String作为参数传递时,却有值类型的特点,当传一个string类型的变量进入方法内部进行处理后,当离开方法后此变量的值并不会改变。原因是每次修改string的值时,都会创建一个新的对象。比如下面这段程序:
class Program { /// <summary> /// 应用程序的主入口点。 /// </summary> [STAThread] static void Main(string[] args) { string a = "111"; //a是一个引用,指向string类的一个实例 string b = a; //b与a都是同一个对象 //这时候b与a指向的并不是同一样对象,因为给b赋值后,已经创建了一个新的对象,并将这个新的string对象的引用赋给了b。 b = "2222"; //所以a的值不变,输出a=111. Console.WriteLine("a={0}",a); Console.ReadLine(); } }
注意:如果按引用传值时,则会与引用类型的参数一样,值会发生改变,比如以下代码:
class Program { /// <summary> /// 应用程序的主入口点。 /// </summary> [STAThread] static void Main(string[] args) { string a = "111"; TestByValue(a); //输出a=111. Console.WriteLine("a={0}",a); TestByReference(ref a); //按引用传值时则会改变,输出a="". Console.WriteLine("a={0}",a); Console.ReadLine(); } static void TestByValue(string s) { //设置值 s = ""; } static void TestByReference(ref string s) { //设置值 s = ""; } }
五、关于C#中的堆和栈
C#中存储数据的地方有两种:线程堆栈 和 托管堆。
在传统的C/C++语言中,栈是机器操作系统提供的数据结构,而堆则是C/C++函数提供的。所以机器有专门的寄存器来指向栈所在的地址,有专门的机器指令实现数据的入栈/出栈动作。其执行效率高,但不过也正因为此,栈一般只支持整数、指针、浮点数等系统直接支持的类型。堆是由C/C++语言提供函数库来维护的,其内存是动态分配的。相对于堆来说,栈的分配速度快,不会有内存碎片,但支持的数据有限。
在C#中,值变量由系统分配在栈上。用来分配固定长度的数据(值类型大都有固定长度)。每一个程序都有单独的堆栈,其他程序不能访问。在调用函数时,调用函数的本地变量都被推入程序的栈中。与C/C++类似,堆用来存放可变长度的数据,不过与C/C++不同的是,C#中数据是存放在托管堆中。
由于值变量在栈中分配,所以把一个值变量赋给另一个值变量,会在栈中复制两个相同数据的副本;相反,把一个引用变量赋给另一个引用变量时,会在内存中创建对同一个位置的引用。
在栈中分配相对于堆中分配,有以下特点:
(1)分配速度快;
(2)用完以后自动解除分配;
(3)可以用等号的方式把一个值类型的变量赋给另一个值类型。
使用基于CLR的语言编译器开发的代码都成为托管代码。
托管堆是CLR中自动内存管理的基础。初始化新进程时,运行会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。最初,该指针设置为指向托管堆的基址。
以下代码说明的很形象:
//引用类型(‘class’类类型) class SomeRef { public int32 x; } //值类型(‘struct’) struct SomeVa { public Int32 x; } static void Value TypeDemo() { SomeRef r1=new SomeRef();//分配在托管堆 SomeVal v1=new SomeVal();//分配堆栈上 r1.x=5;//解析指针 v1.x=5;//在堆栈上修改 SomeRef r2=r1;//仅拷贝引用(指针) SomerVal v2=v1;//先在堆栈上分配,然后拷贝成员 r1.x=8;//改变了r1,r2的值 v1.x=9;//改变了v1,没有改变v2 }
栈是内存中完全用于存储局部变量或成员(值类型数据)的高效区域,但其大小有限制。
托管堆占内存比栈大得多,当访问速度较慢。托管堆只用于分配内存,一般有CLR来出来释放问题。
当创建值类型数据时,在栈上分配内存;
当创建引用型数据时,在托管堆上分配内存并返回对象的引用。注意这个对象的引用,像其他局部变量一样也是保存在栈中,该引用指向的值则位于托管堆中。
如果创建一个包含值类型的引用类型,比如数组,其元素的值也是存放在托管堆中的某个地方,由使用该实体的变量引用;而值类型存储在使用它们的地方,有几处在使用,就有几个副本存在。
对于引用类型,如果在声明变量的时候没有使用new运算符,运行时不会给它分配托管堆的内存空间,而是在栈伤给她分配一个包含null值的引用。对于值类型,运行时会给它分配栈上的空间,并且调用构造函数来初始化对象的状态。
一、栈和托管堆
通用类型系统(CTS)区分两种基本类型:值类型和引用类型。它们之间的根本区别在于它们在内存中的存储方式。.NET使用两种不同的物理内存快来存储数据------栈和托管堆:
值类型总是在内存中占用一个预定义的字节数(例如,int类型占4个字节,而string类型占用的字节数会根据字符串的长度而不同)。当生命一个值类型变量时,会在栈中分配适当大小的内存(除了引用类型的值类型成员外,如类的int字段)。内存中的这个空间用来存储变量所包含的值。.NET维护一个栈指针,它包含栈中下一个可用的内存空间的地址。当一个变量离开作用域时,栈指针指向下移动被释放变量所占有的字节数。所以它仍指向下一个可用地址。
引用变量也利用栈,但这时候栈包含的只是对另一个内存位置的引用,而不是实际值。这个位置是托管堆中的一个地址,和栈一样,他也维护一个指针,包含堆中下一个可用的内存地址。但是,堆不是先入后出的,因为对对象的引用可在我们的程序中传递(例如,作为参数传递给方法调用)。堆中的对象不会在程序的一个预定点离开作用域。为了在不适用在堆中分配的内存时将它释放。.NET定期执行垃圾回收集,垃圾收集递归检查应用程序中所有对象的引用。引用不再有效的对象使用的内存无法从程序中访问,该内存就可以回收。
二、类型层次结构
CTS定义了一种类型层次结构,该结构不仅仅描述了不同预定义类型,还指出了用户定义类型的层次结构中的
三、引用类型
引用类型包含一个指针,指向堆中存储对象本身的位置。因为引用类型只包含实际的值,对方法体内参数所做的任何修改都将影响传递给方法调用的引用类型的变量。
下图显示了声明一个字符串变量并把它作为参数传递给一个方法时所发生的事情。
string s1="something"; DoSomething(s1); //..... DoSomething(string s2) { //.... }
当声明一个字符变量s1时,一个值被压入栈中,它指向栈中的一个位置,在上图中,引用存放在地址1243044中,而实际的字符串存放在堆地址12662032中,当该字符串传递给一个方法中,在栈伤对应输入参数声明了一个新的变量(这次是在地址1243032上),保存在引用变量,即堆中内存位置中的值被传递给这个新的变量。
委托是引用方法的一种引用类型,类似于c++中的函数指针(两者的主要区别于委托包括调用其方法的对象)。
四、预定义的引用类型
有两种引用类型在c#中受到了特别的重视,他们的c#别名和预定义值类型的c#别名很相像。第一种是object(c#别名是object,o小写)。这是所有值类型和引用类型的最终基类。因为所有的类型派生object,所以可以把任何类型转换成Object,甚至值类型也可以转换。这个把值类型转换为Object的过程称为装箱。所有的值类型都派生自引用类型,在这件事看似矛盾的事情背后,装箱的作用不可或缺。
第二种是String类,字符串代表一个固定不变的Unicode字符序列,这种不变性意味着,一旦在堆中分配了一个字符串,他的值将永远不会改变,如果值该改变了,.NET就创建一个全新的String对象,并把它赋值给该变量,这意味着,字符串在很多方面都像值类型,而不像引用类型。如果把一个字符串传递给方法。然后在方法体内改变参数的值,这不会影响最初的字符串(当然,除非参数按引用传递的)。c#提供了别名String(s小写)来代表Ststem.String类,如果代码中使用String,必须在代码一开始添加Using System;这一行。使用内建的别名string则不需要using System;