Skip to content

05.所有权

在 Rust 中,基本数据类型(如 i32, f64, bool, char 等)和固定大小的 struct 变量通常存储在栈(stack) 中,因为它们的大小在编译时是已知的,并且可以直接被分配。

动态大小的数据(如 String, Vec<T>, Box<T> 等),它们的指针和元数据存储在栈上,而实际的数据存储在 堆(heap) 中。

栈帧

在 Rust 中,每次调用一个函数,都会在调用栈(stack)上创建一个新的栈帧(stack frame)。该函数中的变量等存放在该栈帧中。

NOTE

栈和栈帧不是同一个概念,栈以先进后出的方式存储,栈帧存放在栈中,而栈帧不一定按照先进后出的方式存储。

栈帧由 Rust 管理,Rust 不允许手动管理内存。

以下是 2 个代码段对应栈和栈帧内容的示例:

示例 1,共 3 行代码,分别对应 3 个图示:

05.所有权

示例 2,3 张图示分别代表序号 ① ② ③:

05.所有权2

堆和栈

固定大小的变量在分配时被分配到栈上。

我们使用以下方法声明变量时,数据将被分配到栈上,使用 let b = a; 时,发生浅拷贝,即将数据复制一份给到 b,这样会占用较多的内存:

image-20250209180904505

我们可以使用指针,将数据分配到堆上,堆中的数据不依赖与栈帧,可以长期存活。使用 let b = a; 时,b 也是一个指针,此时 a 被移动:

image-20250209181415713

栈和堆的区别可以总结如下:

栈用于保存与特定函数相关联的数据,堆用于保存可能比函数存活更长时间的数据。

Box 智能指针

在 Rust 中的 box,将其理解为一个智能指针,Box<T> 允许将类型 T数据存储在堆上,并在栈上只存储对该数据的指针

使用 box 的集合有 Vec, String, & HashMap ...

IMPORTANT

Box 内存释放原则:

如果一个变量绑定到一个 Box,当 rust 释放变量的栈帧时,rust 也会释放 box 的堆内存。

案例 1:

image-20250209182901007

解释:

执行 L4 所在行代码时,将 first 变量当作参数传递给了 add_suffix 函数,此时,由于数据本身是一个 String ,因此,在栈上新分配一个指针(这里为 name 变量),指向堆上的这个数据,如图 L2,数据 Ferris 的所有权发生转移,从原本的 first 变量转移到了 name 变量,执行完 L3 所在行代码时,由于使用新增字符串的方法,因此原本堆上的数据后面增加了 Jr. ,使用原本内存所在地址,最终,数据赋给 full 变量,数据的所有权从 name 变量转移给了 full。此时,first 已不再可用(移动堆数据原则,见下)。

虽然所有权发生了转移,但指针变量占用的栈上的内存空间不会随着所有权的转移而立即释放,而是等到整个函数执行完成,由 rust 将整个函数的栈帧释放时释放。如上述的 first 指针变量的所有权虽然转移到了 add_suffix 内部,但占用的栈内存不会立即释放,而是等到 main() 函数执行完成后随着整个 main 函数栈帧被释放时释放,但在所有权发生转移后,变量 first 将不再可用。

IMPORTANT

移动堆数据原则:

如果变量 x 将堆中的数据的所有权移动到了另一个变量 y,那么移动后,x 将不再可用。

避免移动:

方法之一是使用 .clone() 方法进行克隆。如下示例,所有权将不会发生移动:

image-20250209200822649

所有权的经典问题

问题 1,以下代码能否通过编译并执行?如果能,输出是什么?

rust
let s = String::from("hello");
let s2;
let b = false;
if b {
    s2 = s;
}
println!("{}", s);

答案:代码不能通过编译。

原因:

在运行时 b 是一个未定变量,rust 无法确定 if 中的语句能否执行,而 rust 需要在编译时就确定所有权的归属。其中,s 是一个在栈上的变量,指向分配到堆上的数据 hello 。如果 if 可以执行,则 s 则将自己指向的数据归属到了 s2,不再拥有数据 hello 的所有权,此时再访问 s 将出错。而无法保证 if 能否执行,导致 rust 无法确认 s 是否可访问,编译不会通过。