資料項的型別會決定兩件事:存放該資料所需的記憶體容量(位元數),以及此型別包含的資料成員;除此之外,型別也會規範物件在記憶體中的配置位置,分為堆疊(Stack)與堆積(Heap)兩種。
型別主要分成兩大類:實值型別(Value Types)與參考型別(Reference Types),兩類物件在記憶體的存放機制完全不同。
1、實值型別只佔用單一區塊記憶體,這塊記憶體會直接存放原始資料內容。

2、參考型別需要兩段獨立記憶體:
- 第一段用來存放物件真實資料,且永遠配置在堆積裡;
- 第二段是一組參考位址,指向堆積中資料的存放位置。(堆疊只存堆積的記憶體位址,C語言時代稱為指標)

上面兩張示意圖,僅適用於物件不隸屬其他物件之資料成員的狀況。
重點提醒
參考型別物件的實體資料區塊,一律存放在堆積,如上圖所示。
但實值型別執行個體、或是參考型別的參考位址,存放位置可能是堆疊也可能是堆積,取決於程式碼的撰寫結構。
舉例說明:
假設有一個參考型別 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,可能會覺得它們是實值型別就該存放在堆疊,實際並非如此。就算這些是內建基礎型別,只要做為自訂類別的成員變數,就會跟隨物件執行個體一併存放在堆積,完整記憶體配置如下:

請記住:所有自訂類別的成員變數一律存放在堆積,無論該成員是實值或參考型別。若類別內有參考型別成員(例如範例中的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在合適時機回收。
實值型別與參考型別