堆疊與堆積

程式執行時所有資料都要載入記憶體存放,資料佔用的空間與儲存位置由資料型別決定;程式執行期間會使用兩塊記憶體區域來存放資料:堆疊(Stack)堆積(Heap)

堆疊(Stack)

堆疊本質是一段連續記憶體陣列,遵循後進先出(LIFO)的資料結構,主要存放三類資料:

  • 部分型別變數的數值
  • 程式當下的執行上下文
  • 傳入方法的參數
堆疊就像裝硬幣的投幣筒,只能從單一端口放入、取出內容。

堆疊的記憶體分配與釋放全由系統自動控管,開發者不需手動操作;搞懂堆疊的底層運作邏輯,能更順利看懂程式執行流程與 C# 官方文件。

堆疊核心特性

  • 僅能在堆疊頂端新增、刪除資料
  • 往堆疊頂端存入資料:推入(Push)
  • 從堆疊頂端取出資料:彈出(Pop)

堆疊記憶體特色

  • 記憶體分配釋放速度極快,使用連續記憶體空間
  • 生命週期綁定方法呼叫:方法執行完畢,堆疊框架自動銷毀
  • 存放內容:實值型別變數、方法參數、區域變數、執行上下文

透過簡單範例,更容易理解堆積的運作方式

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        int sum = Add(a, b);
    }

    static int Add(int x, int y)
    {
        return x + y;
    }
}
Code language: JavaScript (javascript)

這段範例邏輯十分簡單:從程式入口 Main 函式第一行開始建立 int 型別變數 a,第二行建立變數 b,第三行呼叫 Add 函式並傳入 a、b 的數值進入函式內部。x 承接 a 的值、y 承接 b 的值,函式回傳 x 加 y 的運算結果,數值為 30 並送回呼叫處指派給 sum。Add 執行結束後 x、y 會從堆疊彈出,sum 數值更新為 30;Main 執行完畢後程式結束。下面我們一步步拆解整個堆疊推入、彈出的完整流程。

程式執行流程與堆疊推入彈出步驟

建立變數 a 時,內建基礎型別會直接配置在堆疊中,執行推入動作
執行第二行程式,建立第二個整數變數 b,數值為 20;先推入的內容位於堆疊底部,後推入的內容會在堆疊頂端
第三行程式會先執行等號右側 Add(a,b),呼叫 Add 函式並把 a、b 的數值複製給 x、y;這裡僅傳遞數值,a 和 x 是兩個完全獨立的變數,互不影響。
此步驟僅計算 x 加 y 並回傳結果,不需在堆疊長期存放;運算結果 30 直接送回呼叫位置,等同 int sum = 30 的效果。執行完 return x+y 後 Add 函式即將結束,函式內部建立的區域變數都要執行彈出,彈出會清空該變數佔用的記憶體空間,供後續重複使用。
堆疊最上方的位置稱為堆疊頂(Top),變數彈出後堆疊頂會自動往下移動。
Add 執行完畢,x、y 屬於該函式內部的區域參數,必須依序彈出銷毀,釋放堆疊空間維持系統流暢。
彈出後 x、y 就不存在;Add 回傳的 30 送至呼叫處,準備指派給 sum。
執行 int sum = 30 會再次觸發推入動作,將 sum(數值30)放在堆疊頂端。該敘述執行完後 Main 函式即將結束,接下來要依序銷毀屬於 Main 的區域變數 sum、b、a(彈出 Pop)。
彈出永遠從堆疊頂端開始;堆疊如同只有單一出入口的通道,資料只能從同一端進出。因此銷毀變數順序與建立順序相反:先銷毀 sum,再來 b,最後才是 a。
變數 b 執行彈出並銷毀,堆疊頂指標自動向下移動。
所有變數皆已彈出,最後一個變數 a 也清除完畢。嚴格來說 Main 還有另一個區域變數:Main(string[] args) 的字串陣列,用來接收控制台啟動時傳入的命令列參數。args 陣列的儲存位置分為兩塊,這部分和後續介紹的堆積有關,本示範暫不討論。

各位讀者記住一個重點:函式執行時變數由上至下推入堆疊,順序為 a → b → 進入 Add → x → y;Add 結束 x、y 彈出,接著推入 sum;Main 結束後 sum、b、a 依反向順序彈出銷毀。


堆積(Heap)

堆積是一塊大型記憶體區域,用來配置記憶體區塊存放物件資料;和堆疊不同,堆積內的物件可任意順序新增、刪除。

程式無法手動刪除堆積內的物件,由 CLR(通用語言執行平台)的垃圾回收器 GC自動清理無任何參考、無法存取的孤立物件;省去其他語言手動釋放記憶體容易出錯的步驟。

堆積可以想像成一張大書桌,能隨意擺放各式物品;前面介紹的堆疊則像投幣筒,僅能單端進出資料。

堆積如同寬廣的書桌,擺放物品不受位置限制。開發者不用手動刪除堆積物件,交由 GC(垃圾回收器)管理;一旦物件沒有任何程式參考,GC 會不定時自動銷毀釋放記憶體。這大幅減輕開發者負擔,也避免忘記釋放資源造成記憶體滿載、程式崩潰的問題。像 C++ 這類語言,所有堆積記憶體都要開發者手動釋放。

GC 回收流程

  1. 程式在堆積建立三個物件,並持有有效參考
  2. 程式不再使用其中一個物件,該物件失去所有參考
  3. GC 偵測到無參考的孤立物件,釋放其佔用記憶體
  4. 回收完成,釋放的記憶體可重新配置給新物件使用

堆積記憶體特色

  • 記憶體空間不連續,配置速度比堆疊慢
  • 生命週期不受方法範圍限制,統一由 GC 管理
  • 存放內容:參考型別實體(類別、陣列、委派等),堆疊只存放物件的記憶體位址

透過以下範例更直觀說明:前面提到基礎型別區域變數存在堆疊,而開發者自訂型別大多存放於堆積。請看範例程式:

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        Student tom = new Student("Tom", 12);
        Student lucy = new Student("Lucy", 12);
    }

}

class Student
{
    //成員變數
    private string name;

    private int age;

    //建構函式,後續章節會詳細解說
    public Student(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    //成員方法
    public void DisplayInfo()
    {
        Console.WriteLine($"Name: {name}, Age: {age}");
    }
}   
Code language: JavaScript (javascript)

下圖展示 Main 內四個變數 a、b、tom、lucy 的記憶體存放示意。和前面堆疊教學邏輯相同,程式由上至下執行,先推入 a、再推入 b,a 與 b 的數值直接存放在堆疊中。

基礎型別變數直接存放於堆疊內
自訂 Student 類別的實際資料儲存於堆積,堆疊只存放該物件在堆積的記憶體位址
舉例 tom 的資料存在堆積 00001 區塊,堆疊就儲存 00001 這個位址,後續可透過位址定位物件資料。

Main 執行完畢後,堆疊內 lucy: 01010 這個位址參考會彈出銷毀;但銷毀的只有位址資訊,堆積裡真正的物件資料依舊存在。此時堆積物件失去所有堆疊變數的參考記錄。

堆積內 Lucy 物件(區塊01010)不再有任何堆疊變數儲存其位址,成為無人參考的孤立物件;垃圾回收器 GC 會在合適時機銷毀釋放記憶體。
堆積中的 Lucy 物件失去所有外部參考,因為堆疊存放位址的參考已經彈出。物件變成無人參照狀態後,GC 會擇機清除釋放記憶體供後續重複使用;只要還有任何程式持有參考,GC 就不會銷毀該物件。

記憶體區域說明

堆疊、堆積都屬於軟體層次劃分的記憶體區段,並非實體硬體層面的分區。

堆疊與堆積只是軟體定義的邏輯記憶體區塊,實體記憶體硬體並沒有專屬標示「堆疊區/堆積區」的硬體模組。

實體記憶體 RAM 僅有統一的記憶晶片與連續實體位址,不存在任何區分堆疊、堆積的硬體標記

作業系統會為每個執行中的程式分配獨立虛擬記憶體空間,並預先將這塊虛擬記憶體切分為四大邏輯區段:

程式碼段:存放編譯後的程式指令

全域靜態段:靜態變數、常數

堆疊段(Stack):程式主執行緒、子執行緒各自獨立擁有專屬堆疊記憶體

堆積段(Heap):大型共享記憶池,動態配置記憶體

這裡提到的堆疊、堆積,僅是作業系統給行程劃分的邏輯區域,硬體層面並無隔離機制。

堆疊與堆積差異

發佈留言

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