Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust memory management
Rust 的内存管理

What you’ll learn: Rust’s ownership system, the single most important concept in the language. This chapter covers move semantics, borrowing rules, Clone、Copy and Drop. For many C/C++ developers, once ownership clicks, the rest of Rust suddenly stops looking mystical.
本章将学到什么: Rust 的所有权系统,也就是整门语言里最核心的概念。本章会讲 move 语义、借用规则、Clone、Copy 和 Drop。对很多 C/C++ 开发者来说,一旦所有权想明白了,Rust 后面的大半内容都会顺眼很多。

  • Memory management in C and C++ is a major source of bugs
    C 和 C++ 里的内存管理,本来就是大量 bug 的来源。
    • In C, malloc() and free() offer no built-in protection against dangling pointers, use-after-free, or double-free.
      在 C 里,malloc() 和 free() 本身不会替着防悬空指针、释放后继续使用、重复释放这些事故。
    • In C++, RAII and smart pointers help a lot, but moved-from objects still exist and misuse can slide into undefined behavior.
      在 C++ 里,RAII 和智能指针当然已经好很多了,但 moved-from 对象依然存在,玩脱了照样可能滚进未定义行为。
  • Rust turns RAII into something much harder to misuse
    Rust 把 RAII 这套机制做得更难被误用。
    • Moves are destructive: once ownership is moved, the old binding becomes invalid.
      move 是破坏性的:一旦所有权转走,旧变量立刻失效。
    • No Rule of Five ceremony is needed.
      不需要再手动背一套 Rule of Five 样板。
    • Rust still gives low-level control over stack and heap allocation, but safety is enforced at compile time.
      Rust 依然保留了对栈和堆分配的控制力,只不过安全检查被前移到了编译期。
    • Ownership, borrowing, mutability, and lifetimes work together to make this possible.
      它靠的是所有权、借用、可变性和生命周期这几套机制一起配合。

For C++ developers — Smart Pointer Mapping:
给 C++ 开发者的智能指针对照表:

C++RustSafety Improvement
std::unique_ptr<T>Box<T>No use-after-move possible
std::shared_ptr<T>Rc<T> (single-thread)No reference cycles by default
std::shared_ptr<T> (thread-safe)Arc<T>Explicit thread-safety
std::weak_ptr<T>Weak<T>Must check validity
Raw pointer*const T / *mut TOnly in unsafe blocks

对 C 开发者来说,Box<T> 可以看成替代 malloc/free 配对,Rc<T> 可以看成替代手写引用计数,而裸指针虽然还在,但基本被关进了 unsafe 区域。

Rust ownership, borrowing and lifetimes
Rust 的所有权、借用与生命周期

  • Rust permits either one mutable reference or many read-only references to the same value
    Rust 允许的模式非常明确:同一时间要么一个可变引用,要么多个只读引用。
    • The original variable owns the value.
      最初声明变量时,它就成了该值的所有者。
    • Later references borrow from that owner.
      后续产生的引用,则是在向这个所有者借用。
    • A borrow can never outlive the owning scope.
      借用的存活时间绝对不能超过拥有者的作用域。
fn main() {
    let a = 42; // Owner
    let b = &a; // First borrow
    {
        let aa = 42;
        let c = &a; // Second borrow; a is still in scope
        // Ok: c goes out of scope here
        // aa goes out of scope here
    }
    // let d = &aa; // Will not compile unless aa is moved to outside scope
    // b implicitly goes out of scope before a
    // a goes out of scope last
}
  • Functions can receive values in several ways
    函数接收参数时,也有几种不同方式。
    • By value copy for small Copy types.
      按值复制,常见于实现了 Copy 的小类型。
    • By reference using & or &mut.
      按引用借用,用 & 或 &mut 表示。
    • By move, transferring ownership into the function.
      按 move 转移所有权,把值整个交给函数。
fn foo(x: &u32) {
    println!("{x}");
}
fn bar(x: u32) {
    println!("{x}");
}
fn main() {
    let a = 42;
    foo(&a);    // By reference
    bar(a);     // By value (copy)
}
  • Rust forbids returning dangling references
    Rust 明确禁止返回悬空引用。
    • Returned references must still refer to something that is alive when the function ends.
      函数返回的引用,在函数结束之后也必须还能指向活着的数据。
    • When a value leaves scope, Rust automatically drops it.
      值离开作用域时,Rust 会自动执行清理。
fn no_dangling() -> &u32 {
    // lifetime of a begins here
    let a = 42;
    // Won't compile. lifetime of a ends here
    &a
}

fn ok_reference(a: &u32) -> &u32 {
    // Ok because the lifetime of a always exceeds ok_reference()
    a
}
fn main() {
    let a = 42;     // lifetime of a begins here
    let b = ok_reference(&a);
    // lifetime of b ends here
    // lifetime of a ends here
}

Rust move semantics
Rust 的 move 语义

  • By default, assignment transfers ownership for non-Copy values
    默认情况下,对非 Copy 类型做赋值时,会转移所有权。
fn main() {
    let s = String::from("Rust");    // Allocate a string from the heap
    let s1 = s; // Transfer ownership to s1. s is invalid at this point
    println!("{s1}");
    // This will not compile
    //println!("{s}");
    // s1 goes out of scope here and the memory is deallocated
    // s goes out of scope here, but nothing happens because it doesn't own anything
}
graph LR
    subgraph "Before: let s1 = s<br/>赋值之前"
        S["s (stack)<br/>ptr"] -->|"owns"| H1["Heap: R u s t"]
    end

    subgraph "After: let s1 = s<br/>赋值之后"
        S_MOVED["s (stack)<br/>⚠️ MOVED"] -.->|"invalid"| H2["Heap: R u s t"]
        S1["s1 (stack)<br/>ptr"] -->|"now owns"| H2
    end

    style S_MOVED fill:#ff6b6b,color:#000,stroke:#333
    style S1 fill:#51cf66,color:#000,stroke:#333
    style H2 fill:#91e5a3,color:#000,stroke:#333

After let s1 = s, ownership transfers to s1. The heap data stays where it is; only the owning pointer moves, and s becomes invalid.
执行 let s1 = s 之后,所有权转移给 s1。堆上的数据并没有搬家,移动的只是拥有它的那根指针,而 s 从此失效。


Rust move semantics and borrowing
move 语义与借用

fn foo(s : String) {
    println!("{s}");
    // The heap memory pointed to by s will be deallocated here
}
fn bar(s : &String) {
    println!("{s}");
    // Nothing happens -- s is borrowed
}
fn main() {
    let s = String::from("Rust string move example");    // Allocate a string from the heap
    foo(s); // Transfers ownership; s is invalid now
    // println!("{s}");  // will not compile
    let t = String::from("Rust string borrow example");
    bar(&t);    // t continues to hold ownership
    println!("{t}"); 
}

Rust move semantics and ownership
move 与所有权转移

  • It is perfectly legal to transfer ownership by moving
    通过 move 转移所有权,本身就是 Rust 的正常操作。
    • Any outstanding borrows must be respected; moved values cannot still be used through old bindings.
      但借用规则仍然有效,已经转走的值不能再通过旧变量继续碰。
    • If moving feels too destructive, borrowing is usually the first alternative to consider.
      如果 move 太“狠”,第一反应通常应该是改成借用。
struct Point {
    x: u32,
    y: u32,
}
fn consume_point(p: Point) {
    println!("{} {}", p.x, p.y);
}
fn borrow_point(p: &Point) {
    println!("{} {}", p.x, p.y);
}
fn main() {
    let p = Point {x: 10, y: 20};
    // Try flipping the two lines
    borrow_point(&p);
    consume_point(p);
}

Rust Clone
Rust 的 Clone

  • clone() creates a true duplicate of the owned data
    clone() 会把拥有的数据真正复制一份出来。
    • The upside is both values stay valid.
      好处是原值和新值都继续有效。
    • The downside is that extra allocation or copy work may happen.
      代价则是会产生额外分配或复制成本。
fn main() {
    let s = String::from("Rust");    // Allocate a string from the heap
    let s1 = s.clone(); // Copy the string; creates a new allocation on the heap
    println!("{s1}");  
    println!("{s}");
    // s1 goes out of scope here and the memory is deallocated
    // s goes out of scope here, and the memory is deallocated
}
graph LR
    subgraph "After: let s1 = s.clone()<br/>clone 之后"
        S["s (stack)<br/>ptr"] -->|"owns"| H1["Heap: R u s t"]
        S1["s1 (stack)<br/>ptr"] -->|"owns (copy)"| H2["Heap: R u s t"]
    end

    style S fill:#51cf66,color:#000,stroke:#333
    style S1 fill:#51cf66,color:#000,stroke:#333
    style H1 fill:#91e5a3,color:#000,stroke:#333
    style H2 fill:#91e5a3,color:#000,stroke:#333

clone() creates a separate heap allocation. Both values are valid because each owns its own copy.
clone() 会得到一块独立的堆内存。两个变量都合法,因为它们各自拥有自己那份副本。

Rust Copy trait
Rust 的 Copy trait

  • Primitive types use copy semantics through the Copy trait
    Rust 里的很多原始类型,都是通过 Copy trait 按值拷贝的。
    • Examples include u8、u32、i32 这些简单值。
      像 u8、u32、i32 这些简单数值类型,基本都属于这一类。
    • User-defined types can opt in with #[derive(Copy, Clone)] if every field is also Copy.
      用户自定义类型如果所有字段都满足条件,也可以通过 #[derive(Copy, Clone)] 主动加入 Copy 语义。
// Try commenting this out to see the change in let p1 = p; belw
#[derive(Copy, Clone, Debug)]   // We'll discuss this more later
struct Point{x: u32, y:u32}
fn main() {
    let p = Point {x: 42, y: 40};
    let p1 = p;     // This will perform a copy now instead of move
    println!("p: {p:?}");
    println!("p1: {p:?}");
    let p2 = p1.clone();    // Semantically the same as copy
}

Rust Drop trait
Rust 的 Drop trait

  • Rust automatically calls drop() at the end of scope
    值离开作用域时,Rust 会自动调用对应的 drop() 逻辑。
    • Drop is the trait that defines custom destruction behavior.
      Drop trait 用来定义自定义析构行为。
    • String uses it to release heap memory, and other resource-owning types do similar cleanup.
      比如 String 就靠它释放堆内存,其他资源管理类型也一样会在这里做清理。
    • For C developers, this replaces a lot of manual free() calls with scope-based cleanup.
      对 C 开发者来说,这基本就是把大量手动 free() 换成了作用域结束自动清理。
  • Key safety: You cannot call .drop() directly. Instead use drop(obj), which consumes the value and prevents further use.
    关键安全点: 不能直接手调 .drop() 方法。正确方式是 drop(obj),它会把值吃掉,析构完之后也杜绝再次使用。

For C++ developers: Drop maps very closely to a destructor.
给 C++ 开发者: Drop 基本就对应析构函数。

C++ destructorRust Drop
Syntax~MyClass() { ... }impl Drop for MyType { fn drop(&mut self) { ... } }
When calledEnd of scopeEnd of scope
Called on moveMoved-from object still existsMoved-from value is gone
Manual callDangerous explicit destructor calldrop(obj) consumes safely
OrderReverse declaration orderReverse declaration order
Rule of FiveMust manage special member functionsOnly Drop; Clone is opt-in
Virtual dtor needed?Sometimes yesNo inheritance, no slicing issue
struct Point {x: u32, y:u32}

// Equivalent to: ~Point() { printf("Goodbye point x:%u, y:%u\n", x, y); }
impl Drop for Point {
    fn drop(&mut self) {
        println!("Goodbye point x:{}, y:{}", self.x, self.y);
    }
}
fn main() {
    let p = Point{x: 42, y: 42};
    {
        let p1 = Point{x:43, y: 43};
        println!("Exiting inner block");
        // p1.drop() called here — like C++ end-of-scope destructor
    }
    println!("Exiting main");
    // p.drop() called here
}

Exercise: Move, Copy and Drop
练习:move、copy 与 drop

🟡 Intermediate — experiment freely; the compiler will teach a lot here
🟡 进阶练习:这里很适合自己多试,编译器会把很多关键区别直接指出来。

  • Create your own Point experiments with and without Copy in the derive list, and make sure the difference between move and copy is fully clear.
    给 Point 自己做几组实验,分别试试带 Copy 和不带 Copy 的情况,务必把 move 和 copy 的区别看明白。
  • Implement a custom Drop for Point that sets x and y to 0 inside drop() just to observe the pattern.
    再给 Point 手写一个 Drop,在 drop() 里把 x 和 y 设成 0,单纯用来感受这类资源释放模式。
struct Point{x: u32, y: u32}
fn main() {
    // Create Point, assign it to a different variable, create a new scope,
    // pass point to a function, etc.
}
Solution 参考答案
#[derive(Debug)]
struct Point { x: u32, y: u32 }

impl Drop for Point {
    fn drop(&mut self) {
        println!("Dropping Point({}, {})", self.x, self.y);
        self.x = 0;
        self.y = 0;
        // Note: setting to 0 in drop demonstrates the pattern,
        // but you can't observe these values after drop completes
    }
}

fn consume(p: Point) {
    println!("Consuming: {:?}", p);
    // p is dropped here
}

fn main() {
    let p1 = Point { x: 10, y: 20 };
    let p2 = p1;  // Move — p1 is no longer valid
    // println!("{:?}", p1);  // Won't compile: p1 was moved

    {
        let p3 = Point { x: 30, y: 40 };
        println!("p3 in inner scope: {:?}", p3);
        // p3 is dropped here (end of scope)
    }

    consume(p2);  // p2 is moved into consume and dropped there
    // println!("{:?}", p2);  // Won't compile: p2 was moved

    // Now try: add #[derive(Copy, Clone)] to Point (and remove the Drop impl)
    // and observe how p1 remains valid after let p2 = p1;
}
// Output:
// p3 in inner scope: Point { x: 30, y: 40 }
// Dropping Point(30, 40)
// Consuming: Point { x: 10, y: 20 }
// Dropping Point(10, 20)