實值型別與參考型別

資料項的型別會決定兩件事:存放該資料所需的記憶體容量(位元數),以及此型別包含的資料成員;除此之外,型別也會規範物件在記憶體中的配置位置,分為堆疊(Stack)與堆積(Heap)兩種。

型別主要分成兩大類:實值型別(Value Types)參考型別(Reference Types),兩類物件在記憶體的存放機制完全不同。

1、實值型別只佔用單一區塊記憶體,這塊記憶體會直接存放原始資料內容。

實值型別的資料直接存放在堆疊Stack當中,Data欄位直接儲存數值,舉例int age = 12; 數值12就直接存放於此堆疊區塊

2、參考型別需要兩段獨立記憶體:

  • 第一段用來存放物件真實資料,且永遠配置在堆積裡
  • 第二段是一組參考位址,指向堆積中資料的存放位置。(堆疊只存堆積的記憶體位址,C語言時代稱為指標)
參考型別的資料主體存放在堆積,會分配一組記憶體位址例如000001,堆疊則儲存這組對應的參考位址Reference

上面兩張示意圖,僅適用於物件不隸屬其他物件之資料成員的狀況。

重點提醒

參考型別物件的實體資料區塊,一律存放在堆積,如上圖所示。

但實值型別執行個體、或是參考型別的參考位址,存放位置可能是堆疊也可能是堆積,取決於程式碼的撰寫結構。

舉例說明:

假設有一個參考型別 Person,內含兩個成員變數:一個實值型別成員、一個參考型別成員。

不少人會誤以為:實值型別成員放堆疊、參考位址放堆疊,物件實體放堆積(如上圖),但這個觀念是錯誤的。

記住核心規則:參考型別執行個體的全部實體資料永遠配置在堆積。兩個成員都屬於該物件的實體內容,因此不論成員本身是實值或參考型別,都會一併存放在堆積內。

程式範例:

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        Person bill = new Person("Bill", 18);
    }

}

class Person
{
    private string name;

    private int age;

    private Phone myPhone;

    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
        this.myPhone = new Phone();
        this.myPhone.number = 12345;
    }
}   

class Phone
{
    public long number;
}Code language: JavaScript (javascript)

觀察上面範例,Person類別的成員除了基礎型別age、name,還有自訂型別Phone;而Phone本身又包含long基礎型別,用來儲存手機號碼。

讀者看到Person內的int age、或是Phone內的long number,可能會覺得它們是實值型別就該存放在堆疊,實際並非如此。就算這些是內建基礎型別,只要做為自訂類別的成員變數,就會跟隨物件執行個體一併存放在堆積,完整記憶體配置如下:

從圖中可見int age與long number雖是內建實值型別,但做為自訂類別成員時存放於堆積;myPhone儲存的是參考位址,由此可見參考型別的參考欄位也有可能存放在堆積。

請記住:所有自訂類別的成員變數一律存放在堆積,無論該成員是實值或參考型別。若類別內有參考型別成員(例如範例中的myPhone),該欄位只會存放另一個物件在堆積的位址。

如果Phone類別當中再包含其他自訂型別,Phone的記憶體區塊就會再多一組欄位,存放另一個自訂物件的參考位址。

整支程式從Main函式開始執行,最頂層的參考物件位址存放在堆疊,後續所有關聯物件會像鏈條一樣層層連結在堆積中。

物件參考會形成一條記憶體鏈結

另一個範例

// 參考型別類別
class MyType
{
    public int A;       // 實值型別成員,內嵌於堆積的MyType執行個體內
    public Person B;    // 參考型別成員,參考指標存堆積,物件實體另開堆積空間
}

class Person
{
    public string Name;
}

void Test()
{
    // 堆疊存放myObj的參考指標
    MyType myObj = new MyType(); 
    /*
    堆積配置:
    MyType物件 {
        A: 內嵌int數值
        B: 指向Person的參考指標
    }
    另一塊堆積空間:Person{ Name: ... }
    */

    // 區域實值變數,直接配置在堆疊
    int localNum = 10;
}Code language: PHP (php)

上述規則適用於類別的成員變數,不包含類別內的函式方法。若類別方法內宣告區域變數或參考物件,這些區域實值變數、參考物件的位址依舊存放在堆疊,可參考下方程式碼:

再看以下範例:

using System.ComponentModel.DataAnnotations;

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        Person bill = new Person("Bill", 18);
        bill.Calculate();
    }

}

class Person
{
    private string name;

    private int age;

    private Phone myPhone;

    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
        this.myPhone = new Phone();
        this.myPhone.number = 12345;
    }

    public int Calculate()
    {
        int one = 12;
        int two = 23;
        Phone myNewPhone = new Phone();
        return one + two;
    }
}   

class Phone
{
    public long number;
}Code language: PHP (php)

我在這份程式中替Person新增Calculate方法,方法內同時有實值型別與參考型別的區域變數,由Main呼叫執行。這些變數不屬於Person的成員,而是方法專屬的區域變數:區域實值型別存放於堆疊,區域參考型別的參考位址同樣存放在堆疊。

執行 bill.Calculate(); 這行程式時,記憶體配置狀況如下

圖中藍色區塊,就是方法執行期間內部變數的記憶體存放位置。

一旦方法執行完畢,這些區域變數就會從堆疊中彈出,記憶體布局變成如下樣貌

Calculate方法執行完畢後流程回到Main,方法內建立的所有區域變數會從堆疊移除銷毀。但myNewPhone的物件實體(圖中藍色部分)仍存於堆積,堆疊僅釋放參考位址;真正的物件資料不會立刻清除,會留在堆積等待垃圾回收器GC在合適時機回收。

實值型別與參考型別

Previous:
Next:

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *