大家都知道要學好 .NET,深入了解值類型和引用類型是必不可少的。在這裡我給大家簡單分析一下它們內存分配的區別和聯系。
在分析之前,我們先行構造出一個最簡單的類引用類型:
public class MyClass
{
}
局部變量的聲明
在我們使用類型時,代碼裡面必然少不了變量的聲明,我們先看一下方法內的局部變量的聲明,請看如下代碼:
private static void Main()
{
int i;
MyClass mc;
i = 5;
mc = new MyClass();
}
當一個局部變量聲明之後,就會在棧的內存中分配一塊內存給這個變量,至於這塊內存多大,裡面存放什麼東西,就要看這個變量是值類型還是引用類型了。
l 值類型
如果是值類型,為變量分配這塊內存的大小就是值類型定義的大小,存放值類型自身的值(內容)。比如,對於上面的整型變量 i,這塊內存的大小就是 4個字節(一個 int型定義的大小),如果執行 i = 5;這行代碼,則這塊內存的內容就是 5(如圖 -1)。
對於任何值類型,無論是讀取還是寫入操作,可以一步到位,因為值類型變量本身所占的內存就存放著值。
引用類型
如果是引用類型,為變量分配的這塊內存的大小,就是一個內存指針(實例引用、對象引用)的大小(在 32位系統上為 4字節,在 64位系統上為 8字節)。因為所有引用類型的實例(對象、值)都是創建在堆上的,而這個為變量分配的內存就存放變量對應在堆上的實例(對象、值)的內存首地址(內存指針),也叫實例(對象)的引用。以圖形化的方式展現仿佛是變量有一條線指向著它在堆中的實例(有如圖 -2),而如果變量的類型還沒有被實例化,則為零地址( null、空引用)。
以下為執行 mc = new MyClass ();代碼後,內存中的示例:
由圖 -2可知,變量 mc中存放的是 MyClass實例(對象)的對象引用,如果需要訪問 mc實例,系統需要首先從 mc變量中得到實例的引用(在堆中的地址),然後用這個引用(地址)找到堆中的實例,再進行訪問。需要至少 2步操作才可以完成實例訪問。
類型賦值
另一個常見的操作就是類型的賦值操作,即變量之間的賦值。由於值類型和引用類型的變量內部存放的內容不同,導致在變量賦值的時候,會有相同的行為而有不同的結果。
值類型
請看如下代碼:
private void SomeMethod()
{
int i, j;
i = 5;
j = i;
j = 10;
}
相信大家一定都知道最後的結果是 i:5, j:10。不過在 .NET中, int類型也是一個結構,不但可以存放整數值,還有一系列的方法和屬性可以使用,而非我們以前學 C語言時的那種單純 int存放一個整數的概念。所以我們現在看針對 int的代碼,其實也是在看針對 struct類型的代碼。
對於值類型的賦值語句“ j = i”,請看圖 -3:
在執行 j = i;語句時,變量 i中的內容被復制了一份,然後放到了變量 j中,此時變量 i和 j都有一個值為 5,同時也可以看出, i和 j的值現在互不相干,完全獨立,所以任意修改其中的某個變量的值,不會影響到另外一個。
引用類型
請看如下代碼:
private void SomeMethod()
{
MyClass x, y;
x = new MyClass();
y = x;
}
代碼中先對 x進行了實例化,然後將 x賦值到 y,這段代碼的結果請看圖 -4:
當執行 y = x;代碼時,變量 x中的內容同樣復制了一份,然後放到了變量 y之中,但是因為變量 x中存放是一個類型實例(對象)的引用,因此這次賦值操作等同於把這個引用傳遞給了變量 y,結果就是 x和 y中的引用指向堆中同一個類型的實例(對象)。
你可以使用 x的引用去修改 MyClass實例(對象),然後用 y的引用得到修改後的 MyClass實例(對象),反之亦可,因為 x和 y引用的是同一個實例(對象)。
復雜類型的內存布局概述
以上內容是以值類型或者引用類型為一個整體敘述值類型和引用類型的變量聲明和賦值的情況。下面我們看看值類型和引用類型內部含有其他類型成員變量(一般稱為字段)的情況。雖然看起來情況似乎復雜了一點,但是只要我們可以把握住值類型的值存放在值類型變量內部,而引用類型的值在堆中存放,引用類型的變量只存放對它實例(對象)的引用這個原則,就可以很清晰的做出分析。
值類型
且看下面的類型定義代碼:
public struct MyStruct
{
/* 注意:作為結構,內部字段是不能象下面所寫那樣,在聲明時直接初始化的。
* 但這裡為了節省篇幅,從表達語義的角度,直接在聲明時初始化了
* 此結構的代碼無法通過編譯的 */
public int i = 5; //值類型
public System.Exception ex = new Exception(); //引用類型
}
在 MyStruct結構中,有2個字段,一個是值類型的i變量,一個是引用類型的ex變量。這種情況下,內存中應該是一個什麼模樣呢?
首先,變量 i和ex作為MyStruct的成員,必然存放在MyStruct實例的內部,而變量i作為值類型,其值就存放在自身;ex作為引用類型,變量內只存放實例(對象)的引用,而實例(對象)則在堆上創建,因此就有如圖-5所示:
引用類型
且看下面的類型定義代碼:
public class MyClass
{
MyStruct ms = new MyStruct(); //上面所述的MyStruct結構
System.Random r = new Random(); //引用類型
}
在 MyClass中,有2個字段成員,一個是我們上面的所定義的MyStruct結構值類型ms,另外一個是Random類類型r。
這裡我們把情況再變得復雜一些了,因為 MyStruct內部還有值類型和引用類型的字段,這時候內存中是一幅什麼景象呢?我們要記住,不管情況多麼復雜,把握住值類型和引用類型的特點,慢慢分析,總會得到正確的結果,正如圖-6所示:
作為引用類型的實例(對象),無論什麼情況,都是在堆中的。而 MyStruct結構作為MyClass的成員,它也在MyClass實例所占的堆內存中,而且因為值類型的值是在自身存放的,所以就是圖-6中看到的結果。整個圖-6,所有的值類型和引用類型的布局,都完全負責值類型和引用類型的特點,沒有例外。
總結
以前在問起值類型和引用類型有什麼區別的時候,經常聽到同學說“值類型存放在棧上,引用類型存放在堆上”。其實這麼說並不嚴謹,因為當值類型作為引用類型的一個成員的時候,它的值是內嵌在引用類型實例內部在堆上存放的。我認為,正確的說法應該是:值類型變量的值存放在變量內部,而引用類型變量的值存放在堆上,變量本身存放一個指向堆中的值的引用。同時我們也可以看到在 2個變量賦值的時候,值類型和引用類型的差別,值類型將自身的值復制給對方,之後,2方互不相干;引用類型把引用復制給對方,從而雙方都指向同一個堆中的實例,其中任何一方對實例做出修改,都會在另一方的操作中得到反映。最後我們通過復雜類型的內部成員的內存布局情況,進一步了解了值類型和引用類型的內存布局情況。