1. 程式人生 > >淺析值型別與引用型別的記憶體分配[轉載]

淺析值型別與引用型別的記憶體分配[轉載]

1、值型別和引用型別的區別

1.值型別的資料儲存記憶體的棧中;引用型別的資料儲存在記憶體的堆中,而記憶體單元中只存放堆中物件的地址。

2.     值型別存取速度快,引用型別存取速度慢。

3.     值型別表示實際資料,引用型別表示指向儲存在記憶體堆中的資料的指標或引用

4.     值型別繼承自System.ValueType,引用型別繼承自System.Object

5.     的記憶體分配是自動釋放;而堆在.NET中會有GC來釋放

C#中基本資料型別是值型別,結構體、列舉也是值型別。而陣列、類、介面、字串都是引用型別。

作者:林立

      大家都知道要學好 .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方互不相干;引用型別把引用複製給對方,從而雙方都指向同一個堆中的例項,其中任何一方對例項做出修改,都會在另一方的操作中得到反映。最後我們通過複雜型別的內部成員的記憶體佈局情況,進一步瞭解了值型別和引用型別的記憶體佈局情況。