Skip to content

所有权基础

> Rust 最独特的特性,内存安全的核心保障——无需垃圾回收器

本章目标

完成本章学习后,你将能够:

  • 理解为什么 Rust 需要所有权系统
  • 掌握所有权的三大规则
  • 理解作用域与内存释放的关系
  • 区分栈内存和堆内存的使用场景

核心概念

栈内存与堆内存

在深入所有权之前,必须理解 Rust 程序如何使用内存。

为什么需要两种内存?

┌─────────────────────────────────────────────────────────────┐
│              程序运行时的内存布局                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  高地址                                                      │
│  ┌─────────────────────────────────────────────┐           │
│  │             栈(Stack)                      │           │
│  │  ┌─────────────────────────────────────┐   │           │
│  │  │  函数调用栈帧                        │ ↓ 增长      │
│  │  │  - 局部变量                          │   │           │
│  │  │  - 函数参数                          │   │           │
│  │  │  - 返回地址                          │   │           │
│  │  └─────────────────────────────────────┘   │           │
│  ├─────────────────────────────────────────────┤           │
│  │                                             │           │
│  │             ↓↓↓ 空闲内存 ↓↓↓                │           │
│  │                                             │           │
│  ├─────────────────────────────────────────────┤           │
│  │             堆(Heap)                      │           │
│  │  ┌─────────────────────────────────────┐   │           │
│  │  │  动态分配的数据                      │ ↑ 增长      │
│  │  │  - Box::new()                       │   │           │
│  │  │  - String                           │   │           │
│  │  │  - Vec<T>                           │   │           │
│  │  └─────────────────────────────────────┘   │           │
│  └─────────────────────────────────────────────┘           │
│  低地址                                                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

栈 vs 堆:详细对比

┌─────────────────────────────────────────────────────────────┐
│              栈(Stack)vs 堆(Heap)详细对比                │
├──────────────────┬────────────────────┬─────────────────────┤
│     特性         │       栈           │        堆            │
├──────────────────┼────────────────────┼─────────────────────┤
│ 分配方式         │ 编译时确定大小     │ 运行时动态分配      │
│ 分配速度         │ 极快(移动指针)   │ 较慢(搜索空闲块)  │
│ 访问方式         │ 直接访问           │ 通过指针访问        │
│ 大小限制         │ 通常较小(MB级)   │ 受系统内存限制      │
│ 生命周期         │ 自动管理(LIFO)   │ 手动/自动管理       │
│ 内存碎片         │ 无                 │ 可能产生碎片        │
│ 缓存友好性       │ 高(连续内存)     │ 较低(分散内存)    │
├──────────────────┼────────────────────┼─────────────────────┤
│ 存储内容         │ • 基本类型         │ • String            │
│                  │ • 固定大小数组     │ • Vec<T>            │
│                  │ • 指针             │ • Box<T>            │
│                  │ • 函数参数         │ • HashMap           │
│                  │ • 局部变量         │ • 动态大小数据      │
├──────────────────┼────────────────────┼─────────────────────┤
│ 示例             │ let x: i32 = 5;    │ let s = String::    │
│                  │ let arr = [1,2,3]; │     from("hello");  │
└──────────────────┴────────────────────┴─────────────────────┘

栈内存分配过程

fn main() {
    let x = 5;        // ①
    let y = 10;       // ②
    {
        let z = 15;   // ③
    }                 // ④ z 被释放
}                     // ⑤ y, x 被释放

栈内存变化过程:

① 分配 x 后:
┌─────────────┐
│ x = 5       │ ← 栈顶
└─────────────┘

② 分配 y 后:
┌─────────────┐
│ y = 10      │ ← 栈顶
├─────────────┤
│ x = 5       │
└─────────────┘

③ 分配 z 后:
┌─────────────┐
│ z = 15      │ ← 栈顶
├─────────────┤
│ y = 10      │
├─────────────┤
│ x = 5       │
└─────────────┘

④ z 离开作用域:
┌─────────────┐
│ y = 10      │ ← 栈顶
├─────────────┤
│ x = 5       │
└─────────────┘

⑤ main 结束:
┌─────────────┐
│   (空)      │ ← 栈顶
└─────────────┘

堆内存分配过程

let s = String::from("hello");

堆分配过程:

步骤 1:请求分配
┌─────────────────────────────────────────────────────┐
│  内存分配器在堆中查找足够大的空闲块                  │
│                                                     │
│  堆内存:                                            │
│  ┌──────┬──────┬──────┬──────┬──────┬────────────┐ │
│  │ 已用 │ 已用 │ 空闲 │ 空闲 │ 空闲 │    ...     │ │
│  └──────┴──────┴──────┴──────┴──────┴────────────┘ │
│                     ↑                               │
│               找到空闲块                             │
└─────────────────────────────────────────────────────┘

步骤 2:写入数据
┌─────────────────────────────────────────────────────┐
│  在找到的位置写入 "hello"                            │
│                                                     │
│  堆内存:                                            │
│  ┌──────┬──────┬──────┬──────┬──────┬────────────┐ │
│  │ 已用 │ 已用 │ 'h'  │ 'e'  │ 'l'  │ 'l'  │'o'  │ │
│  └──────┴──────┴──────┴──────┴──────┴──────┴─────┘ │
│                     ↑                               │
│               地址 0x1000(示例)                   │
└─────────────────────────────────────────────────────┘

步骤 3:栈上存储元数据
┌─────────────────────────────────────────────────────┐
│  栈上创建 String 结构体                              │
│                                                     │
│  栈:                                                │
│  ┌────────────────────────────────┐               │
│  │ s                              │               │
│  ├────────────────┬───────────────┤               │
│  │ ptr            │ 0x1000 ────────┼──┐           │
│  │ len            │ 5              │  │           │
│  │ capacity        │ 5              │  │           │
│  └────────────────┴────────────────┘  │           │
│                                        ▼           │
│  堆:                               ┌─────┐        │
│                                     │'h'  │        │
│                                     │'e'  │        │
│                                     │'l'  │        │
│                                     │'l'  │        │
│                                     │'o'  │        │
│                                     └─────┘        │
└─────────────────────────────────────────────────────┘

所有权的三大规则

为什么需要所有权?

┌─────────────────────────────────────────────────────┐
│              内存管理方式对比                        │
├─────────────────────────────────────────────────────┤
│                                                     │
│  C/C++:手动管理                                     │
│  ├── 程序员负责分配/释放                            │
│  ├── 灵活但容易出错                                 │
│  └── 常见问题:                                      │
│      • 内存泄漏:忘记释放                          │
│      • 悬垂指针:释放后继续使用                    │
│      • 双重释放:同一内存释放两次                  │
│      • 使用后释放:访问已释放的内存                │
│                                                     │
│  Java/Python/Go:垃圾回收(GC)                      │
│  ├── 运行时自动回收                                 │
│  ├── 安全但有性能开销                               │
│  └── 问题:                                          │
│      • GC 暂停:程序停止进行垃圾回收                │
│      • 不可预测延迟:GC 时机不确定                  │
│      • 内存占用高:需要预留 GC 工作内存             │
│      • 实时性差:不适合实时系统                     │
│                                                     │
│  Rust:所有权系统                                    │
│  ├── 编译时检查                                     │
│  ├── 零运行时开销                                   │
│  └── 安全且高效!                                    │
│      • 编译期保证内存安全                           │
│      • 无 GC 暂停                                    │
│      • 可预测的性能                                 │
│      • 适合系统编程和实时系统                       │
│                                                     │
└─────────────────────────────────────────────────────┘

C++ 内存安全问题示例

cpp
// C++ 内存泄漏示例
void create_leak() {
    char* buffer = new char[100];
    // 忘记 delete,内存泄漏!
    // 程序每次调用都会泄漏 100 字节
}

// C++ 悬垂指针示例
char* create_dangling() {
    char buffer[10] = "hello";
    return buffer;  // 返回局部变量的地址
    // buffer 在函数返回后被释放
    // 返回的指针指向已释放的内存
}

// C++ 双重释放示例
void double_free() {
    char* p = new char[10];
    delete[] p;
    delete[] p;  // 双重释放!程序崩溃或数据损坏
}

// C++ 使用后释放示例
void use_after_free() {
    char* p = new char[10];
    delete[] p;
    p[0] = 'a';  // 写入已释放的内存
    // 可能导致程序崩溃或数据损坏
}

Rust 的解决方案

rust
fn no_leak() {
    let buffer = String::with_capacity(100);
    // 离开作用域时自动释放!
    // 编译器在作用域结尾插入 drop 调用
}

fn main() {
    no_leak();
    // buffer 内存已正确释放
}
▶ Run

所有权的三大定律

┌─────────────────────────────────────────────────────┐
│              所有权三定律                            │
├─────────────────────────────────────────────────────┤
│                                                     │
│  第一定律:每个值都有一个所有者                      │
│           Each value has an owner                  │
│                                                     │
│  第二定律:任一时刻只能有一个所有者                  │
│           There can only be one owner at a time    │
│                                                     │
│  第三定律:当所有者离开作用域,值会被丢弃          │
│           When the owner goes out of scope,        │
│           the value will be dropped                │
│                                                     │
└─────────────────────────────────────────────────────┘

定律详解与图解

定律 1:每个值都有一个所有者

let s = String::from("hello");

┌─────────────────────────────────────────────────────┐
│  变量 s 是值 "hello" 的唯一所有者                   │
│                                                     │
│  栈:                                                │
│  ┌────────────────────────────────┐               │
│  │ s (所有者)                     │               │
│  ├────────────────┬───────────────┤               │
│  │ ptr            │ 0x1000 ────────┼──┐           │
│  │ len            │ 5              │  │           │
│  │ capacity        │ 5              │  │           │
│  └────────────────┴────────────────┘  │           │
│                                        ▼           │
│  堆:                               ┌─────┐        │
│                                     │"hello"│       │
│                                     └─────┘        │
│                                                     │
│  所有权关系:s ──拥有──▶ "hello"                   │
└─────────────────────────────────────────────────────┘
定律 2:任一时刻只能有一个所有者

let s1 = String::from("hello");
let s2 = s1;  // 所有权转移

移动前:
┌─────────────────────────────────────┐
│ 栈                    堆            │
│ ┌──────┐         ┌─────────┐      │
│ │ s1   │ ───────▶│ "hello" │      │
│ └──────┘         └─────────┘      │
└─────────────────────────────────────┘

移动后(所有权转移):
┌─────────────────────────────────────┐
│ 栈                    堆            │
│ ┌──────┐         ┌─────────┐      │
│ │ s1   │ (无效)  │ "hello" │      │
│ ├──────┤         │         │      │
│ │ s2   │ ───────▶│         │      │
│ └──────┘         └─────────┘      │
└─────────────────────────────────────┘

关键点:
• s2 现在是唯一的所有者
• s1 变成无效(编译器保证)
• 数据没有移动,只是所有权转移
定律 3:所有者离开作用域,值被丢弃

{
    let s = String::from("hello");
    // s 从这里开始有效
}   // s 离开作用域
    // Rust 自动调用 drop(s)
    // 堆内存被释放

作用域内:
┌─────────────────────────────────────┐
│ 作用域开始 {                         │
│     let s = String::from("hello");  │
│     // s 有效                        │
│     println!("{}", s);              │
│ } ← 作用域结束                       │
│   ↓                                  │
│   Rust 调用:drop(s)                 │
│   堆内存被释放                       │
└─────────────────────────────────────┘

作用域结束后:
┌─────────────────────────────────────┐
│ 栈:                                │
│   (s 已被释放)                      │
│                                     │
│ 堆:                                │
│   ("hello" 已被释放)                │
└─────────────────────────────────────┘

规则验证代码

rust
fn main() {
    println!("=== 定律 1:每个值都有一个所有者 ===");
    
    // s 是 "hello" 的唯一所有者
    let s = String::from("hello");
    println!("所有者 s: {}", s);
    
    println!("\n=== 定律 2:任一时刻只能有一个所有者 ===");
    
    let s1 = String::from("world");
    let s2 = s1;  // 所有权从 s1 移动到 s2
    
    // println!("s1: {}", s1);  // ❌ 错误:s1 已无效
    println!("s2: {}", s2);      // ✅ 正确:s2 是所有者
    
    println!("\n=== 定律 3:作用域结束自动释放 ===");
    
    {
        let inner = String::from("inner scope");
        println!("inner: {}", inner);
    }  // inner 在这里被释放
    
    // println!("inner: {}", inner);  // ❌ 错误:inner 不存在
    println!("inner 已离开作用域");
}
▶ Run

规则记忆口诀

一值一主,时刻唯一
主离作用域,值即丢弃

实战案例

案例 1:内存泄漏对比

问题描述

演示 Rust 如何防止内存泄漏,对比 C++ 的手动管理。

C++ 版本(内存泄漏风险)

cpp
#include <iostream>
#include <cstring>

// C++ 容易发生内存泄漏
void process_data() {
    char* buffer = new char[1000];
    
    // 如果这里抛出异常
    if (true) {  // 模拟异常条件
        throw std::runtime_error("Error occurred");
    }
    
    // 这行永远不会执行
    delete[] buffer;
}

int main() {
    try {
        process_data();
    } catch (const std::exception& e) {
        std::cout << "Exception: " << e.what() << std::endl;
        // buffer 内存泄漏!
    }
    return 0;
}

Rust 版本(自动清理)

rust
fn process_data() -> Result<(), String> {
    let buffer = String::with_capacity(1000);
    
    // 即使这里返回错误
    if true {  // 模拟错误条件
        return Err("Error occurred".to_string());
    }
    
    // 这行永远不会执行
    // 但 buffer 会自动清理
    Ok(())
}

fn main() {
    match process_data() {
        Ok(()) => println!("Success"),
        Err(e) => println!("Error: {}", e),
    }
    // buffer 内存已正确释放,无泄漏!
}
▶ Run

运行结果

Error: Error occurred

关键点:Rust 的 RAII(Resource Acquisition Is Initialization)保证即使发生错误,资源也会自动清理。


案例 2:嵌套作用域

问题描述

理解变量的生命周期与作用域的关系。

代码实现

rust
fn main() {
    println!("=== 外层作用域开始 ===");
    
    let outer = String::from("外层");
    println!("创建 outer: {}", outer);
    
    {
        println!("\n--- 内层作用域开始 ---");
        
        let inner = String::from("内层");
        println!("创建 inner: {}", inner);
        
        // 可以访问外层变量
        println!("内层访问 outer: {}", outer);
        println!("内层访问 inner: {}", inner);
        
        println!("--- 内层作用域结束 ---");
    }  // inner 在这里被释放
    
    println!("\n=== 回到外层作用域 ===");
    println!("outer 仍然有效: {}", outer);
    
    // println!("inner: {}", inner);  // ❌ 错误:inner 不存在
}
▶ Run

运行结果

=== 外层作用域开始 ===
创建 outer: 外层

--- 内层作用域开始 ---
创建 inner: 内层
内层访问 outer: 外层
内层访问 inner: 内层
--- 内层作用域结束 ---

=== 回到外层作用域 ===
outer 仍然有效: 外层

内存布局变化

外层作用域:
┌─────────────────┐
│ outer: "外层"    │
└─────────────────┘

内层作用域:
┌─────────────────┐
│ inner: "内层"   │ ← 栈顶
├─────────────────┤
│ outer: "外层"   │
└─────────────────┘

内层作用域结束:
┌─────────────────┐
│ (inner 已释放)  │
├─────────────────┤
│ outer: "外层"   │
└─────────────────┘

案例 3:释放顺序验证

问题描述

验证 Rust 中变量的释放顺序(后进先出 LIFO)。

代码实现

rust
struct DropTracker {
    name: String,
}

impl Drop for DropTracker {
    fn drop(&mut self) {
        println!("🗑️  释放: {}", self.name);
    }
}

fn main() {
    println!("=== 创建变量 ===");
    
    let _a = DropTracker { name: "a".to_string() };
    let _b = DropTracker { name: "b".to_string() };
    let _c = DropTracker { name: "c".to_string() };
    
    {
        println!("\n--- 内层作用域 ---");
        let _d = DropTracker { name: "d".to_string() };
        let _e = DropTracker { name: "e".to_string() };
        println!("内层作用域结束");
    }  // e 先释放,然后 d
    
    println!("\n=== 主函数结束 ===");
}  // c 先释放,然后 b,最后 a
▶ Run

运行结果

=== 创建变量 ===

--- 内层作用域 ---
内层作用域结束
🗑️  释放: e
🗑️  释放: d

=== 主函数结束 ===
🗑️  释放: c
🗑️  释放: b
🗑️  释放: a

关键点:变量按照创建的相反顺序释放(LIFO)。


常见错误

错误 1:使用已移动的值

错误描述

当一个值被移动后,原变量不能再使用。

错误代码

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1 的所有权移动到 s2
    
    println!("{}", s1);  // ❌ 错误:使用了已移动的值
}
▶ Run

编译错误信息

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:20
  |
3 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
4 |     let s2 = s1;
  |              -- value moved here
5 |     println!("{}", s1);
  |                    ^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value before moving it
  |
4 |     let s2 = s1.clone();
  |              +++++++++++

修复方法

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();  // 显式克隆
    
    println!("s1 = {}", s1);  // ✅ 正确
    println!("s2 = {}", s2);  // ✅ 正确
}
▶ Run

错误 2:变量在作用域外使用

错误描述

变量只在其定义的作用域内有效。

错误代码

rust
fn main() {
    {
        let s = String::from("hello");
        println!("{}", s);
    }  // s 在这里被释放
    
    println!("{}", s);  // ❌ 错误:s 不在作用域内
}
▶ Run

编译错误信息

error[E0425]: cannot find value `s` in this scope
 --> src/main.rs:7:20
  |
7 |     println!("{}", s);
  |                    ^ not found in this scope

修复方法

rust
fn main() {
    let s = String::from("hello");  // 提升到外层作用域
    
    {
        println!("内层: {}", s);
    }
    
    println!("外层: {}", s);  // ✅ 正确
}
▶ Run

错误 3:部分移动

错误描述

数组或元组的部分移动会导致整个变量失效。

错误代码

rust
fn main() {
    let tuple = (String::from("hello"), 42);
    let s = tuple.0;  // 移动第一个元素
    
    // println!("{:?}", tuple);  // ❌ 错误:tuple 部分移动
    println!("{}", tuple.1);  // ✅ 正确:第二个元素是 Copy 类型
}
▶ Run

编译错误信息

error[E0382]: use of partially moved value: `tuple`
 --> src/main.rs:6:20
  |
4 |     let s = tuple.0;
  |             ------- value partially moved here
5 |     
6 |     println!("{:?}", tuple);
  |                    ^^^^^ value borrowed here after partial move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value before moving it
  |
4 |     let s = tuple.0.clone();
  |                    ++++++++++++

修复方法

rust
fn main() {
    let tuple = (String::from("hello"), 42);
    let s = tuple.0.clone();  // 克隆而不是移动
    
    println!("s = {}", s);
    println!("tuple = {:?}", tuple);  // ✅ 正确
}
▶ Run

性能考虑

栈 vs 堆的性能差异

┌─────────────────────────────────────────────────────────┐
│              栈 vs 堆性能对比                            │
├──────────────────┬────────────────┬─────────────────────┤
│     操作         │   栈           │      堆            │
├──────────────────┼────────────────┼─────────────────────┤
│ 分配速度         │ ~1 纳秒        │ ~100 纳秒          │
│ 访问速度         │ 直接访问       │ 指针间接访问       │
│ 缓存命中率       │ 高             │ 较低               │
│ 内存碎片         │ 无             │ 可能产生           │
│ 分配次数         │ 无需分配       │ 需要分配器介入     │
├──────────────────┴────────────────┴─────────────────────┤
│                                                         │
│ 性能建议:                                               │
│ • 优先使用栈分配的类型(Copy 类型)                     │
│ • 小对象优先使用栈                                      │
│ • 大对象或动态大小使用堆                                │
│ • 减少堆分配次数(预分配、对象池)                      │
└─────────────────────────────────────────────────────────┘

零成本抽象的含义

┌─────────────────────────────────────────────────────┐
│              零成本抽象                             │
├─────────────────────────────────────────────────────┤
│                                                     │
│  什么是零成本抽象?                                  │
│  • 抽象不引入运行时开销                              │
│  • 编译时代价转化为零运行时代价                      │
│  • 高级特性编译后等同于手写底层代码                  │
│                                                     │
│  Rust 中的零成本抽象:                              │
│  • 所有权检查:编译时完成,无运行时开销              │
│  • 泛型:单态化,生成特化代码                       │
│  • 迭代器:内联优化,等同于手写循环                 │
│  • 智能指针:编译时优化,无额外开销                 │
│                                                     │
│  示例:迭代器 vs 手写循环                           │
│                                                     │
│  let sum: i32 = (1..=100).sum();                   │
│  // 编译后等同于:                                   │
│  let mut sum = 0;                                   │
│  for i in 1..=100 { sum += i; }                    │
│  // 无额外开销!                                     │
│                                                     │
└─────────────────────────────────────────────────────┘

性能提示

  • 优先使用栈分配:基本类型、小数组、小元组优先使用栈分配
  • 预分配容量:对于 Vec、String 等集合,预分配容量减少重新分配
  • 避免不必要的克隆:使用引用代替克隆,或使用 Cow 类型
  • 减少堆分配次数:使用对象池、arena 分配器等优化频繁分配

最佳实践

✅ 推荐做法

  • 理解所有权转移:明确知道每次赋值是否发生所有权转移
  • 优先使用引用:不需要所有权时,使用引用 &T
  • 合理使用 clone:需要独立副本时显式调用 clone()
  • 遵循 RAII:利用作用域自动管理资源

❌ 避免做法

  • 不要过度克隆:大量克隆会严重影响性能
  • 不要忽略所有权:理解所有权转移,避免编译错误
  • 不要手动管理内存:让 Rust 自动处理,不要使用 unsafe

扩展阅读

官方文档

深入理解


小结

本章核心知识点

概念关键字/语法核心要点
栈内存自动管理LIFO,快速分配,大小固定
堆内存Box, String, Vec动态分配,运行时大小
所有权三大规则let, move一值一主,时刻唯一,自动释放
作用域{}定义变量生命周期

学习检查清单

  • [ ] 理解栈和堆的区别
  • [ ] 掌握所有权三大规则
  • [ ] 理解作用域与内存释放的关系
  • [ ] 能够识别所有权转移的场景
  • [ ] 理解 Rust 的内存安全保证

下一章

下一章将深入学习 String 类型和移动语义,理解所有权在具体类型中的表现。

➡️ String 与移动