從這個簡單程序的輸出結果,你想到了什麼?是否是與你心中想的結果不一致?是否是以爲輸出的結果應該爲:i is 1,o is 8,o2 is 8性能
圖 2學習
咱們都知道,每個方法在執行前,操做系統會給方法內每一個變量分配內存空間。從圖2中就能夠看出,在執行前各變量(i,o,o2)已分配了內存,且各自都有初始值。spa
從圖中,能夠發現變量i和變量o,o2有些許不一樣。變量i在內存中存儲的值和程序中的值是同樣的,都是0;變量o,o2在內存中存儲的值和程序中的值不同,內存中存儲的值是一個地址(0x00000000),程序中的值是null,那變量o,o2的null值存儲在哪呢?爲何變量i和變量o,o2會有如此大的不一樣呢?操作系統
咱們都知道,C#有兩大類型:值類類型和引用類型。圖2中int屬於值類型,object屬於引用類型。接下來,介紹一下值類型和引用類型:3d
園中有不少博文這麼描述,我用程序驗證了一下全局的值類型變量的值,靜態的值類型變量的值,引用類型實例中值類型成員的值,以下圖3code
圖 3orm
從圖中,能夠看出變量(j,o,seg,st)的值應該是在同一個存儲區域中,而變量(gi)是在另一個存儲區域中。引用類型Student的成員Age的地址還未分配。因此說值類型的值存儲在內存棧上是不許確的。對象
查找了一些資料,內存格局分爲四個區:blog
1)全局數據區:存放全局變量,靜態變量,常量的值接口
2)代碼區:存放程序代碼
3)棧區:存放爲運行而分配的局部變量,參數等
4)堆區:自由存儲區。
更爲準確的說,方法體內的值類型變量的值存儲在內存棧上,引用類型變量的值存儲在內存堆上。因爲對象實例是引用類型變量的值,而對象實例成員只是對象實例的一部分,因此其隨對象實例整個存儲在內存堆上。
或許眼尖的園友發現了,上面那句話仍是不對,結構體StructEg的引用類型成員Name的數據就沒有存儲在內存棧上。從圖3看,結構體變量seg的數據分紅兩部分,值類型成員數據存儲在內存棧上,引用類型成員數據存儲在內存堆上。
因此確切的說:方法體內的預約義的值類型(如int,bool,char)變量的數據存儲在內存棧上,引用類型變量的值存儲在託管堆中,結構體的值類型成員的值存儲在內存棧上,結構體的引用類型成員的值存儲在內存堆中。(下面介紹的值類型基本是預約義的值類型和只包含值類型成員的結構體,通常包含引用類型成員的都定義成類)
這句話怎麼理解呢?這句話中關鍵詞是」存儲」,其實仍是在描述程序中的變量在內存棧中的表現。
值類型變量在內存棧中存儲的是其在程序中的變量值,引用類型變量在內存棧中存儲的是其程序中的值在內存堆中的引用。(固然值類型變量和引用類型變量都是方法體內的局部變量或參數)
1)分配內存堆空間:咱們都知道要存儲數據,首先得申請內存空間。引用類型變量在new實例化時,系統在內存堆中分配空間。
2)更新地址:把引用類型變量在內存棧中存儲的值更新成新的值(新值爲新分配的內存堆的首地址)。至此,引用類型變量指向了新的內存空間。
3)填充值:把初始化值填充到內存堆中。
可能有些園友會說,瞭解這個有什麼意義呢?那我就簡單的說一個現象:
1)在學習方法理論時,傳參會有這樣的描述:值類型按值傳遞,傳遞的是對象的副本,對已調用方法中的對象的更改對原始對象無影響;引用類型的對象按值傳遞傳遞的是對對象的引用,使用此引用更改對象的成員,此更改將影響原始對象。
其實,這裏究其原理,就是由於值類型與引用類型的值的不一樣存儲位置,來描述其傳參後的影響。
因此,有時咱們爲了影響值類型實參的值,而在形參前面加ref或out;有時咱們爲了避免影響引用類型實參的值,而採用深拷貝的方式傳遞參數值。
理論是爲了指導實踐,當了解了其原理,在實踐時,咱們纔會顯得踏實。
從圖中能夠看出,執行後,值類型變量在內存中存儲的值和其在程序中的值是同樣的,都是1。
圖 5
從圖中能夠看出,引用類型變量o的值變成1了,在內存棧中存儲的值更新成新地址了。經過前面分析,咱們知道變量o指向了1的新地址。值類型變量的值存儲在內存棧中,引用類型變量的值存儲在內存堆中,內存棧中的值是如何到內存堆中的?這就是本節要介紹的第二個重要概念,裝箱和拆箱。
園中不少博文介紹:值類型轉換爲引用類型,就叫裝箱。我以爲這表述不太準確,以下圖6
圖 6
從圖6中能夠看出,值類型不能隨意的轉換爲引用類型,它只能隱式轉換爲如下兩種引用類型:
1)object類型;
2)值類型實現的接口
前面已介紹了引用類型變量賦值過程了,裝箱步驟也相似:
1)分配新的內存空間
2)更改地址
3)填充值:從值類型變量處拷貝一份值,存儲到新分配的內存堆中。
object
類型到值類型或從接口類型到實現該接口的值類型的顯式轉換。1)檢查對象實例,以確保它是給定值類型的裝箱值。若不能顯式轉換,則拋異常。
意思是:裝箱時的值類型和拆箱時的值類型要徹底一致(哪怕類型兼容也不行,以下圖7中的裝箱前的類型是short,拆箱後的類型是int,就會產生異常)。如圖7
圖 7
2)驗證成功後,複製實例的值到值類型變量中
相對於簡單的賦值而言,裝箱和拆箱過程須要進行大量的計算,因此其對性能會有較大的損耗。特別裝箱時,要建立新的對象實例,要在內存堆上分配新的內存空間,在分配新的內存空間時,可能會引發垃圾回收(垃圾回收對性能損耗很是大,具體垃圾回收爲何會有很大的性能損耗,網上相關介紹不少,在此不作介紹)。
或許有園友會說,平時裝箱/拆箱操做很少,其實在你不經意間,存在不少裝箱操做
1)string s=string.Format(「{0}」,i);//i爲值類型數據---典型的字符串格式化
圖 8
圖 9
不管是值類型變量賦值仍是引用類型變量賦值,都是把數據複製一份,而後賦給另外一個變量。只是引用類型變量在內存棧中存儲的是其值在內存堆中的地址。因此引用類型變量間賦值,就使兩個變量指向了同一個內存堆空間。如上圖8,圖9
此時,或許有人會說,這句不是表示對引用類型變量進行操做嗎?賦值了8後,它在內存堆內的值應該是8了,因爲o2與o都指向內存堆內的同一個地址,因此o2的值也應該也是8。
呵呵,請注意,8是值類型,o是引用類型,類型不同,要進行裝箱操做,裝箱的過程當中會建立新的實例分配新的內存空間。因此引用類型變量o指向了新的內存堆空間了,因爲引用類型變量o2沒有作任何操做,因此此時引用類型變量o和o2在內存棧中存儲的地址不同了,指向的內存堆地址也不同了,因此它們的值也就不同了。以下圖10,圖11
圖 10
圖 11
那如何讓o的值改變,o2的值也同時變化,就要改變o對應的內存堆內的值。
因此最後結果的值是:i is 1,o is 8,o2 is 1
通篇經過一則簡短的賦值程序,介紹了
1)C#兩大類型:值類型與引用類型
2)值類型與引用類型互相賦值,引出的裝箱、拆箱操做
其中簡要介紹了裝箱操做會有比較大的性能損耗,特別是垃圾回收。
最後,經過兩張圖來簡要歸納下本篇博文的內容:
1)C#兩大類型:
2)變量賦值