
Stack 和 Heap 的爱恨情仇:你的数据到底住哪儿?
你有没有想过,当你写下一行 let x = 5; 时,Rust 到底把你的 5 放在了哪里?
是随便找个地方塞进去?还是有个精心规划的"小区"?
我第一次学 Rust 时,看到"栈"和"堆"这两个词,脑子里浮现的是码头上的集装箱和建筑工地... 后来才发现,这俩确实是"存放东西的地方",但区别可大了去了。
今天咱们就来扒一扒 Rust 的内存模型,搞清楚你的数据到底住在哪儿,以及为什么 Rust 要这么设计。
想象你去电影院看电影,座位是固定好的:先到先得,按序排列,散场时必须全部离开。这就是栈。
栈的特点:
fn main() {
let x = ; // 栈上
let y = ; // 栈上
let z = x + y; // 栈上
// 函数结束,x、y、z 自动清理
}
堆就像是一个自由市场:你可以随便圈地,想多大就多大,但得自己记得收拾(Rust 里是所有权系统帮你记着)。
堆的特点:
fn main() {
let s = String::from("hello"); // 字符串数据在堆上
// s 离开作用域时,堆上的数据自动释放
}
来看个具体的例子:
fn main() {
let x = ; // 栈上:直接存值 5
let s = String::from("hello"); // 栈上存指针+长度+容量,堆上存"hello"
}
内存布局示意图:
栈(Stack) 堆(Heap)
┌─────────────┐
│ s (指针) │ ───────────→ ┌─────────────┐
├─────────────┤ │ "hello" │
│ s (长度) │ │ (实际数据) │
├─────────────┤ └─────────────┘
│ s (容量) │
├─────────────┤
│ x = 5 │
└─────────────┘
看到了吗? s 在栈上只有三个信息:指针、长度、容量。真正的字符串数据在堆上!
fn main() {
let x = ;
let y = x; // 直接复制值,x 还能用
println!("x = {}, y = {}", x, y);
// 输出:x = 5, y = 5
}
为什么 x 还能用? 因为 5 是固定大小的整数,直接存在栈上,复制成本几乎为零。Rust 说:"这种小东西,随便复制,不用管我!"
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 移动!s1 不能用了
// println!("{}", s1); // ❌ 编译错误!
// error[E0382]: borrow of moved value: `s1`
println!("{}", s2); // ✅ 输出:hello
}
编译器在吼什么? "s1 的所有权已经给 s2 了!你现在用 s1 就是无主之物,会出内存问题的!"
内存发生了什么?
移动前:
s1 → [堆上的"hello"]
移动后:
s1 → (无效)
s2 → [堆上的"hello"]
只有指针、长度、容量这三个栈上数据被复制了,堆上的实际数据没动!
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝!
println!("s1 = {}, s2 = {}", s1, s2);
// 输出:s1 = hello, s2 = hello
// 现在 s1 和 s2 各自有一份数据
}
内存布局:
s1 → [堆上的"hello" (副本 1)]
s2 → [堆上的"hello" (副本 2)]
clone() 和直接赋值的区别:
s2 = s1:移动所有权,只有一份数据s2 = s1.clone():深拷贝,两份独立数据fn takes_ownership(s: String) {
println!("函数里:{}", s);
// s 在这里离开作用域,数据被释放
}
fn main() {
let s = String::from("hello");
takes_ownership(s); // 移动所有权
// println!("{}", s); // ❌ 错误!s 已经不属于你了
}
如果你想保留所有权怎么办?
fn takes_ownership(s: String) {
println!("函数里:{}", s);
}
fn main() {
let s = String::from("hello");
takes_ownership(s.clone()); // 给副本
println!("main 里:{}", s); // ✅ 还能用!
}
fn main() {
let s1 = String::from("hello");
let s2 = s1;
// 新手常见错误:以为 s1 还能用
println!("{}", s1); // ❌ 编译错误!
}
编译器错误信息:
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:20
|
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}", s1);
| ^^ value borrowed here after move
翻译成人话: "第 3 行 s1 已经移给 s2 了,第 5 行你还想用?想啥呢!"
// ❌ 错误理解
let x = ;
let y = x;
// 以为 x 不能用了(其实可以,因为 i32 实现了 Copy)
let s1 = String::from("hi");
let s2 = s1;
// 以为 s1 还能用(其实不能,String 没有 Copy)
记住:
fn create_string() -> String {
let s = String::from("hello");
s // 返回所有权
}
fn main() {
create_string(); // ❌ 创建了但没接收,数据直接丢弃
let s = create_string(); // ✅ 正确接收
}
编译器会警告你:
warning: unused return value of `create_string` that must be used
--> src/main.rs:7:5
|
7 | create_string();
| ^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
翻译: "你好不容易创建个字符串,不用就扔了?败家啊!"
// ❌ 性能差
fn process_data(s: String) {
// 处理 s
}
fn main() {
let s = String::from("large data...");
process_data(s.clone()); // 每次都深拷贝
process_data(s.clone()); // 又拷贝一次
process_data(s.clone()); // 还来?
}
// ✅ 用借用
fn process_data(s: &str) {
// 处理 s
}
fn main() {
let s = String::from("large data...");
process_data(&s); // 只是借用
process_data(&s); // 再借一次
process_data(&s); // 随便借
}
// 场景:游戏配置,有些固定,有些动态
struct GameConfig {
// 栈上:固定大小,编译时已知
screen_width: u32,
screen_height: u32,
fullscreen: bool,
// 堆上:运行时才能确定
player_name: String,
key_bindings: Vec<String>,
}
fn main() {
let config = GameConfig {
screen_width: ,
screen_height: ,
fullscreen: true,
player_name: String::from("Larry"),
key_bindings: vec![
String::from("W - 前进"),
String::from("S - 后退"),
String::from("A - 左转"),
String::from("D - 右转"),
],
};
println!("玩家 {} 的配置已加载", config.player_name);
}
为什么这样设计?
// 场景:处理大型日志文件
fn read_log_file(path: &str) -> String {
// 假装读了个大文件
String::from("大量的日志数据...")
}
fn analyze_logs(logs: String) -> Vec<String> {
// 分析日志,返回结果
// logs 在这里被消耗(可以提取数据)
vec![String::from("发现 3 个错误"), String::from("警告 15 次")]
}
fn save_report(report: Vec<String>) {
// 保存报告
for item in report {
println!("保存:{}", item);
}
// report 在这里释放
}
fn main() {
let logs = read_log_file("app.log");
let report = analyze_logs(logs); // logs 所有权转移
save_report(report); // report 所有权转移
// 整个流程清晰:数据流向一目了然
}
这种设计的优势:

核心要点:
下篇预告:
既然堆上数据这么麻烦,Rust 有没有什么"智能"的方式来管理?当然有!下篇我们聊聊智能指针:Box、Rc、Arc,让你像用快递柜一样优雅地管理堆上数据!
互动问题:
你之前知道栈和堆的区别吗?有没有被 Rust 的所有权转移坑过?评论区聊聊你的"血泪史"!