栈和堆是我们 Rust 代码在运行时可用的内存区域。
Rust 是一种内存安全的编程语言。为确保 Rust 内存安全,它引入了所有权、引用和借用等概念。
要理解这些概念,我们必须先了解如何在栈和堆之间分配和释放内存。
栈
栈可以看作是一叠书。当我们添加更多书时,我们将它们放在最上面。当我们拿书时,我们从最上面拿。
栈按顺序插入值。它按照相反的顺序获取并移除值。
- 添加数据称为“压栈”。
- 移除数据称为“出栈”。
这种现象在编程中被称为“后进先出 (LIFO)”。
存储在栈上的数据在编译时必须具有固定的大小。Rust 默认会为原始类型在栈上分配内存。
让我们通过一个例子来可视化栈上内存如何分配和释放。
fn foo() {
let y = 999;
let z = 333;
}
fn main() {
let x = 111;
foo();
}
在上面的例子中,我们首先调用 main()
函数。main()
函数有一个变量绑定 x
。
当 main()
执行时,我们将一个 32 位整数(x
)分配给栈帧。
地址 | 名称 | 值 |
---|---|---|
0 | x | 111 |
在表中,“地址”列指的是 RAM 的内存地址。
它从0开始,直到您的计算机有多少 RAM(字节数)。“名称”列指的是变量,“值”列指的是变量的值。
当调用 foo()
时,会分配一个新的栈帧。foo()
函数有两个变量绑定:y
和 z
。
地址 | 名称 | 值 |
---|---|---|
2 | z | 333 |
1 | y | 999 |
0 | x | 111 |
数字0、1和2不使用计算机实际使用的地址值。实际上,地址会根据值以一定的字节数分隔。
在 foo()
完成后,其栈帧会被释放。
地址 | 名称 | 值 |
---|---|---|
0 | x | 111 |
最后,main()
完成,所有内容都消失了。
Rust 会自动在栈内和栈外进行内存的分配和释放。
堆
与栈相反,大多数时候我们需要将变量(内存)传递给不同的函数,并让它们比单个函数的执行时间更长地存活。这时我们可以使用堆。
我们可以使用 Box<T>
类型在堆上分配内存。例如,
fn main() {
let x = Box::new(100);
let y = 222;
println!("x = {}, y = {}", x, y);
}
输出
x = 100, y = 222
让我们可视化上面例子中调用 main()
时的情况。
地址 | 名称 | 值 |
---|---|---|
1 | y | 222 |
0 | x | ??? |
和之前一样,我们在栈上分配了两个变量 x
和 y
。
但是,当调用 Box::new()
时,x
的值会在堆上分配。因此,x
的实际值是指向堆的指针。
内存现在看起来像这样:
地址 | 名称 | 值 |
---|---|---|
5678 | 100 | |
… | … | … |
1 | y | 222 |
0 | x | → 5678 |
在这里,变量 x
持有一个指向地址→ 5678的指针,这是一个用于演示的任意地址。堆可以按任何顺序分配和释放。因此,它可能最终拥有不同的地址,并在地址之间产生空隙。
所以,当 x
消失时,它首先会释放堆上分配的内存。
地址 | 名称 | 值 |
---|---|---|
1 | y | 222 |
0 | x | ??? |
一旦 main()
完成,我们就会释放栈帧,所有内容都会消失,释放所有内存。
我们可以通过转移所有权来使内存保持更长时间的生命周期,这样堆就可以比分配 Box
的函数更长地存活。要了解更多关于所有权的信息,请访问Rust 所有权。
栈与堆的区别
栈 | 堆 |
---|---|
访问栈中的数据速度更快。 | 访问堆中的数据速度较慢。 |
栈上的内存管理是可预测且简单的。 | 堆的内存管理(任意大小)比较复杂。 |
Rust 默认在栈上分配。 | 使用 Box 在堆上分配。 |
原始类型和函数的局部变量在栈上分配。 | 大小可变的数据类型,如 String 、Vector 、Box 等,在堆上分配。 |