Rust: Rust 所有权(Ownership):内存安全的核心三原则
最后更新:2026-08-26
所有权是 Rust 最独特的特性——它让 Rust 在不需要垃圾回收器的情况下保证内存安全。
所有权是 Rust 和其他语言最根本的区别。理解了它,你就理解了 Rust 的设计灵魂。
1. 你将学到
- 所有权三原则:每个值有且只有一个所有者
- 值在离开作用域时被自动释放(drop)
- move 语义:所有权转移
- Copy 和 Clone trait 的区别
- 栈与堆的数据存储方式
- 函数调用中的所有权传递
2. 概念图解
flowchart LR
A["值创建<br>let s = String::from(...)"] --> B["所有者绑定<br>s 拥有该值"]
B --> C["所有权转移 move<br>let s2 = s; s 失效"]
C --> D["离开作用域 drop<br>} 自动释放内存"]
D --> E["内存安全<br>无双重释放、无悬垂指针"]
3. 一个图书馆的故事
(1) 痛苦:书被两个人同时借走了
Tom 管理一家小型图书馆。他遇到了一个经典问题:
- Alice 借了一本《Rust 编程》,看到第 50 页
- Bob 也想要同一本书,Tom 又借给了他
- 结果 Alice 在第 50 页写批注,Bob 在第 100 页写批注
- 最后书还回来时,批注混在一起,根本分不清是谁写的
- 更糟的是,两个人同时修改同一页,导致页面撕裂
"如果一本书一次只能借给一个人就好了……"
(2) Rust 所有权的规则
fn main() {
let book = String::from("Rust 编程"); // book 是这本书的所有者
// let book2 = book; // ❌ 如果这样写,book 的所有权转移给了 book2
// println!("{}", book); // ❌ book 不能再被使用
// ✅ 正确的做法:一次只有一个人拥有所有权
println!("{} 属于 Tom 的图书馆", book);
} // book 在这里被自动销毁——不需要手动 free
Rust 的所有权规则就像图书馆的"一次只借给一个人"原则:每个值在任意时刻有且只有一个所有者。所有者离开作用域时,值被自动销毁。
4. 所有权三原则
graph TB
A[所有权三原则] --> B[原则1: 每个值有且只有一个所有者]
A --> C[原则2: 值离开作用域即被销毁]
A --> D[原则3: 所有权可以转移(move)]
B --> E[let s = String::from("hi")]
C --> F[} 结束时自动调用 drop]
D --> G[let s2 = s; // s 不再有效]
(1) 原则详解
| 原则 | 说明 | 类比 |
|---|---|---|
| 唯一所有者 | 每个值在任意时刻只有一个人"拥有"它 | 图书馆的书一次只能借给一个人 |
| 作用域结束 = 销毁 | 变量离开作用域时自动调用 drop |
借书到期自动归还 |
| 所有权转移(move) | 赋值或传参时所有权转移,原变量失效 | 书从一个人手里交到另一个人手里 |
5. 栈与堆
理解所有权必须先理解栈和堆的区别:
| 维度 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 分配速度 | 极快(push/pop) | 较慢(需找空闲内存) |
| 存储数据 | 编译时已知大小的数据 | 大小未知或动态变化的数据 |
| 典型类型 | i32、bool、f64、数组 |
String、Vec、Box |
| 内存管理 | 自动(函数调用栈帧) | 需要手动或通过所有权管理 |
(3) Copy vs Clone vs Move 对比
| 操作 | 语法 | 原变量是否有效 | 性能开销 | 适用类型 |
|---|---|---|---|---|
| Copy | let b = a; |
有效 | 极低(栈按位复制) | i32、f64、bool、char、元组(含Copy类型) |
| Clone | let b = a.clone(); |
有效 | 较高(堆内存分配) | String、Vec<T>、Box<T> |
| Move | let b = a; |
失效 | 零(只复制指针) | String、Vec<T>、Box<T> |
如果一个类型实现了
Copy,赋值时自动复制;如果没实现Copy,赋值时自动移动。需要保留原变量时用.clone()显式深拷贝。
fn main() {
// 栈上数据:大小固定,Copy 语义
let x: i32 = 5; // 栈上分配 4 字节
let y = x; // 复制一份,x 和 y 都有效
println!("x={}, y={}", x, y); // ✅ 都可用
// 堆上数据:大小不定,move 语义
let s1 = String::from("hello"); // s1 在栈上(ptr/len/cap),实际数据在堆上
let s2 = s1; // ❌ 所有权从 s1 转移到 s2
// println!("{}", s1); // ❌ 编译错误:s1 已被移动
println!("{}", s2); // ✅ s2 是新的所有者
}
6. 所有权示例
▶ 示例 1:move 语义——所有权转移(难度 ⭐⭐)
// ============================================
// 演示所有权转移:赋值时发生 move
// ============================================
fn main() {
let s1 = String::from("Rust");
let s2 = s1; // s1 的所有权转移到 s2
// println!("s1: {}", s1); // ❌ 编译错误!s1 已被移动
println!("s2: {}", s2); // ✅ s2 现在是所有者
// 整数类型是 Copy 的,所以不会 move
let a = 42;
let b = a; // a 的值被复制给 b
println!("a: {}, b: {}", a, b); // ✅ 两者都有效
}
输出:
s2: Rust
a: 42, b: 42
整型等标量类型实现了
Copytrait,赋值时是复制而不是移动。String没有实现Copy,赋值时是移动。移动后原变量失效,这是 Rust 防止"双重释放"的关键设计。
▶ 示例 2:函数调用中的所有权传递(难度 ⭐⭐)
// ============================================
// 传参和返回值中的所有权转移
// ============================================
fn take_ownership(s: String) {
println!("拿到所有权: {}", s);
} // s 在这里被销毁(drop)
fn give_ownership() -> String {
let s = String::from("新创建的字符串");
s // 所有权返回给调用者
}
fn main() {
let s1 = String::from("hello");
take_ownership(s1); // s1 的所有权被移入函数
// println!("{}", s1); // ❌ s1 已无效
let s2 = give_ownership(); // 从函数中获取所有权
println!("s2: {}", s2); // ✅ s2 拥有所有权
// 传进去再传回来
let s3 = String::from("来回传递");
let s3_back = takes_and_returns(s3);
// println!("{}", s3); // ❌ s3 已被移动
println!("s3_back: {}", s3_back); // ✅
}
fn takes_and_returns(s: String) -> String {
println!("函数内部: {}", s);
s // 把所有权返回给调用者
}
输出:
拿到所有权: hello
s2: 新创建的字符串
函数内部: 来回传递
s3_back: 来回传递
函数调用时,传入的参数所有权被转移给函数参数。函数返回值时,所有权被转移给调用者。这就是"所有权在函数间流动"——和谁拿着钥匙谁就能开门一个道理。
▶ 示例 3:Copy vs Clone 的对比(难度 ⭐⭐⭐)
// ============================================
// Copy(自动复制) vs Clone(显式克隆)
// ============================================
fn main() {
// --- Copy 类型:赋值自动复制 ---
let num1 = 100;
let num2 = num1; // 自动复制,num1 仍然有效
println!("Copy: num1={}, num2={}", num1, num2); // ✅
// --- Clone 类型:需要显式调用 .clone() ---
let s1 = String::from("需要克隆");
let s2 = s1.clone(); // 显式克隆,s1 仍然有效
println!("Clone: s1={}, s2={}", s1, s2); // ✅
// --- 不 Clone 也不 Copy:只能 move ---
let v1 = vec![1, 2, 3];
let v2 = v1; // move!v1 失效
// println!("{:?}", v1); // ❌
println!("v2: {:?}", v2); // ✅
}
输出:
Copy: num1=100, num2=100
Clone: s1=需要克隆, s2=需要克隆
v2: [1, 2, 3]
Copy:赋值时自动按位复制,原始变量仍然有效(适用于栈上数据)。Clone:需要显式调用
.clone()方法,适用于堆上数据。Clone 的代价更高(需要分配堆内存),所以 Rust 设计成"你必须显式选择克隆"。
▶ 示例 4:函数中所有权的获取与归还模式(难度 ⭐⭐⭐)
// ============================================
// 常见模式:传进去再传回来(获取+归还)
// ============================================
struct User {
name: String,
score: i32,
}
fn boost_score(mut user: User, bonus: i32) -> User {
user.score += bonus;
user
}
fn format_user(user: &User) -> String {
format!("{}: {} 分", user.name, user.score)
}
fn main() {
let alice = User {
name: String::from("Alice"),
score: 80,
};
println!("升级前: {}", format_user(&alice));
let alice = boost_score(alice, 15);
println!("升级后: {}", format_user(&alice));
let alice = boost_score(alice, 10);
println!("再次升级: {}", format_user(&alice));
let mut bob = User {
name: String::from("Bob"),
score: 60,
};
let bonus = 20;
bob.score += bonus;
println!("Bob 加分后: {}", format_user(&bob));
let names = vec![alice.name.clone(), bob.name.clone()];
println!("所有用户: {:?}", names);
println!("Alice 最终分数: {}", alice.score);
println!("Bob 最终分数: {}", bob.score);
}
输出:
升级前: Alice: 80 分
升级后: Alice: 95 分
再次升级: Alice: 105 分
Bob 加分后: Bob: 80 分
所有用户: ["Alice", "Bob"]
Alice 最终分数: 105
Bob 最终分数: 80
结构体的所有权传递与基本类型一致。
boost_score通过参数获取所有权、修改后返回——这是"获取+归还"模式。用引用(&User)可以只读访问而不获取所有权。
▶ 示例 5:所有权与集合的交互(难度 ⭐⭐⭐)
// ============================================
// Vec、String 与所有权的常见交互模式
// ============================================
fn take_first_word(words: &Vec<String>) -> Option<&str> {
words.first().map(|s| s.as_str())
}
fn remove_and_return(vec: &mut Vec<i32>, index: usize) -> Option<i32> {
if index < vec.len() {
Some(vec.remove(index))
} else {
None
}
}
fn main() {
let mut words = vec![
String::from("hello"),
String::from("rust"),
String::from("world"),
];
println!("原始向量: {:?}", words);
if let Some(first) = take_first_word(&words) {
println!("第一个单词: {}", first);
}
let removed = remove_and_return(&mut words, 1);
println!("移除索引 1: {:?}, 剩余: {:?}", removed, words);
let mut numbers = vec![10, 20, 30, 40, 50];
while let Some(val) = numbers.pop() {
println!("弹出: {}", val);
}
println!("向量已空: {:?}", numbers);
let s1 = String::from("foo");
let s2 = String::from("bar");
let combined = s1 + &s2;
println!("拼接结果: {}", combined);
let data = vec![String::from("a"), String::from("b")];
for word in &data {
println!("借用: {}", word);
}
println!("向量仍可用: {:?}", data);
let data2 = vec![1, 2, 3];
for num in data2 {
println!("消费: {}", num);
}
}
输出:
原始向量: ["hello", "rust", "world"]
第一个单词: hello
移除索引 1: Some(20), 剩余: ["hello", "world"]
弹出: 50
弹出: 40
弹出: 30
弹出: 20
弹出: 10
向量已空: []
拼接结果: foobar
借用: a
借用: b
向量仍可用: ["a", "b"]
消费: 1
消费: 2
消费: 3
for item in &vec借用遍历(向量仍可用),for item in vec消费遍历(向量被 move)。+操作符消费左侧 String。pop()返回Option<T>并消费最后一个元素。
❓ 常见问题
📖 小节
- Rust 的所有权是三个核心原则:唯一所有者、作用域结束自动销毁、所有权可转移
- 栈上数据(标量类型)是 Copy 语义,赋值自动复制
- 堆上数据(String、Vec)是 move 语义,赋值转移所有权
- 移动后原变量失效,编译器阻止访问——这是编译时检查
- 函数调用和返回值也会转移所有权
.clone()显式深拷贝,保留原变量——但要付出性能代价
📝 作业
- 难度 ⭐:写一个程序,创建一个
String,把它赋值给另一个变量,然后尝试打印第一个变量——观察编译器报错信息。 - 难度 ⭐⭐:写一个函数
fn append_world(s: String) -> String,在字符串末尾追加 " world" 并返回。在main中调用并观察所有权如何进出函数。 - 难度 ⭐⭐⭐:定义一个函数
fn calculate_length(s: String) -> (String, usize),返回字符串及其长度,同时把所有权还回来。验证调用后原变量可以继续使用。