計算機科學

Stack(堆疊)vs Heap(堆積區):變數儲存位置完整整理(含 Swift 值型別)

記憶體中的堆疊會隨函式呼叫一併整理,而堆積區支援動態生命週期與大小。本文整理兩種配置區域的成本、Swift 值型別與參考型別的實際配置,以及它們與 Stack、Heap 資料結構的差異。

閱讀 4 分鐘
Stack(堆疊)vs Heap(堆積區):變數儲存位置完整整理(含 Swift 值型別) 封面圖

宣告變數時,它的值會儲存在哪裡?

回答「在記憶體裡」只對了一半,也不夠完整。即使都在記憶體中,放在堆疊與堆積區的值,其生命週期也完全不同。

堆疊溢位這個錯誤名稱、類別與結構的效能差異,以及 ARC 為何必要——都必須先了解這兩個區域。

本文整理堆疊與堆積區的運作方式、速度差異的原因,以及 Swift 中各項資料會配置到哪裡。

這裡指的是記憶體區域。LIFO 資料結構會在Swift 的堆疊與佇列另行介紹,而實作優先佇列的樹狀資料結構則在堆積區與優先佇列說明。

先來看重點摘要。

  1. 堆疊:隨函式呼叫建立、隨回傳消失的自動管理區域。速度快。
  2. 堆積區:在執行階段依需求借用指定大小的動態區域。彈性高,但有管理成本。
  3. 速度差異的本質,在於「配置與釋放方式」以及「由誰負責整理」。
  4. 在 Swift 中,結構通常位於堆疊,類別實例則位於堆積區。

堆疊:疊起來的盤子

堆疊是儲存函式呼叫資訊的區域。顧名思義,就像疊盤子一樣,是最後放入、最先取出的 LIFO(後進先出)結構。

呼叫函式時,其區域變數、參數與回傳位址會以一個整體(堆疊框架)被放入。函式回傳後,整個框架就會消失。

堆疊的優點就在這裡。

配置速度很快。只要向上移動堆疊指標即可,不必搜尋空白空間。

釋放也是免費的。函式結束後只要把指標移回去即可,不必煩惱由誰清理。

但它也有限制。大小必須在編譯時決定,函式結束後一定消失,而且整體上限很小(通常為數 MB)。若遞迴函式沒有終止條件而持續呼叫自己,框架就會超過上限,這就是堆疊溢位。


堆積區:寬廣但需要管理的倉庫

堆積區是在執行階段依需求借用的區域。不必預先知道大小,函式結束後仍可存留,空間也大得多。

代價是管理成本。

配置相對較慢。必須尋找符合要求大小的空間,多個執行緒同時要求配置時也需要協調。

釋放必須由某人負責。資料不會因函式結束而消失,因此必須有人判斷「現在已不再使用」。C 由程式設計師直接呼叫 free,Java 由垃圾收集器定期清理,Swift 則由 ARC 計算參考計數,計數歸零時釋放。

判斷錯誤會造成兩種問題:太晚釋放會導致記憶體洩漏,太早釋放則會存取已釋放的記憶體(懸置指標)。

堆疊框架的參考指向堆積區實際物件的結構
堆疊框架的參考指向堆積區實際物件的結構

Swift 中什麼會配置到哪裡

Swift 官方文件與 WWDC 工作階段反覆強調一個基本架構:值型別(結構、列舉)通常配置在堆疊,參考型別(類別)則配置在堆積區。

struct PointStruct { var x, y: Double }
class PointClass { var x = 0.0, y = 0.0 }

func run() {
    let a = PointStruct(x: 1, y: 2) // 整個放在堆疊框架中
    let b = PointClass()            // 配置在堆積區,堆疊只放參考
}

結構 a 的值會完整放進堆疊框架。函式結束時會隨框架消失,因此不需要參考計數。

類別實例 b 會位於堆積區,堆疊只存放其位址。由於它可從多處參考,因此 ARC 必須計算參考數量。

Apple 建議「優先考慮結構」的效能依據就在這裡:堆積區配置、參考計數與鎖定成本都可以省下。

不過,「結構=一定在堆疊」並不正確。作為類別屬性的結構會和該類別一起存在於堆積區;String、Array 等型別雖然表面上是結構,實際資料緩衝區卻放在堆積區。準確的理解是:「結構具備可放在堆疊上的條件」。

結構完整放在堆疊,類別本體放在堆積區,堆疊只存放參考
結構完整放在堆疊,類別本體放在堆積區,堆疊只存放參考

面試常見追問

「為什麼堆疊比堆積區快?」——堆疊只需移動指標即可完成配置與釋放;堆積區則需要搜尋空間並管理釋放(參考計數、GC)。

「為什麼會發生堆疊溢位?」——堆疊上限很小,深度遞迴或巨大的區域變數會讓框架超過上限。

「區域變數一定在堆疊嗎?」——不是。即使是區域變數,只要是參考型別,本體就在堆積區;堆疊只存放參考。


總結

  • 堆疊會以函式呼叫為單位自動建立與消失。只透過移動指標管理,因此速度快。
  • 堆積區是在執行階段決定大小後借用的區域。彈性高,但需要配置搜尋與釋放管理成本。
  • 釋放責任:堆疊自動處理,堆積區依語言而異(C 手動、Java 使用 GC、Swift 使用 ARC)。
  • 堆疊溢位是因深度遞迴等情況超過堆疊上限而產生的錯誤。
  • 在 Swift 中,結構通常使用堆疊,類別使用堆積區。這就是優先採用值型別的效能依據。
  • 但類別中的結構會存在於堆積區,而 String、Array 的內部緩衝區也位於堆積區。