计算机科学

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 的内部缓冲区也位于堆上。