宣告變數時,它的值會儲存在哪裡?
回答「在記憶體裡」只對了一半,也不夠完整。即使都在記憶體中,放在堆疊與堆積區的值,其生命週期也完全不同。
堆疊溢位這個錯誤名稱、類別與結構的效能差異,以及 ARC 為何必要——都必須先了解這兩個區域。
本文整理堆疊與堆積區的運作方式、速度差異的原因,以及 Swift 中各項資料會配置到哪裡。
這裡指的是記憶體區域。LIFO 資料結構會在Swift 的堆疊與佇列另行介紹,而實作優先佇列的樹狀資料結構則在堆積區與優先佇列說明。
先來看重點摘要。
- 堆疊:隨函式呼叫建立、隨回傳消失的自動管理區域。速度快。
- 堆積區:在執行階段依需求借用指定大小的動態區域。彈性高,但有管理成本。
- 速度差異的本質,在於「配置與釋放方式」以及「由誰負責整理」。
- 在 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 的內部緩衝區也位於堆積區。

