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、CopyandDrop. 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()andfree()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 对象依然存在,玩脱了照样可能滚进未定义行为。
- In C,
- 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.
它靠的是所有权、借用、可变性和生命周期这几套机制一起配合。
- Moves are destructive: once ownership is moved, the old binding becomes invalid.
For C++ developers — Smart Pointer Mapping:
给 C++ 开发者的智能指针对照表:
C++ Rust Safety 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 unsafeblocks对 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.
借用的存活时间绝对不能超过拥有者的作用域。
- The original variable owns the value.
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
Copytypes.
按值复制,常见于实现了Copy的小类型。 - By reference using
&or&mut.
按引用借用,用&或&mut表示。 - By move, transferring ownership into the function.
按 move 转移所有权,把值整个交给函数。
- By value copy for small
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 会自动执行清理。
- Returned references must still refer to something that is alive when the function ends.
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-
Copyvalues
默认情况下,对非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 太“狠”,第一反应通常应该是改成借用。
- Any outstanding borrows must be respected; moved values cannot still be used through old bindings.
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 dataclone()会把拥有的数据真正复制一份出来。- The upside is both values stay valid.
好处是原值和新值都继续有效。 - The downside is that extra allocation or copy work may happen.
代价则是会产生额外分配或复制成本。
- The upside is both values stay valid.
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
Copytrait
Rust 里的很多原始类型,都是通过Copytrait 按值拷贝的。- Examples include
u8、u32、i32这些简单值。
像u8、u32、i32这些简单数值类型,基本都属于这一类。 - User-defined types can opt in with
#[derive(Copy, Clone)]if every field is alsoCopy.
用户自定义类型如果所有字段都满足条件,也可以通过#[derive(Copy, Clone)]主动加入Copy语义。
- Examples include
// 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()逻辑。Dropis the trait that defines custom destruction behavior.Droptrait 用来定义自定义析构行为。Stringuses 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 usedrop(obj), which consumes the value and prevents further use.
关键安全点: 不能直接手调.drop()方法。正确方式是drop(obj),它会把值吃掉,析构完之后也杜绝再次使用。
For C++ developers:
Dropmaps very closely to a destructor.
给 C++ 开发者:Drop基本就对应析构函数。
C++ destructor Rust DropSyntax ~MyClass() { ... }impl Drop for MyType { fn drop(&mut self) { ... } }When called End of scope End of scope Called on move Moved-from object still exists Moved-from value is gone Manual call Dangerous explicit destructor call drop(obj)consumes safelyOrder Reverse declaration order Reverse declaration order Rule of Five Must manage special member functions Only Drop;Cloneis opt-inVirtual dtor needed? Sometimes yes No 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
Pointexperiments with and withoutCopyin the derive list, and make sure the difference between move and copy is fully clear.
给Point自己做几组实验,分别试试带Copy和不带Copy的情况,务必把 move 和 copy 的区别看明白。 - Implement a custom
DropforPointthat setsxandyto0insidedrop()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)