面試中遇到的問題——值類型與引用類型的區別
可能大家面試過程中會經常被問到值類型與引用類型的它們之間的區別,不然我昨天也正好被考到了這樣的題目(結果題目做錯了)。
C#值类型数据直接在他自身分配到的内存中存储数据,而C#引用类型只是包含指向存储数据位置的指针。
C#值类型,我们可以把他归纳成三类:
第一类: 基础数据类型(string类型除外):包括整型、浮点型、十进制型、布尔型。
整型包括:sbyte、byte、char、short、ushort、int、uint、long、ulong 这九种类型;
浮点型就包括 float 和 double 两种类型;
十进制型就是 decimal ;
布尔型就是 bool 型了。
第二类:结构类型:就是 struct 型
第三类:枚举类型:就是 enum 型
C#引用类型有五种:class、interface、delegate、object、string。
上面说的是怎么区分哪些C#值类型和C#引用类型,而使用上也是有区别的。所有值类型的数据都无法为null的,声明后必须赋以初值;引用类型才允许为null。
1.值类型直接存储其值,变量本身就包含了其实例数据,而引用类型保存的只是实例数据的内存引用。因此,一个值类型变量就永远不会影响到其他的值类型变量,而两个引用类型变量则很有可能指向同一地址,从而发生相互影响。
2.从内存分配上来看,值类型通常分配在线程的堆栈上,作用域结束时,所占空间自行释放,效率高,无需进行地址转换,而引用类型通常分配在托管堆上,由GC来控制其回收,需要进行地址转换,效率降低,这也正是c#需要定义两种数据类型的原因之一。
3.值类型均隐式派生自System.ValueType,而System.ValueType又直接派生于System.Object,每种值类型均有一个隐式的默认构造函数来初始化该类型的默认值,注意所有的值类型都是密封(sealed)的,所以无法派生出新的值类型。而且System.ValueType本身是一个类类型,而不是值类型,因为它重写了object的Equals()方法,所以对值类型将按照实例的值来比较,而不是比较引用地址。
4.C#的统一类型系统,使得值类型可以转化为对象来处理,这就是常说的装箱和拆箱。由于装拆箱需要装建全新对象或做强制类型转换,这些操作所需时间和运算要远远大于赋值操作,因此不提倡使用它,同时也要尽量避免隐式装拆箱的发生。
注:栈是操作系统分配的一个连续的内存区域,用于快速访问数据。因为值类型的容量是已知的,因此它可存储在栈上。而托管堆是CLR在应用程序启动时为应用程序预留的一块连续内存区,是用于动态内存分配的内存区,引用类型的容量只有到运行时才能确定,所有用堆来存储引用类型。
说了这么多,可能大家会认为这是废话,请看下面的例子吧?
例子一:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { int i = 100; int j = i; i = 12; Console.WriteLine("i={0},j={1}",i,j); Console.ReadKey(); } } }
输出结果:
分析:值类型直接存储其值,变量本身就包含了其实例数据
例子二:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { string str1 = "abc"; string str2 = str1; str1 = "123"; Console.WriteLine(str2); Console.ReadKey(); } } }
输出结果:
分析:这样的结果会使我们误以为string就是值类型。其实不然,str1 = "123"语句编译器私底下创建了一个新的字符串对象来保存新的字符序列"123",也就是此str1已非彼str1了,“此”str1的值的改变也就不能影响“彼”str1的值了,当然str2的值也就不会改变了。实质上str1 = "123"是str1=new string("123")的简写,它的每一次赋值都会抛掉原来的对象而生成一个新的字符串对象,分配新的内存空间,因此string是不可改变的。如果要创建可修改的字符串,可使用stringbuilder以获得更好的性能。
例子三:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class Person { public int Blood = 100; } class Program { public static void Add(int x) { x += 10; Console.WriteLine("值類型當參數被傳遞并修改之後:" + x); } public static void Add(Person person) { person.Blood += 10; Console.WriteLine("引用類型當參數被傳遞并修改之後:"+person.Blood); } static void Main(string[] args) { //值類型變量 int i = 10; Console.WriteLine("i的原值:"+i); Add(i); Console.WriteLine("但是i的值并沒有因為函數的修改而修改"+i); //引用類型變量 Person person = new Person(); Console.WriteLine("Blood的原值:"+person.Blood); Add(person); Console.WriteLine("但是Blood的值因為函數的修改:"+person.Blood); Console.ReadKey(); //值類型和引用類型的區別,就在于當函數傳遞的時候 //值類型是把自己的值複製一份傳遞給別的函數操作,無論複製的值怎麼被改變其自身的值是不會改變的 //而引用類型是把自己的內存地址傳遞給別的函數操作,操作的就是引用類型的本身,所以值被函數改變了 //這就是傳值和傳地址的區別 } } }
输出结果:
说明: 值类型是把自己的值复制一份传递给别的函数操作,无论复制的值怎么被改变,其自身的值是不会改变的,而引用类型的把自己的内存地址传递给别的函数操作,操作的就是引用类型值的本身,所以值被函数改变了
例子三:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { int i = 100; object n=(object)i; int j = (int)i; i--; Console.WriteLine("i={0},j={1},n={2}",i,j,n); Console.ReadKey(); } } }
输出结果: