所有权与内存模型

本章目标: 深入理解 Rust 所有权系统和内存布局,掌握 Move/Copy 语义

学习时间: 5-7 天

前置知识: 完成 01-从Go到Rust-思维转换.md


所有权: Rust 的核心创新

所有权系统是 Rust 区别于所有其他语言的核心特性。理解它,你就理解了 Rust。

三条所有权规则

规则 1: Rust 中每个值都有一个变量作为它的所有者
规则 2: 每个值在任一时刻只能有一个所有者
规则 3: 当所有者离开作用域,值会被立即释放(drop)

这三条规则看似简单,却是整个内存安全保证的基础。


1. 内存布局: 栈与堆

栈内存 (Stack)

fn stack_example() {
    let x: i32 = 42;    // 栈上分配, 4 字节
    let y: bool = true;  // 栈上分配, 1 字节
    let z: f64 = 3.14;  // 栈上分配, 8 字节
    
    // 函数返回时,x/y/z 立即释放
}  // ← 栈指针回退, 内存瞬间回收

栈的特点:

  • 分配和释放极快(移动栈指针)
  • 大小必须在编译期已知
  • 后进先出(LIFO)
  • 线程专属,无同步开销

堆内存 (Heap)

fn heap_example() {
    let s = String::from("hello");  // 堆上分配
    let v = vec![1, 2, 3];         // 堆上分配
    
    // s 和 v 在栈上各有一个"胖指针"
    // 指向堆上的实际数据
}  // ← s 和 v 离开作用域, drop 被调用, 堆内存释放

堆的特点:

  • 大小可以在运行时确定
  • 分配/释放较慢(需要内存分配器)
  • 通过指针访问

Rust 内存布局图解

栈 (Stack)                堆 (Heap)
┌──────────────────┐      ┌─────────────────┐
│ s: String        │      │ h e l l o       │
│  ┌─────────────┐ │      │ (5 bytes)       │
│  │ ptr ────────┼─┼─────►│                 │
│  │ len: 5      │ │      │                 │
│  │ cap: 5      │ │      │                 │
│  └─────────────┘ │      └─────────────────┘
│                  │      
│ x: i32 = 42     │      (整数直接在栈上)
└──────────────────┘

哪些类型分配在栈上?

// 栈分配类型
let a: i32 = 42;         // 整数
let b: f64 = 3.14;       // 浮点数
let c: bool = true;      // 布尔值
let d: char = 'A';       // 字符
let e: (i32, bool) = (1, true);  // 元组(如果元素都是栈类型)
let f: [i32; 3] = [1, 2, 3];    // 固定大小数组
 
// 堆分配类型
let s = String::from("hello");   // 字符串
let v = vec![1, 2, 3];          // 动态数组
let b = Box::new(42);           // Box 包装

2. 所有权与 Move 语义

Move 的本质

fn main() {
    let s1 = String::from("hello");
    
    // 内存状态:
    // s1: ptr → "hello" (堆上)
    
    let s2 = s1;  // Move: s1 的所有权转移到 s2
    
    // 内存状态:
    // s1: 无效 (编译器记录)
    // s2: ptr → "hello" (堆上)
    
    // println!("{}", s1);  // 编译错误!
    println!("{}", s2);     // 正常
}

Move 发生的场景:

  1. 赋值: let s2 = s1;
  2. 函数参数: take_ownership(s1);
  3. 函数返回: return s1;
  4. 集合插入: vec.push(s1);

为什么需要 Move?

考虑如果没有 Move 会发生什么:

// 假设赋值是"浅拷贝" (Rust 不这样做)
fn hypothetical_shallow_copy() {
    let s1 = String::from("hello");
    let s2 = s1;  // 假设只复制指针
    
    // 此时 s1 和 s2 的 ptr 指向同一个堆内存!
    
    // 作用域结束时:
    // s2 被 drop, 释放堆内存
    // s1 被 drop, 再次释放同一堆内存 ← 双重释放 bug!
}

Move 语义通过转移所有权,确保每块内存只有一个所有者,从根本上避免了双重释放。

函数与所有权

fn takes_ownership(some_string: String) {
    // some_string 进入作用域
    println!("{}", some_string);
}  // some_string 离开作用域, drop 被调用
 
fn makes_copy(some_integer: i32) {
    // some_integer 进入作用域
    println!("{}", some_integer);
}  // some_integer 离开作用域,但 i32 是 Copy 的,无 drop
 
fn gives_ownership() -> String {
    let some_string = String::from("hello");
    some_string  // 移动到调用方
}
 
fn takes_and_gives_back(a_string: String) -> String {
    a_string  // 接收并原样返回
}
 
fn main() {
    let s1 = gives_ownership();         // 接收所有权
    let s2 = String::from("world");
    let s3 = takes_and_gives_back(s2);  // s2 moved, s3 得到所有权
    
    takes_ownership(s1);  // s1 的所有权进入函数
    // println!("{}", s1);  // 错误!
    
    let x = 5;
    makes_copy(x);  // i32 是 Copy 类型
    println!("{}", x);  // 正常!
}

3. Copy 语义

Copy Trait

有些类型实现了 Copy trait。复制这些类型时,原变量仍然有效:

fn main() {
    let x = 5;
    let y = x;  // Copy: 在栈上复制了一份
    
    // x 和 y 都可以使用
    println!("x = {}, y = {}", x, y);
}

哪些类型实现了 Copy?

// 实现 Copy 的类型:
let a: i8 = 1;       // 所有整数类型 i8/i16/i32/i64/i128/u8...
let b: f32 = 1.0;    // 浮点类型 f32/f64
let c: bool = true;  // bool
let d: char = 'a';   // char
let e: () = ();      // 单元类型
 
// 如果元素实现了 Copy, 固定大小数组和元组也是 Copy
let f: [i32; 3] = [1, 2, 3];  // Copy
let g: (i32, bool) = (1, true);  // Copy
 
// 没有实现 Copy 的类型:
// String, Vec<T>, Box<T>, 以及所有包含堆分配的类型

自定义 Copy 类型

// 使用 derive 宏实现 Copy (需要所有字段都实现 Copy)
#[derive(Clone, Copy, Debug)]
struct Point {
    x: f64,
    y: f64,
}
 
fn main() {
    let p1 = Point { x: 1.0, y: 2.0 };
    let p2 = p1;  // Copy, 不是 Move
    
    println!("p1 = {:?}, p2 = {:?}", p1, p2);  // 两者都可用
}

注意: 实现 Copy 的类型必须也实现 Clone,且不能包含任何需要 Drop 的字段。


4. Drop Trait 与 RAII

RAII 模式

RAII (Resource Acquisition Is Initialization) 是 Rust 内存管理的核心模式:

fn raii_demo() {
    let f = File::open("hello.txt").unwrap();  // 资源获取
    // 使用 f...
    // 作用域结束时, f 自动 drop, 文件句柄自动关闭
}  // ← Drop 在此处自动调用

实现 Drop Trait

struct Database {
    name: String,
    connection_id: u32,
}
 
impl Drop for Database {
    fn drop(&mut self) {
        println!("关闭数据库连接 {} (id: {})", 
                 self.name, self.connection_id);
        // 实际项目中这里会清理资源
    }
}
 
fn main() {
    let db1 = Database { 
        name: String::from("users"), 
        connection_id: 1 
    };
    
    {
        let db2 = Database { 
            name: String::from("orders"), 
            connection_id: 2 
        };
        println!("使用 db2");
    }  // ← db2 在此 drop
    
    println!("db1 还在使用");
}  // ← db1 在此 drop

输出:

使用 db2
关闭数据库连接 orders (id: 2)
db1 还在使用
关闭数据库连接 users (id: 1)

Drop 顺序

struct Foo(i32);
 
impl Drop for Foo {
    fn drop(&mut self) {
        println!("Dropping Foo({})", self.0);
    }
}
 
fn main() {
    let _x = Foo(1);
    let _y = Foo(2);
    let _z = Foo(3);
}
// 输出 (逆序 drop):
// Dropping Foo(3)
// Dropping Foo(2)  
// Dropping Foo(1)

规则: 变量按声明顺序的逆序 drop。结构体字段按声明顺序 drop。

提前 drop

fn main() {
    let mut db = connect_to_database();
    {
        // 使用 db...
    }
    // 此时 db 还未 drop!
    
    drop(db);  // 手动提前 drop
    
    // 现在可以做其他独占操作了
    do_exclusive_operation();
}

5. 所有权与函数: 深入分析

高效传递所有权

// 低效: 不必要地移动然后返回
fn bad_append(mut s: String) -> String {
    s.push_str(" world");
    s  // 必须返回才能继续使用
}
 
// 高效: 使用借用
fn good_append(s: &mut String) {
    s.push_str(" world");
    // 不需要返回
}
 
// 最惯用: 参数和返回值都是值类型
fn transform(s: String) -> String {
    format!("{} world", s)
}

所有权与集合

fn ownership_with_collections() {
    let mut v: Vec<String> = Vec::new();
    
    let s1 = String::from("hello");
    v.push(s1);  // s1 moved 进 v
    // println!("{}", s1);  // 错误!
    
    // 从集合中取出元素
    let s2 = v.remove(0);  // 从 v 中 move 出来
    println!("{}", s2);    // 正常
    
    // 迭代消费集合
    let v2 = vec![
        String::from("a"),
        String::from("b"),
        String::from("c"),
    ];
    
    for s in v2 {  // v2 被消费
        println!("{}", s);
    }
    // v2 不再可用
}

6. 内存安全保证

Rust 防止的内存错误

// 1. 使用后释放 (Use After Free) - Rust 通过所有权防止
fn use_after_free_prevented() {
    let x = Box::new(42);
    let y = x;  // x moved
    // println!("{}", *x);  // 编译错误! 而非运行时崩溃
    println!("{}", *y);
}
 
// 2. 双重释放 (Double Free) - 单一所有者保证不会双重释放
fn double_free_prevented() {
    let s = String::from("test");
    let t = s;  // 所有权转移
    // s 会被 drop 吗? 不会! 编译器知道 s 已移动
}
 
// 3. 内存泄漏 (Memory Leak) - RAII 自动释放
fn no_memory_leak() {
    let s = String::from("will be freed");
    // 即使 panic 发生, drop 也会被调用 (unwinding)
}  // s 在此自动释放
 
// 4. 悬垂指针 (Dangling Pointer) - 生命周期系统防止
fn dangling_pointer_prevented() -> &str {
    let s = String::from("hello");
    // &s  // 编译错误! s 的生命周期在函数结束时结束
    "static string"  // 这是安全的
}

7. 所有权与性能

零成本所有权

// Rust 的所有权转移在运行时只是移动几个字节(指针)
fn move_is_zero_cost(data: Vec<u8>) -> Vec<u8> {
    // 移动 Vec (3 个 usize: ptr, len, cap)
    // 不复制任何实际数据!
    data
}
 
// 对比 Go 的 GC:
// - Go 需要 GC 扫描和暂停
// - Rust 在编译期确定释放点, 零运行时开销

栈 vs 堆分配

// 栈分配: 快速
fn stack_alloc() {
    for _ in 0..1_000_000 {
        let x: [i32; 100] = [0; 100];  // 栈上, 不需要分配器
        let _ = x;
    }
}
 
// 堆分配: 较慢
fn heap_alloc() {
    for _ in 0..1_000_000 {
        let v: Vec<i32> = vec![0; 100];  // 堆上, 需要 malloc/free
        let _ = v;
    }
}

8. 实战: 实现一个简单的内存池

理解所有权后,我们来实现一个简单的内存池:

/// 简单的连接池示例
pub struct ConnectionPool {
    available: Vec<Connection>,
    max_size: usize,
}
 
pub struct Connection {
    id: u32,
    query_count: u32,
}
 
impl Connection {
    fn new(id: u32) -> Self {
        println!("创建连接 {}", id);
        Connection { id, query_count: 0 }
    }
    
    pub fn query(&mut self, sql: &str) -> String {
        self.query_count += 1;
        format!("连接 {} 执行查询 {}: {}", self.id, self.query_count, sql)
    }
}
 
impl Drop for Connection {
    fn drop(&mut self) {
        println!("关闭连接 {} (执行了 {} 次查询)", 
                 self.id, self.query_count);
    }
}
 
impl ConnectionPool {
    pub fn new(size: usize) -> Self {
        let available = (0..size)
            .map(|i| Connection::new(i as u32))
            .collect();
        ConnectionPool { available, max_size: size }
    }
    
    pub fn get(&mut self) -> Option<Connection> {
        self.available.pop()
    }
    
    pub fn put(&mut self, conn: Connection) {
        if self.available.len() < self.max_size {
            self.available.push(conn);
        }
        // 如果池已满, conn 在此 drop (连接关闭)
    }
}
 
fn main() {
    let mut pool = ConnectionPool::new(3);
    
    // 取出一个连接
    if let Some(mut conn) = pool.get() {
        println!("{}", conn.query("SELECT * FROM users"));
        pool.put(conn);  // 归还连接
    }
    
    // 用完连接不归还
    if let Some(mut conn) = pool.get() {
        println!("{}", conn.query("SELECT * FROM orders"));
        // conn 在这里 drop (超出作用域)
    }
    
    println!("程序结束");
}
// 所有剩余连接在 pool drop 时关闭

练习题

练习 1 (基础): 追踪所有权

给以下代码注释 “move” 或 “copy”:

fn main() {
    let x = 42;        
    let y = x;         // ___?
    
    let s1 = String::from("hi");
    let s2 = s1;       // ___?
    
    let p = (1i32, 2i32);
    let q = p;         // ___?
    
    let v1 = vec![1,2,3];
    let v2 = v1;       // ___?
}

答案:

y = x       → copy (i32 实现 Copy)
s2 = s1     → move (String 不实现 Copy)
q = p       → copy (元组内都是 i32, 实现 Copy)
v2 = v1     → move (Vec 不实现 Copy)

练习 2 (基础): 修复所有权错误

fn main() {
    let s = String::from("hello");
    let r = first_word(&s);
    s.clear();  // 问题在哪?
    println!("{}", r);
}
 
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();
    for (i, &b) in bytes.iter().enumerate() {
        if b == b' ' {
            return &s[..i];
        }
    }
    &s[..]
}

答案: s.clear() 会修改 s,但 r 持有对 s 的不可变借用。解决方法:

fn main() {
    let s = String::from("hello world");
    let r = first_word(&s);
    println!("{}", r);  // 先使用 r
    // r 的生命周期在这里结束
    // 注意: 如果需要 clear(), 用 s.to_string() 先把 r 变成 String
    // let r = first_word(&s).to_string();
    // s.clear();
    // println!("{}", r);
}

练习 3 (中等): 实现 Drop

实现一个 TempFile 类型,在 drop 时自动删除文件:

use std::fs;
use std::path::PathBuf;
 
struct TempFile {
    path: PathBuf,
}
 
impl TempFile {
    fn new(path: &str) -> Self {
        fs::write(path, "temp content").unwrap();
        TempFile { path: PathBuf::from(path) }
    }
    
    fn path(&self) -> &str {
        self.path.to_str().unwrap()
    }
}
 
impl Drop for TempFile {
    fn drop(&mut self) {
        if self.path.exists() {
            fs::remove_file(&self.path).unwrap_or_default();
            println!("已删除临时文件: {:?}", self.path);
        }
    }
}
 
fn main() {
    {
        let tmp = TempFile::new("/tmp/test_temp_file.txt");
        println!("创建了: {}", tmp.path());
        // 使用文件...
    }  // tmp 在这里 drop, 文件被删除
    
    println!("文件应该已被删除");
}

练习 4 (中等): 所有权与集合

// 修复这段代码使其能输出所有字符串
fn main() {
    let strings = vec![
        String::from("alpha"),
        String::from("beta"),
        String::from("gamma"),
    ];
    
    for s in strings {
        println!("{}", s);
    }
    
    // 问题: strings 已被消费, 这里还能使用吗?
    println!("总数: {}", strings.len()); // 如何修复?
}

三种修复方法:

// 方法 1: 先记长度
let len = strings.len();
for s in strings {
    println!("{}", s);
}
println!("总数: {}", len);
 
// 方法 2: 借用迭代
for s in &strings {
    println!("{}", s);
}
println!("总数: {}", strings.len());
 
// 方法 3: 克隆
let strings2 = strings.clone();
for s in strings {
    println!("{}", s);
}
println!("总数: {}", strings2.len());

练习 5 (中等): 函数所有权转移

实现一个函数 most_common_char,找出字符串中出现次数最多的字符。注意所有权的正确转移:

use std::collections::HashMap;
 
fn most_common_char(s: String) -> (char, usize) {
    let mut counts: HashMap<char, usize> = HashMap::new();
    
    for c in s.chars() {
        *counts.entry(c).or_insert(0) += 1;
    }
    
    counts
        .into_iter()
        .max_by_key(|(_, count)| *count)
        .unwrap_or(('\0', 0))
}
 
fn main() {
    let text = String::from("hello world");
    let (c, count) = most_common_char(text);
    println!("最常见字符: '{}' 出现 {} 次", c, count);
}

练习 6 (挑战): 实现 RAII 锁

实现一个简单的互斥锁,使用 RAII 确保锁总被释放:

use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicBool, Ordering};
 
pub struct SimpleMutex<T> {
    locked: AtomicBool,
    data: UnsafeCell<T>,
}
 
pub struct MutexGuard<'a, T> {
    mutex: &'a SimpleMutex<T>,
}
 
unsafe impl<T: Send> Send for SimpleMutex<T> {}
unsafe impl<T: Send> Sync for SimpleMutex<T> {}
 
impl<T> SimpleMutex<T> {
    pub fn new(data: T) -> Self {
        SimpleMutex {
            locked: AtomicBool::new(false),
            data: UnsafeCell::new(data),
        }
    }
    
    pub fn lock(&self) -> MutexGuard<'_, T> {
        // 自旋等待
        while self.locked.compare_exchange(
            false, true, 
            Ordering::Acquire, Ordering::Relaxed
        ).is_err() {
            std::hint::spin_loop();
        }
        MutexGuard { mutex: self }
    }
}
 
impl<T> std::ops::Deref for MutexGuard<'_, T> {
    type Target = T;
    fn deref(&self) -> &T {
        unsafe { &*self.mutex.data.get() }
    }
}
 
impl<T> std::ops::DerefMut for MutexGuard<'_, T> {
    fn deref_mut(&mut self) -> &mut T {
        unsafe { &mut *self.mutex.data.get() }
    }
}
 
impl<T> Drop for MutexGuard<'_, T> {
    fn drop(&mut self) {
        self.mutex.locked.store(false, Ordering::Release);
        println!("锁已释放");
    }
}
 
fn main() {
    let mutex = SimpleMutex::new(0i32);
    
    {
        let mut guard = mutex.lock();
        println!("获得锁, 值 = {}", *guard);
        *guard = 42;
        println!("修改值为 = {}", *guard);
    }  // guard drop, 锁自动释放
    
    let guard = mutex.lock();
    println!("再次获得锁, 值 = {}", *guard);
}

练习 7 (挑战): 内存布局分析

分析以下结构体在内存中的布局:

use std::mem;
 
struct Small {
    a: u8,
    b: u8,
}
 
struct Padded {
    a: u8,
    b: u32,  // 注意对齐
    c: u8,
}
 
#[repr(C)]
struct PackedC {
    a: u8,
    b: u32,
    c: u8,
}
 
fn main() {
    println!("Small size: {} bytes", mem::size_of::<Small>());
    println!("Padded size: {} bytes", mem::size_of::<Padded>());
    println!("PackedC size: {} bytes", mem::size_of::<PackedC>());
    
    // Small: 2 bytes (u8 + u8, 无填充)
    // Padded: 12 bytes (u8(1) + 填充(3) + u32(4) + u8(1) + 填充(3))
    // PackedC: 9 bytes (#[repr(C)] 按 C 标准布局)
}

练习 8 (挑战): 自定义 Drop 顺序

理解并修复以下代码的 drop 顺序问题:

struct Logger {
    name: &'static str,
}
 
impl Drop for Logger {
    fn drop(&mut self) {
        println!("Logger '{}' dropped", self.name);
    }
}
 
struct Service {
    logger: Logger,
    name: &'static str,
}
 
impl Drop for Service {
    fn drop(&mut self) {
        // 问题: 此时 self.logger 还存在吗?
        println!("Service '{}' shutting down", self.name);
        // 可以在这里使用 self.logger
    }
}
 
fn main() {
    let s = Service {
        name: "web",
        logger: Logger { name: "web-logger" },
    };
    println!("Service running");
}
// 期望输出顺序是什么?

答案:

Service running
Service 'web' shutting down   ← impl Drop 先运行
Logger 'web-logger' dropped   ← 然后字段按声明顺序 drop

结构体的 drop() 方法在字段被 drop 之前调用,所以可以在 drop() 中使用字段。


练习 9 (中等): 所有权与枚举

实现一个表示计算结果的枚举,包含值或错误:

#[derive(Debug)]
enum Computation {
    Value(f64),
    Error(String),
    Pending,
}
 
impl Computation {
    fn compute(input: f64) -> Self {
        if input < 0.0 {
            Computation::Error(format!("负数不支持: {}", input))
        } else if input.is_nan() {
            Computation::Pending
        } else {
            Computation::Value(input.sqrt())
        }
    }
    
    fn unwrap_or(self, default: f64) -> f64 {
        match self {
            Computation::Value(v) => v,
            _ => default,
        }
    }
}
 
fn main() {
    let results: Vec<Computation> = vec![-1.0, 4.0, 9.0, 16.0]
        .into_iter()
        .map(Computation::compute)
        .collect();
    
    for result in results {
        println!("{:?}", result);
    }
}

练习 10 (综合): 文件处理器

实现一个文件处理器,展示所有权在实际场景中的应用:

use std::fs;
use std::io::{self, Read, Write};
use std::path::Path;
 
pub struct FileProcessor {
    input_path: String,
    output_path: String,
}
 
impl FileProcessor {
    pub fn new(input: &str, output: &str) -> Self {
        FileProcessor {
            input_path: input.to_string(),
            output_path: output.to_string(),
        }
    }
    
    pub fn process<F>(&self, transform: F) -> io::Result<usize>
    where
        F: Fn(String) -> String,
    {
        // 读取输入文件(获得 String 所有权)
        let content = fs::read_to_string(&self.input_path)?;
        
        // 转换内容(消费 content, 产生新 String)
        let transformed = transform(content);
        
        // 写入输出文件
        let bytes_written = transformed.len();
        fs::write(&self.output_path, transformed)?;
        
        Ok(bytes_written)
    }
}
 
fn main() -> io::Result<()> {
    // 写入测试文件
    fs::write("/tmp/input.txt", "hello world\nfoo bar\n")?;
    
    let processor = FileProcessor::new("/tmp/input.txt", "/tmp/output.txt");
    
    let bytes = processor.process(|content| {
        content.to_uppercase()
    })?;
    
    println!("处理了 {} 字节", bytes);
    println!("结果: {}", fs::read_to_string("/tmp/output.txt")?);
    
    Ok(())
}

源码导读

标准库中的 Drop

查看 std::collections::vec_deque::VecDeque 的 drop 实现,理解复杂数据结构如何管理内存:

// 在 Rust 源码中 (简化版)
impl<T, A: Allocator> Drop for VecDeque<T, A> {
    fn drop(&mut self) {
        // 1. 安全地 drop 所有元素
        struct Dropper<'a, T, A: Allocator>(&'a mut VecDeque<T, A>);
        impl<T, A: Allocator> Drop for Dropper<'_, T, A> {
            fn drop(&mut self) {
                // 即使 panic, 也确保释放内存
                unsafe { ... }
            }
        }
        
        // 2. 释放底层内存
    }
}

重点理解:

  1. 为什么内部用 Dropper 结构体? (处理 panic 安全性)
  2. drop_in_place 和 drop 的区别
  3. allocator 接口的作用

小结

概念要点
栈 vs 堆栈快但大小固定,堆灵活但需管理
所有权规则一个值一个所有者,离开作用域自动释放
Move 语义转移所有权,原变量失效
Copy 语义类型可 Copy 时,赋值复制而非移动
Drop/RAII离开作用域自动清理,类似 Go 的 defer+close

下一步

  • 完成本章所有练习题
  • 阅读 03-借用检查器原理.md
  • 在 Rust Playground 中实验 drop 顺序

记住: 所有权系统让 Rust 成为”编译器是最严格的代码审查员”的语言! 🦀