C#引用型別和值型別在堆、棧中的儲存

知更鳥的碼發表於2020-10-12

一、棧和堆是什麼

程式執行時,它的資料必須儲存在記憶體中。一個資料項需要多大的記憶體、儲存在什麼地方、以及如何儲存都依賴於該資料項的型別。

執行中的程式使用兩個記憶體區域來儲存資料:棧和堆。

1、棧

棧是一個記憶體陣列,是一個LIFO(last-in first-out,後進先出)的資料結構。棧儲存幾種型別的資料:某些型別變數的值、程式當前的執行環境、傳遞給方法的引數。

棧的特點:(1)資料只能從棧的頂端插入和刪除。(2)把資料放到棧頂稱為入棧。(3)從棧頂刪除資料稱為出棧。

2、堆

堆是一塊記憶體區域,在堆裡可以分配大塊的記憶體用於儲存某種型別的資料物件。與棧不同,堆裡的記憶體能夠以任意順序存入和移除。下圖展示了一個在堆裡放了4項資料的程式。

雖然程式可以在堆裡儲存資料,但並不能顯式地刪除它們。CLR的自動垃圾收集器在判斷出程式的程式碼將不會再訪問某資料項時,會自動清除無主的堆物件。下圖闡明瞭垃圾收集過程。

二、值型別和引用型別

C#中的值型別和引用型別大致如下圖所示

程式的資料項的型別定義了儲存資料需要的記憶體大小以及組成該型別的資料成員。型別還決定了物件在記憶體中的儲存位置——棧或堆。

值型別與引用型別的儲存方式:

值型別:值型別只需要一段單獨的記憶體,用於儲存實際的資料。

引用型別:引用型別需要兩段記憶體,第一段儲存實際的資料,它總是位於堆中。第二段是一個引用,指向資料在堆中的存放位置。


問題:如下程式碼所示,我們都說值型別存在於棧中,引用型別的實際資料在棧中,引用存在堆中。那麼下面的程式碼,當例項化 Student stu=new Student()時,那麼Age屬於值型別會存在於棧中嗎?StudentAddress屬於引用型別,會在棧和堆之間分成兩半嗎?答案是否定的。

public class Student
{ 
    public int Age { get; set; }
    public string Name { get; set; }
    public Address StudentAddress { get; set; }
}
public class Address
{

   public string address { get; set; }
}

請記住,對於一個引用型別,其例項的資料部分始終存放在堆裡。既然兩個成員都是物件資料的一部分,那麼它們都會被存放在堆裡,無論它們是值型別還是引用型別。如下圖所示:

儘管成員Age是值型別,但它也是new Student()例項資料的一部分,因此和物件的資料一起被存放在堆裡。

成員StudentAddress是引用型別,所以它的資料部分會始終存放在堆裡,正如圖中“資料”框所示。不同的是,它的引用部分也被存放在堆裡,封裝在new Student()物件的資料部分中。

說明:對於引用型別的任何物件,它所有的資料成員都存放在堆裡,無論它們是值型別還是引用型別。

三、申明變數時,記憶體是如何變化的?

當你在一個.NET應用程式中定義一個變數時,在RAM中會為其分配一些記憶體塊。這塊記憶體有三樣東西:變數的名稱、變數的資料型別以及變數的值。

在.NET中有兩種可分配的記憶體:棧和堆。你的變數究竟會被分配到哪種型別的記憶體取決於資料型別。在接下來的幾個部分中,詳細地說明這兩種型別的儲存。

Line 1:當這一行被執行後,編譯器會在棧上分配一小塊記憶體。棧會在負責跟蹤你的應用程式中是否有執行記憶體需要。

.Net(C#)程式語言中的棧與堆

Line 2:現在將會執行第二步。棧會將此處的一小塊記憶體分配疊加在剛剛第一步的記憶體分配的頂部。你可以認為棧就是一個一個疊加起來的房間或盒子。在棧中,資料的分配和解除都會通過LIFO (Last In First Out)即先進後出的邏輯規則進行。換句話說,也就是最先進入棧中的資料項有可能最後才會出棧。

.Net(C#)程式語言中的棧與堆

Line 3:在第三行中,我們建立了一個物件。當這一行被執行後,.NET會在棧中建立一個指標,而實際的物件將會儲存到一個叫做“堆”的記憶體區域中。“堆”不會監測執行記憶體,它只是能夠被隨時訪問到的一堆物件而已。不同於棧,堆用於動態記憶體的分配。

需要注意的重點是物件的引用指標是分配在棧上的。 例如:宣告語句 Class1 cls1; 其實並沒有為Class1的例項分配記憶體,它只是在棧上為變數cls1建立了一個引用指標(並且將其預設值為null)。只有當其遇到new關鍵字時,它才會在堆上為物件分配記憶體。

.Net(C#)程式語言中的棧與堆

離開這個Method1方法時:現在執行的控制語句開始離開方法體,這時所有在棧上為變數所分配的記憶體空間都會被清除。換句話說,在上面的示例中所有與int型別相關的變數將會按照“LIFO”後進先出的方式從棧中一個一個地出棧。

注意:這時它並不會釋放堆中的記憶體塊,堆中的記憶體塊將會由垃圾回收器稍候進行清理。

.Net(C#)程式語言中的棧與堆

四、總結

Heap space 堆空間: 所有存活的物件在此分配.

Stack space 棧空間: 方法呼叫時儲存變數物件的引用或變數例項.

在C#中只要是成員變數,一旦它所在類被例項化後,都是作為一個整體放在堆記憶體的,不管它是值型別還是引用型別。區域性變數才是放在棧記憶體的。而類的方法是所有的物件共享的,方法是存在方法區的,只用當呼叫的時候才會被壓棧,不用的時候是佔記憶體的。

簡單來說,值型別和引用型別變數本身在棧中分配記憶體,引用型別的例項在堆中分配記憶體。(要注意的是,一定要理解清楚引用型別變數本身和引用型別的例項的區別,引用型別變數好比一個指標,它所指向的內容即引用型別的例項)。有時候又會看到一些說法,值型別在其所定義的位置分配記憶體,這讓人感到很混亂卻也不得不在意,下面簡單捋一下。

如下面程式碼:

public class TestClass 
{
      int a = 0;
}
 
public void Function()
{
     int b = 0;
     TestClass class1 = new TestClass();
}

方法Function中定義的值型別(int b)在棧中分配,引用型別(類class1)以一個類似於指標的形式也儲存於棧中,而類的例項物件即程式碼中class1所引用的實際資料(整型a)是在堆上面分配的。這就可以理解,為什麼有的地方會說“值型別在其所定義的地方分配”,因為上述程式碼中的值型別a由於定義在類中,作為類的一個成員,在類例項化時是被分配到堆中的。

因此,更進一步地,可以理解為:值型別作為一個方法中的區域性變數時,是在棧中分配的,而當作為類的成員變數時,是分配在堆中的

在上面程式碼的TestClass中新增一個Run函式:

public class TestClass 
{
    int a = 0;
    public void Run()
    {
        int c = 0;
    }
}
 
public void Function()
{
    int b = 0;
    TestClass class1 = new TestClass();
    class1.Run();
}

此時,整型a是在堆中分配記憶體,而Run函式中的整型c在棧中分配記憶體。

因此關於值型別、引用型別各自在堆或棧上的記憶體分配可以總結為:

值型別作為方法中的區域性變數時,在棧中分配,而作為類的成員變數時,在堆中分配;引用型別變數在棧中分配,引用型別的例項在堆中分配

 

相關文章