首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >21-Rust 教程 - 内存模型

21-Rust 教程 - 内存模型

作者头像
LarryLan
发布2026-05-14 18:13:54
发布2026-05-14 18:13:54
940
举报

内存模型

Stack 和 Heap 的爱恨情仇:你的数据到底住哪儿?

🎬 引入

你有没有想过,当你写下一行 let x = 5; 时,Rust 到底把你的 5 放在了哪里?

是随便找个地方塞进去?还是有个精心规划的"小区"?

我第一次学 Rust 时,看到"栈"和"堆"这两个词,脑子里浮现的是码头上的集装箱和建筑工地... 后来才发现,这俩确实是"存放东西的地方",但区别可大了去了。

今天咱们就来扒一扒 Rust 的内存模型,搞清楚你的数据到底住在哪儿,以及为什么 Rust 要这么设计。

📌 核心概念

栈(Stack):整齐划一的"经济适用房"

想象你去电影院看电影,座位是固定好的:先到先得,按序排列,散场时必须全部离开。这就是栈。

栈的特点:

  • 📍 后进先出(LIFO):最后进来的最先出去
  • 速度快:分配和释放就是指针移动一下
  • 📏 大小固定:编译时就得知道多大
  • 🎯 自动管理:作用域结束自动清理
代码语言:javascript
复制
fn main() {
    let x = ;      // 栈上
    let y = ;     // 栈上
    let z = x + y;  // 栈上
    // 函数结束,x、y、z 自动清理
}

堆(Heap):自由奔放的"别墅区"

堆就像是一个自由市场:你可以随便圈地,想多大就多大,但得自己记得收拾(Rust 里是所有权系统帮你记着)。

堆的特点:

  • 📍 任意位置:数据可以散落在内存各处
  • 🐌 速度较慢:需要查找可用空间
  • 📏 大小灵活:运行时动态分配
  • 🔧 需要管理:Rust 用所有权系统自动回收
代码语言:javascript
复制
fn main() {
    let s = String::from("hello");  // 字符串数据在堆上
    // s 离开作用域时,堆上的数据自动释放
}

内存布局:数据到底怎么放的?

来看个具体的例子:

代码语言:javascript
复制
fn main() {
    let x = ;                      // 栈上:直接存值 5
    let s = String::from("hello");  // 栈上存指针+长度+容量,堆上存"hello"
}

内存布局示意图:

代码语言:javascript
复制
栈(Stack)                    堆(Heap)
┌─────────────┐              
│ s (指针)    │ ───────────→ ┌─────────────┐
├─────────────┤              │ "hello"     │
│ s (长度)    │              │ (实际数据)  │
├─────────────┤              └─────────────┘
│ s (容量)    │              
├─────────────┤              
│ x = 5       │              
└─────────────┘              

看到了吗? s 在栈上只有三个信息:指针、长度、容量。真正的字符串数据在堆上!

💻 代码示例

示例 1:栈上数据(Copy 类型)

代码语言:javascript
复制
fn main() {
    let x = ;
    let y = x;  // 直接复制值,x 还能用
    
    println!("x = {}, y = {}", x, y);
    // 输出:x = 5, y = 5
}

为什么 x 还能用? 因为 5 是固定大小的整数,直接存在栈上,复制成本几乎为零。Rust 说:"这种小东西,随便复制,不用管我!"

示例 2:堆上数据(Move 语义)

代码语言:javascript
复制
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 就是无主之物,会出内存问题的!"

内存发生了什么?

代码语言:javascript
复制
移动前:
s1 → [堆上的"hello"]

移动后:
s1 → (无效)
s2 → [堆上的"hello"]

只有指针、长度、容量这三个栈上数据被复制了,堆上的实际数据没动!

示例 3:Clone 深拷贝

代码语言:javascript
复制
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();  // 深拷贝!
    
    println!("s1 = {}, s2 = {}", s1, s2);
    // 输出:s1 = hello, s2 = hello
    
    // 现在 s1 和 s2 各自有一份数据
}

内存布局:

代码语言:javascript
复制
s1 → [堆上的"hello" (副本 1)]
s2 → [堆上的"hello" (副本 2)]

clone() 和直接赋值的区别:

  • s2 = s1:移动所有权,只有一份数据
  • s2 = s1.clone():深拷贝,两份独立数据

示例 4:函数传参的内存变化

代码语言:javascript
复制
fn takes_ownership(s: String) {
    println!("函数里:{}", s);
    // s 在这里离开作用域,数据被释放
}

fn main() {
    let s = String::from("hello");
    takes_ownership(s);  // 移动所有权
    
    // println!("{}", s);  // ❌ 错误!s 已经不属于你了
}

如果你想保留所有权怎么办?

代码语言:javascript
复制
fn takes_ownership(s: String) {
    println!("函数里:{}", s);
}

fn main() {
    let s = String::from("hello");
    takes_ownership(s.clone());  // 给副本
    println!("main 里:{}", s);  // ✅ 还能用!
}

错误示例:忘记所有权转移

代码语言:javascript
复制
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    
    // 新手常见错误:以为 s1 还能用
    println!("{}", s1);  // ❌ 编译错误!
}

编译器错误信息:

代码语言:javascript
复制
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 行你还想用?想啥呢!"

🐛 常见坑点

坑点 1:混淆 Copy 和 Move

代码语言:javascript
复制
// ❌ 错误理解
let x = ;
let y = x;
// 以为 x 不能用了(其实可以,因为 i32 实现了 Copy)

let s1 = String::from("hi");
let s2 = s1;
// 以为 s1 还能用(其实不能,String 没有 Copy)

记住:

  • 基本类型(数字、布尔、字符)→ Copy,随便复制
  • 复杂类型(String、Vec、自定义结构体)→ Move,所有权转移

坑点 2:函数返回值忘记接收

代码语言:javascript
复制
fn create_string() -> String {
    let s = String::from("hello");
    s  // 返回所有权
}

fn main() {
    create_string();  // ❌ 创建了但没接收,数据直接丢弃
    
    let s = create_string();  // ✅ 正确接收
}

编译器会警告你:

代码语言:javascript
复制
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

翻译: "你好不容易创建个字符串,不用就扔了?败家啊!"

坑点 3:过度使用 clone()

代码语言:javascript
复制
// ❌ 性能差
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);  // 随便借
}

🎯 实战案例

案例 1:配置数据的内存选择

代码语言:javascript
复制
// 场景:游戏配置,有些固定,有些动态

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);
}

为什么这样设计?

  • 数字、布尔 → 栈上,快速访问
  • 字符串、向量 → 堆上,灵活可变

案例 2:大数据处理的所有权转移

代码语言:javascript
复制
// 场景:处理大型日志文件

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 所有权转移
    
    // 整个流程清晰:数据流向一目了然
}

这种设计的优势:

  • 数据所有权清晰,不会重复处理
  • 内存自动释放,不会泄漏
  • 编译器保证安全,不会用已释放的数据

🧠 思维导图

21-内存模型
21-内存模型

📝 小结

核心要点:

  1. 栈快堆灵活:栈像电影院座位(快但固定),堆像自由市场(慢但灵活)
  2. Copy vs Move:基本类型随便复制,复杂类型转移所有权
  3. String 的真相:栈上存元数据,堆上存实际数据
  4. 所有权即责任:谁拥有谁负责释放,Rust 帮你管着
  5. 借用优于克隆:能借就别抄,性能差很多

下篇预告:

既然堆上数据这么麻烦,Rust 有没有什么"智能"的方式来管理?当然有!下篇我们聊聊智能指针:Box、Rc、Arc,让你像用快递柜一样优雅地管理堆上数据!

互动问题:

你之前知道栈和堆的区别吗?有没有被 Rust 的所有权转移坑过?评论区聊聊你的"血泪史"!

🔗 参考资料

  • Rust Book - Stack and Heap
  • Rust By Example - Box
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Larry的Hub 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 内存模型
    • 🎬 引入
    • 📌 核心概念
      • 栈(Stack):整齐划一的"经济适用房"
      • 堆(Heap):自由奔放的"别墅区"
      • 内存布局:数据到底怎么放的?
    • 💻 代码示例
      • 示例 1:栈上数据(Copy 类型)
      • 示例 2:堆上数据(Move 语义)
      • 示例 3:Clone 深拷贝
      • 示例 4:函数传参的内存变化
      • 错误示例:忘记所有权转移
    • 🐛 常见坑点
      • 坑点 1:混淆 Copy 和 Move
      • 坑点 2:函数返回值忘记接收
      • 坑点 3:过度使用 clone()
    • 🎯 实战案例
      • 案例 1:配置数据的内存选择
      • 案例 2:大数据处理的所有权转移
    • 🧠 思维导图
    • 📝 小结
    • 🔗 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档