声明变量时,它的值会存储在哪里?
回答“在内存中”只对了一半,也不够完整。即使都位于内存中,放在栈上的值和放在堆上的值,其生命周期也完全不同。
栈溢出这个错误名称、类和结构体的性能差异,以及为什么需要 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 的内部缓冲区也位于堆上。

