值類型和引用類型、裝箱與拆箱
本想把這篇博文題目的拆開來說,但是想一想,值類型和引用類型、裝箱與拆箱又是密不可分的,於是決定還是放在一起來說。
一、 值類型和引用類型:
在我們剛開始學習寫程序的時候,面向對象的三大概念等都是我們所能熟悉,並且比較好理解的概念,但是,到值類型和引用類型的時候,我相信有大部分的同仁都曾經迷茫過(包括我^_^)
在我們之前說的基元類型中,無非分為兩大類型,一個就是值類型,另一個則是引用類型。我們先說一下引用類型,首先,我們需要非常明確的一點就是引用類型是從托管堆上面分配空間的而值類型是在一個線程堆棧上分配空間的(值類型變量做為局部變量時,該實例將被創建在堆棧上;而如果值類型變量作為類型的成員變量時,它將作為類型實例數據的一部分,同該類型的其他字段都保存在托管堆上)。當我們用new關鍵字去構建一個引用類型的時候,new操作符返回給我們的是一個對象數據的內存地址,我們也可以理解為一個對象指針。CLR會對我們構建的引用類型對象的成員進行初始化的操作並將引用類型對象中的字段進行初始化操作為0或者null,還有一點我們必須注意的是,在我們構建完一個引用類型的時候,很可能會增加一次GC(垃圾回收)的操作,就是說,當我們這個引用類型的對象被認為不會再使用的時候,將被垃圾回收,以釋放占用的內存空間(關於垃圾回收,將在以后的博文中進行討論)然而值類型是在現成堆棧上進行分配的,所以它沒有一個對象指針,我們操作值類型就是操作值類型中的數據。並且,值類型不受GC垃圾回收的制約。我們來舉個例子,並將一步一步的進行分析。代碼如下:
static void Main(string[] args)
{
String str_a = "abc";//在托管堆上分配內存
Int32 int_a = 123;//在線程堆棧上分配內存
String str_b = str_a;//復制str_a的對象指針
Int32 int_b = int_a;//在線程堆棧上分配內存並復制成員
Console.WriteLine(str_a);//打印str_a的值為abc
Console.WriteLine(str_b);//打印str_b的值為abc
//這里不再打印int_a和int_b的值
str_b = "uio";
int_b = 456;
Console.WriteLine(str_a);//打印str_a的值為abc
Console.WriteLine(str_b);//打印str_b的值為uio
Console.WriteLine(int_a);//打印int_a的值為123
Console.WriteLine(int_b);//打印int_b的值為456
Console.ReadLine();
}
通過以上代碼我們可以看到,當我們分別從托管堆上分配內存給str_a的時候str_a儲存的並不是“abc”而是”abc”的內存地址,同樣將str_a的值賦給str_b的時候,str_b儲存的並不是”abc”而是從str_a那里復制到”abc”的內存地址,所以,我們第一次打印str_a和str_b的時候,打印出來的結果是一樣的,都是”abc”。然而,當我們把str_b的值更改為”uio”的時候,str_b儲存的內存地址就為”uio”的內存地址,而str_a沒有做任何改變,所以,第二次打印str_a和str_b的時候,結果為”abc”和“uio”。
那么,值類型是怎么回事呢?值類型存儲中,並不包含一個對象的指針,而是它本身的一個字段,也就是說當int_a賦值給int_b的時候,int_b把int_a的所有成員全部進行復制在一個新的內存空間,而int_b的修改操作,並不影響int_a。
以上為筆者對值類型和引用類型的理解,有什么錯誤的地方也請大家指出。
一、 裝箱和拆箱
下面我們來討論下裝箱和拆箱,裝箱和拆箱是指:從值類型轉換成引用類型的操作,我們稱之為裝箱,反之,從引用類型轉換成值類型,我們稱之為拆箱操作。
理論上,這兩句話應該很好理解,實際上其實也不難。我們來看一個例子
如下:
static void Main(string[] args)
{
Int32 int_a = 5;
object o = int_a;
int_a = 123;
o = 456;
Console.WriteLine(int_a+","+(Int32)o);
Console.ReadLine();
}
我們來分析下這段代碼執行了幾次裝箱和幾次拆箱。首先,我們給int_a賦值為5,並且把int_a賦值給object類型,這個時候就發生了一次裝箱操作,就是說把int_a的值5,分配相應的內存復制到托管堆上,並把值為5的內存地址指針返回給object類型,這是一次裝箱操作,在裝箱操作的時候,int_a還在線程堆棧上並且值為5。Int_a=123只是單純的更改了原來值。o=456這里面又進行了一次裝箱操作,就是說int類型456裝箱成引用類型(操作與o=int_a一樣),並把o的指針引用更改為456的指針引用。之前123將等待垃圾回收。接下來,看打印輸出的方法。在這里面,我們其實傳入的參數都為String類型,所以在這里int_a也執行了裝箱操作,因為與String類型的“,”進行連接操作。還有一個裝箱操作就是在(Int32)o這里面,我們將o強制類型轉換是一個拆箱操作(引用類型轉換成值類型為拆箱操作),這也是整個Main方法中的唯一一次拆箱操作,那么在轉換成值類型之后,又與String類型的”,”進行連接操作,在這里就又執行了一次裝箱操作。最后返回給WriteLine方法的是一個字符串。打印出來為“123,456”所以上述代碼進行了4次裝箱操作和1次拆箱操作。為了更清晰的表現給大家,直接上IL代碼的圖:
裝箱操作我已經使用方框標識出來,拆箱操作我已經使用橫線標識出來。
對於裝箱與拆箱操作,上述代碼可以看出,為了提高上段代碼的效率,我們沒有必要在執行WriteLine方法的時候對o進行一次拆箱操作。至於為什么這么寫只是為了更清晰的演示裝箱與拆箱操作在編譯為IL代碼的時候是如何表示的。
我想這篇博文也到此結束了,可能沒有很深入的討論到值類型與引用類型、裝箱和拆箱操作,只是點到為止吧,更深層次的東西,也希望大家能夠自己進行一個研究。必要的時候看一下IL代碼,會了解到很多我們之前不知道的東西。如有錯誤、問題也請各位看官留言告訴我。謝謝!
在這里再次為論壇做個宣傳 www.175m.com,希望大家可以在論壇里面一起建設、討論該論壇(希望博客園的大哥們,不要以為我在挖牆角,我只是在為我們程序員的大家庭做貢獻)