一.前言

從這個簡單程式的輸出結果,你想到了什么?是不是與你心中想的結果不一致?是不是覺得輸出的結果應該為:i is 1,o is 8,o2 is 8
二.程式執行前

圖 2
我們都知道,每一個方法在執行前,作業系統會給方法內每個變數分配記憶體空間,從圖2中就可以看出,在執行前各變數(i,o,o2)已分配了記憶體,且各自都有初始值,
從圖中,可以發現變數i和變數o,o2有些許不同,變數i在記憶體中存盤的值和程式中的值是一樣的,都是0;變數o,o2在記憶體中存盤的值和程式中的值不一樣,記憶體中存盤的值是一個地址(0x00000000),程式中的值是null,那變數o,o2的null值存盤在哪呢?為什么變數i和變數o,o2會有如此大的不同呢?
我們都知道,C#有兩大型別:值型別別和參考型別,圖2中int屬于值型別,object屬于參考型別,接下來,介紹一下值型別和參考型別:
1.值型別的值存盤在記憶體堆疊上,參考型別的值存盤在記憶體堆中,
園中有很多博文這么描述,我用程式驗證了一下全域的值型別變數的值,靜態的值型別變數的值,參考型別實體中值型別成員的值,如下圖3

圖 3
從圖中,可以看出變數(j,o,seg,st)的值應該是在同一個存盤區域中,而變數(gi)是在另外一個存盤區域中,參考型別Student的成員Age的地址還未分配,所以說值型別的值存盤在記憶體堆疊上是不準確的,
查找了一些資料,記憶體格局分為四個區:
1)全域資料區:存放全域變數,靜態變數,常量的值
2)代碼區:存放程式代碼
3)堆疊區:存放為運行而分配的區域變數,引數等
4)堆區:自由存盤區,
更為準確的說,方法體內的值型別變數的值存盤在記憶體堆疊上,參考型別變數的值存盤在記憶體堆上,由于物件實體是參考型別變數的值,而物件實體成員只是物件實體的一部分,所以其隨物件實體整個存盤在記憶體堆上,
或許眼尖的園友發現了,上面那句話還是不對,結構體StructEg的參考型別成員Name的資料就沒有存盤在記憶體堆疊上,從圖3看,結構體變數seg的資料分成兩部分,值型別成員資料存盤在記憶體堆疊上,參考型別成員資料存盤在記憶體堆上,
所以確切的說:方法體內的預定義的值型別(如int,bool,char)變數的資料存盤在記憶體堆疊上,參考型別變數的值存盤在托管堆中,結構體的值型別成員的值存盤在記憶體堆疊上,結構體的參考型別成員的值存盤在記憶體堆中,(下面介紹的值型別基本是預定義的值型別和只包含值型別成員的結構體,一般包含參考型別成員的都定義成類)
2. 值型別變數直接存盤資料,而參考型別變數則存盤對資料的參考
這句話怎么理解呢?這句話中關鍵詞是”存盤”,其實還是在描述程式中的變數在記憶體堆疊中的表現,
值型別變數在記憶體堆疊中存盤的是其在程式中的變數值,參考型別變數在記憶體堆疊中存盤的是其程式中的值在記憶體堆中的參考,(當然值型別變數和參考型別變數都是方法體內的區域變數或引數)
3.參考型別變數賦值程序
1)分配記憶體堆空間:我們都知道要存盤資料,首先得申請記憶體空間,參考型別變數在new實體化時,系統在記憶體堆中分配空間,
2)更新地址:把參考型別變數在記憶體堆疊中存盤的值更新成新的值(新值為新分配的記憶體堆的首地址),至此,參考型別變數指向了新的記憶體空間,
3)填充值:把初始化值填充到記憶體堆中,
可能有些園友會說,了解這個有什么意義呢?那我就簡單的說一個現象:
1)在學習方法理論時,傳參會有這樣的描述:值型別按值傳遞,傳遞的是物件的副本,對已呼叫方法中的物件的更改對原始物件無影響;參考型別的物件按值傳遞傳遞的是對物件的參考,使用此參考更改物件的成員,此更改將影響原始物件,
其實,這里究其原理,就是因為值型別與參考型別的值的不同存盤位置,來描述其傳參后的影響,
所以,有時我們為了影響值型別實參的值,而在形參前面加ref或out;有時我們為了不影響參考型別實參的值,而采用深拷貝的方式傳遞引數值,
理論是為了指導實踐,當了解了其原理,在實踐時,我們才會顯得踏實,
三.執行變數i賦值

從圖中可以看出,執行后,值型別變數在記憶體中存盤的值和其在程式中的值是一樣的,都是1,
四.執行object o=i;

圖 5
從圖中可以看出,參考型別變數o的值變成1了,在記憶體堆疊中存盤的值更新成新地址了,通過前面分析,我們知道變數o指向了1的新地址,值型別變數的值存盤在記憶體堆疊中,參考型別變數的值存盤在記憶體堆中,記憶體堆疊中的值是如何到記憶體堆中的?這就是本節要介紹的第二個重要概念,裝箱和拆箱,
4.1.裝箱
1.裝箱:裝箱是把值型別到object型別或值型別到其實作的介面的隱式轉換,
園中很多博文介紹:值型別轉換為參考型別,就叫裝箱,我覺得這表述不太準確,如下圖6

圖 6
從圖6中可以看出,值型別不能隨意的轉換為參考型別,它只能隱式轉換為以下兩種參考型別:
1)object型別;
2)值型別實作的介面
2.裝箱的程序
前面已介紹了參考型別變數賦值程序了,裝箱步驟也類似:
1)分配新的記憶體空間
2)更改地址
3)填充值:從值型別變數處拷貝一份值,存盤到新分配的記憶體堆中,
4.2.拆箱
1.拆箱:從 object 型別到值型別或從介面型別到實作該介面的值型別的顯式轉換,
2.拆箱程序
1)檢查物件實體,以確保它是給定值型別的裝箱值,若不能顯式轉換,則拋例外,
意思是:裝箱時的值型別和拆箱時的值型別要完全一致(哪怕型別兼容也不行,如下圖7中的裝箱前的型別是short,拆箱后的型別是int,就會產生例外),如圖7

圖 7
2)驗證成功后,復制實體的值到值型別變數中
相對于簡單的賦值而言,裝箱和拆箱程序需要進行大量的計算,所以其對性能會有較大的損耗,特別裝箱時,要創建新的物件實體,要在記憶體堆上分配新的記憶體空間,在分配新的記憶體空間時,可能會引起垃圾回收(垃圾回收對性能損耗非常大,具體垃圾回收為什么會有很大的性能損耗,網上相關介紹很多,在此不做介紹),
或許有園友會說,平時裝箱/拆箱操作不多,其實在你不經意間,存在很多裝箱操作
1)string s=string.Format(“{0}”,i);//i為值型別資料---典型的字串格式化
四.執行object o2=o;

圖 8

圖 9
無論是值型別變數賦值還是參考型別變數賦值,都是把資料復制一份,然后賦給另一個變數,只是參考型別變數在記憶體堆疊中存盤的是其值在記憶體堆中的地址,所以參考型別變數間賦值,就使兩個變數指向了同一個記憶體堆空間,如上圖8,圖9
五.執行o=8
此時,或許有人會說,這句不是表示對參考型別變數進行操作嗎?賦值了8后,它在記憶體堆內的值應該是8了,由于o2與o都指向記憶體堆內的同一個地址,所以o2的值也應該也是8,
呵呵,請注意,8是值型別,o是參考型別,型別不一樣,要進行裝箱操作,裝箱的程序中會創建新的實體分配新的記憶體空間,所以參考型別變數o指向了新的記憶體堆空間了,由于參考型別變數o2沒有做任何操作,所以此時參考型別變數o和o2在記憶體堆疊中存盤的地址不一樣了,指向的記憶體堆地址也不一樣了,所以它們的值也就不一樣了,如下圖10,圖11

圖 10

圖 11
那如何讓o的值改變,o2的值也同時變化,就要改變o對應的記憶體堆內的值,
六.最后執行Console.WriteLine("i is " + i.ToString() + ",o is " + o.ToString() + ",o2 is " + o2.ToString());
所以最后結果的值是:i is 1,o is 8,o2 is 1
七.總結:
通篇通過一則簡短的賦值程式,介紹了
1)C#兩大型別:值型別與參考型別
2)值型別與參考型別互相賦值,引出的裝箱、拆箱操作
其中簡要介紹了裝箱操作會有比較大的性能損耗,特別是垃圾回收,
最后,通過兩張圖來簡要概括下本篇博文的內容:
1)C#兩大型別:

2)變數賦值

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/107267.html
標籤:C#
上一篇:C# 方法多載
