值类型与引用类型比较与区别
在.NET中或許我們不用擔心內存管理以及垃圾回收器(Garbage Collection GC)的問題,但是我們還是應該了解這些東東以便在必要的時候優化我們程序的性能。而且,如果對內存管理如何工作有所了解,那將有助于解釋我們每個程序里的每個變量的運行規律。這篇文章主要內容是解釋堆(Heap)和棧(Stack),各種變量以及這些變量到底是如何工作的。
.Net Framework 在執行代碼時,有兩個用來存儲對象的地方,也就是堆和棧,用于幫助執行我們的代碼。它們駐留在機器內存中,包含了所有我們需要實現的信息。
Stack VS Heap
棧多多少少用來負責跟蹤你的代碼里正在執行什么,或者說代碼里的什么東東被called。而堆則或多或少用來跟蹤我們的對象,或者說數據,大多數情況下都是數據啦——后頭再詳解。
把棧想象成堆砌起來由上到下的盒子。每次我們調用一個方法,就新加一個盒子到棧頂,我們用這種方法跟蹤我們的程序在執行些什么。我們能用的,永遠只是最頂上的那個盒子。當我們把最頂上這個盒子用掉了的時候,也就是方法執行完畢并返回的時候,我們就惡狠狠的把它扔掉!然后接著處理下一個盒子里的東東。而堆其實也是類似的東東,除了它的目的是用來保存信息(絕大多數情況下不是用來跟蹤程序執行),因此堆里面的任何東西都不受限制的隨便訪問。堆就像是床上我們沒空收拾的洗好的衣服一般,我們能很快的隨便拿任何一件起來。而棧則跟壁櫥里一堆裝鞋子的盒子似的,我們得一個一個的從頂上取下來才能拿到下一雙鞋子。
上圖雖然并不是真正的內存中堆棧的樣子,不過有助于我們理解它們的區別。
棧是“自我維護”的,意思是基本上是管理自個兒的(而不是別的地方的)內存。當頂部盒子不在使用,就扔之(就不是自家的雪了)!而堆呢,不太一樣的是,必須得跟GC打交道——這東西用來保證堆是clean(沒有過多垃圾內存)的。(木有人喜歡地上擺一堆臟衣服吧!臭死了!)。
What goes on the Stack and Heap?
當代碼執行時,堆棧里頭主要放置四種類型的東東:值類型,引用類型,指針(Pointers),以及指令(Instructions)。
值類型:
c#中,值類型繼承自System.ValueType:
bool, byte, char, decimal, double, enum, float, int, long, sbyte, short, struct, uint, ulong, ushort
引用類型:
而引用類型則有:
class, interface, delegate, object, string
指針:
內存管理模型中的第三種東東是對一個類型的引用。這個引用通常就是指指針。我們不能直接使用指針,它們被CLR所管理。指針不同于引用類型。當我們說某某是引用類型時,實際上就意味著我們要通過指針去訪問這個類型的值。一個指針占用內存中的一塊空間,只想內存中另外一塊空間。指針跟任何別的放在堆棧中的東西一樣,是要占用物理空間的。它的值要么是null,要么就是內存地址。
指令:
在后續文章中再解釋,稍安勿躁……
How is it decided what goes where? (Huh?)
okok,再啰嗦兩句我們就可以正式擺弄我們的堆棧了。
這里有兩條黃金規則:
棧,就像我們剛才提到的,負責跟蹤單個線程(thread)運行到哪兒了。你不妨把其想象成thread的狀態機,每個線程都有自個兒的棧。當我們的代碼調某個方法時,線程開始執行JIT編譯過并且保存在方法表(method table)中的指令集,同時,它把方法參數壓入線程棧中。然后開始執行代碼并訪問方法里需要的、同時已經存在于線程棧頂部的變量。舉個例子吧:
public int AddFive(int pValue) { int result; result = pValue + 5; return result; }讓我們看看棧里頭都發生了什么,記住我們看到的只是棧頂的東西,下頭早有無數別的東東在里面了哦!
當我們開始執行這個方法時,方法的參數被壓棧(稍后我們討論參數傳遞)。
Notice: 方法并不存在stack里頭,圖例只是為了演示概念
下一步,控制(線程執行這個方法)被交給AddFive方法在方法表中的指令集,如果這是第一次使用這個方法,JIT編譯將被執行。
在方法的執行過程中,我們需要內存來保存 "result”,因此棧頂為其分配空間。
方法結束了,result被返回。
這時棧頂指針將會移到最初AddFive方法開始的內存地址,這樣所有剛才分配的內存空間都被清理掉了,然后接著執行AddFive更下面的函數(圖中為顯示)。
在這個例子里,result變量被壓棧。事實上,任何時候值變量在方法內部被聲明,都會被壓棧。
不過,值變量有時候也會存儲在堆里頭。看看黃金規則二,值類型總是保存在被聲明的地方。那么,如果值類型在方法(method)外部聲明,同時本身又存在于引用類型內部時,那么就會被保存在堆里頭。
在來一個例子:
如果我們有MyInt Class(引用類型):
public class MyInt { public int MyValue; }另外一個方法正在執行中:
public MyInt AddFive(int pValue) { MyInt result = new MyInt(); result.MyValue = pValue + 5; return result; }跟前頭一樣,線程開始執行方法,參數被壓棧:
這個時候開始好玩了:
因為MyInt是引用類型,因此保存在堆里頭,通過棧里的指針去引用它。
AddFive執行完畢以后,我們開始清理棧頂:
這樣我們就把MyInt對象當作孤兒留在了堆里頭,因為棧中沒有任何指針在引用它了!
這時候就是GC大顯身手的時候啦!一旦我們程序達到某個特定的內存閥值(threshold)而我們需要更多堆空間時,GC就會開動。GC完全停止所有運行中的線程,找到所有堆里沒有被引用的對象,然后像秋風掃落葉般的刪除它們。GC重新組織所有滯留在內存中活動的對象,并調整堆棧中對其引用的地址。可以看到,從性能角度來說,這可真是很費時間。所以現在你是不是覺得注意一下堆棧的處理會有助于你寫出高性能的代碼呢?
OK,好了好了,很棒很棒,不過,這東西到底會如何來折磨俺們呢?
好問題。
當我們使用引用類型時,我們實際上是使用指向該對象的指針,而不是對象本身。當我們使用值類型時,我們使用的就是值本身。清楚還是不清楚?
最好還是來個例子吧。
如果我們執行以下方法:
public int ReturnValue() { int x = new int(); x = 3; int y = new int(); y = x; y = 4; return x; }我們得到3,夠簡單吧。
如果這樣呢:
public class MyInt { public int MyValue; } public int ReturnValue2() { MyInt x = new MyInt(); x.MyValue = 3; MyInt y = new MyInt(); y = x; y.MyValue = 4; return x.MyValue; }那么我們將得到4。
為什么?
在第一個例子里所有事都按照預定計劃行事:
public int ReturnValue() { int x = 3; int y = x; y = 4; return x; }在后面的例子里,我們得不到3是因為x和y都指向堆里的同一個對象。
public int ReturnValue2() { MyInt x; x.MyValue = 3; MyInt y; y = x; y.MyValue = 4; return x.MyValue; }希望本文能幫助您更好的理解值類型和引用類型基本的區別,以及指針是什么,什么時候會用到指針。在后面的系列中,我們進一步的闡述內存管理,特別的會多聊聊方法參數的問題。
轉載于:https://www.cnblogs.com/tongdengquan/archive/2010/01/31/6090607.html
總結
以上是生活随笔為你收集整理的值类型与引用类型比较与区别的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Silverlight]奇技银巧系列-
- 下一篇: 《 图解 HTTP 》读书笔记