文章目录
-
- 一、为什么需要所有权?
- 二、所有权三大规则
- 三、移动语义(Move)
-
- [3.1 问题引入](#3.1 问题引入)
- [3.2 String 的内存结构](#3.2 String 的内存结构)
- [3.3 浅拷贝的问题](#3.3 浅拷贝的问题)
- [3.4 Rust 的解决方案:移动语义](#3.4 Rust 的解决方案:移动语义)
- [四、简单类型的拷贝:Copy trait](#四、简单类型的拷贝:Copy trait)
-
- [4.1 整型的行为不同](#4.1 整型的行为不同)
- [4.2 Copy trait](#4.2 Copy trait)
- [4.3 Copy 和 Drop 互斥](#4.3 Copy 和 Drop 互斥)
- [五、深拷贝:Clone trait](#五、深拷贝:Clone trait)
-
- [5.1 使用 clone()](#5.1 使用 clone())
- [5.2 手动实现 Clone](#5.2 手动实现 Clone)
- [六、Copy vs Clone 对比](#六、Copy vs Clone 对比)
- 七、函数调用与所有权转移
-
- [7.1 传参时的移动](#7.1 传参时的移动)
- [7.2 返回值转移所有权](#7.2 返回值转移所有权)
- 八、函数参数与返回值配对模式
- 九、常见陷阱与最佳实践
- 十、总结与速查
- 十一、思考题
- 参考链接
理解所有权的核心规则 ------ 移动语义、Clone、Copy trait
一、为什么需要所有权?
在开始学习所有权之前,先来看看其他语言是怎么管理内存的:
| 语言 | 内存管理方式 | 优点 | 缺点 |
|---|---|---|---|
| C/C++ | 手动管理 | 性能最好 | 极易出错, |
| Java/Go/Python | 垃圾回收(GC) | 使用简单 | 运行时开销,STW问题 |
| Rust | 所有权 | 无GC,性能好 | 学习曲线陡 |
Rust 的所有权系统在编译时就能保证内存安全,既不需要 GC,也不会出现悬垂指针。这就是 Rust 最独特的特性。
二、所有权三大规则
所有权系统基于三条简单规则:
- 每个值有唯一的所有者(owner)
- 同一时刻只能有一个可变引用(下篇细讲)
- 当所有者离开作用域,值会被自动释放
作用域示例:
rust
{ // s 未声明
let s = "hello"; // s 进入作用域,生效
// 可以使用 s
} // s 离开作用域,drop 被调用,内存释放
对于 String 类型,离开作用域时会自动调用 drop 函数释放堆内存。
三、移动语义(Move)
3.1 问题引入
rust
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // ❌ 编译错误!
println!("{}", s2); // ✅ 正常工作
}
为什么 s1 不能用了?这要从 String 的内存结构说起。
3.2 String 的内存结构
String 在内存中由三部分组成(存储在栈上):
log
s1 (栈)
+---------+--------+----------+
| ptr | len | capacity |
+---------+--------+----------+
|
↓
堆内存:[h][e][l][l][o]
- ptr:指向堆内存的指针
- len:当前长度(5)
- capacity:总容量
3.3 浅拷贝的问题
如果执行 let s2 = s1,只拷贝栈上的三部分(浅拷贝),会出现问题:
log
s1 (栈) s2 (栈)
+---------+--------+----------+ +---------+--------+----------+
| ptr | len | capacity | | ptr | len | capacity |
+---------+--------+----------+ +---------+--------+----------+
| |
└────────┬───────────────────────────┘
↓
堆内存:[h][e][l][l][o]
当 s1 和 s2 都离开作用域时,同一块堆内存会被释放两次(double free),这是严重的内存错误。
3.4 Rust 的解决方案:移动语义
Rust 的做法是:let s2 = s1 时,将 s1 的栈数据移动到 s2,s1 被标记为无效。
rust
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权移动到 s2
// s1 不再有效,编译器禁止使用
log
移动后:
s1 (失效) s2 (有效)
+---------+--------+----------+ +---------+--------+----------+
| ptr | len | capacity | | ptr | len | capacity |
+---------+--------+----------+ +---------+--------+----------+
|
↓
堆内存:[h][e][l][l][o]
这样 s2 离开作用域时只会释放一次内存。
四、简单类型的拷贝:Copy trait
4.1 整型的行为不同
rust
fn main() {
let x = 5;
let y = x; // x 没有被移动
println!("x = {}, y = {}", x, y); // ✅ 两个都可以用
}
为什么整型和 String 的行为不同?
因为整型是存储在栈上的,不需要堆分配。let y = x 只是把 5 这个值拷贝了一份。
4.2 Copy trait
实现 Copy trait 的类型,赋值时会隐式拷贝而不是移动:
rust
// 实现了 Copy 的类型(部分)
// 整数:i8, i16, i32, i64, i128, isize, u8, ...
// 浮点数:f32, f64
// 布尔:bool
// 字符:char
// 元组:仅当所有元素都是 Copy 时,如 (i32, i32)
4.3 Copy 和 Drop 互斥
rust
struct MyStruct {
data: String, // String 没有实现 Copy
}
// ❌ 编译错误:String 没有实现 Copy
// impl Copy for MyStruct {}
重要:任何需要 drop 处理(如释放堆内存)的类型都不能实现 Copy。
五、深拷贝:Clone trait
5.1 使用 clone()
如果你确实需要完整复制堆上的数据(深拷贝),使用 .clone():
rust
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 完整的深拷贝
println!("s1 = {}, s2 = {}", s1, s2); // ✅ 两者都有效
}
内存结构(深拷贝后):
log
s1 (栈) s2 (栈)
+---------+--------+ +---------+--------+
| ptr | len | | ptr | len |
+---------+--------+ +---------+--------+
↓ ↓
堆:[h][e][l][l][o] 堆:[h][e][l][l][o] (两份独立数据)
5.2 手动实现 Clone
可以为自定义类型实现 Clone:
rust
#[derive(Debug, Clone)]
struct Person {
name: String,
age: u32,
}
fn main() {
let p1 = Person {
name: String::from("Alice"),
age: 30,
};
let p2 = p1.clone(); // 深拷贝
println!("p1: {:?}, p2: {:?}", p1, p2); // 两者都可用
}
六、Copy vs Clone 对比
| 特性 | Copy | Clone |
|---|---|---|
| 发生时机 | 隐式(赋值、传参时自动发生) | 显式(需要调用 .clone()) |
| 性能开销 | 极低(栈拷贝,memcpy) | 可能很大(堆数据需深拷贝) |
| 实现条件 | 所有成员都必须是 Copy 类型 | 无限制,可为任意类型 |
| 能否自定义 | 不能自定义,只能是按位拷贝 | 可以自定义 clone() 方法 |
| 典型类型 | 整数、浮点、bool、char、数组 | String、Vec、HashMap、自定义类型 |
选择建议
- 小数据且频繁传递:考虑实现 Copy(如包装整数的结构体)
- 需要完整拷贝:使用 Clone,但要考虑性能
- 不确定:保持所有权转移(移动),这是最安全高效的
七、函数调用与所有权转移
7.1 传参时的移动
rust
fn main() {
let s = String::from("hello");
take_ownership(s); // s 移动到函数内
// println!("{}", s); // ❌ s 已无效
let x = 5;
make_copy(x); // x 是 Copy 类型,不会移动
println!("{}", x); // ✅ 仍然有效
}
fn take_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 离开作用域,内存释放
fn make_copy(some_int: i32) {
println!("{}", some_int);
} // 无内存释放
7.2 返回值转移所有权
rust
fn main() {
let s1 = gives_ownership(); // 返回值所有权给 s1
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 移动进函数,返回值给 s3
} // s1 和 s3 被 drop,s2 已移动,无事发生
fn gives_ownership() -> String {
let s = String::from("hello");
s // 所有权移出
}
fn takes_and_gives_back(s: String) -> String {
s // 接收后返回
}
八、函数参数与返回值配对模式
如果需要在函数调用后继续使用原值,有几种模式:
模式 1:返回元组
rust
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("'{}' 的长度是 {}", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length) // 返回原值和计算结果
}
但这种写法很繁琐,下一篇讲的引用才是更好的解决方案。
模式 2:使用 clone
rust
fn main() {
let s1 = String::from("hello");
let len = calculate_length(s1.clone()); // 传入副本
println!("'{}' 的长度是 {}", s1, len); // s1 仍然有效
}
fn calculate_length(s: String) -> usize {
s.len()
}
九、常见陷阱与最佳实践
陷阱 1:误以为赋值是深拷贝
rust
let s1 = String::from("hello");
let s2 = s1; // ❌ 误以为是拷贝,实际是移动
println!("{}", s1); // 编译失败
陷阱 2:函数调用后还想用原值
rust
fn process(s: String) {
// 处理...
}
let s = String::from("hello");
process(s);
println!("{}", s); // ❌ 所有权已转移
陷阱 3:对 Copy 类型误解
rust
let v1: Vec<i32> = vec![1, 2, 3];
let v2 = v1; // 移动(Vec 没有 Copy)
let v3 = v1.clone(); // 深拷贝
let arr1 = [1, 2, 3]; // 数组元素是 Copy
let arr2 = arr1; // 拷贝(数组实现 Copy)
最佳实践总结
| 场景 | 推荐做法 |
|---|---|
| 需要在多处使用同一个 | String 使用引用(下篇) |
| 需要独立副本 | 使用 .clone() |
| 小数据且频繁赋值 | 考虑使用 Copy 类型 |
| 不确定所有权 | 让 Rust 编译器告诉你 |
十、总结与速查
核心概念
| 概念 | 说明 | 示例 |
|---|---|---|
| 所有者 | 每个值有唯一所有者 | let s = String::from() |
| 移动 | 所有权转移,原值失效 | let s2 = s1 |
| Copy | 隐式栈拷贝,需实现 Copy trait | 整数、浮点数等 |
| Clone | 显式深拷贝,调用 .clone() | s2 = s1.clone() |
| Drop | 离开作用域自动释放内存 | 编译器自动调用 |
决策树
#mermaid-svg-J7ii9l8CinwMlwsQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-J7ii9l8CinwMlwsQ .error-icon{fill:#552222;}#mermaid-svg-J7ii9l8CinwMlwsQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-J7ii9l8CinwMlwsQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-J7ii9l8CinwMlwsQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-J7ii9l8CinwMlwsQ .marker.cross{stroke:#333333;}#mermaid-svg-J7ii9l8CinwMlwsQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-J7ii9l8CinwMlwsQ p{margin:0;}#mermaid-svg-J7ii9l8CinwMlwsQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-J7ii9l8CinwMlwsQ .cluster-label text{fill:#333;}#mermaid-svg-J7ii9l8CinwMlwsQ .cluster-label span{color:#333;}#mermaid-svg-J7ii9l8CinwMlwsQ .cluster-label span p{background-color:transparent;}#mermaid-svg-J7ii9l8CinwMlwsQ .label text,#mermaid-svg-J7ii9l8CinwMlwsQ span{fill:#333;color:#333;}#mermaid-svg-J7ii9l8CinwMlwsQ .node rect,#mermaid-svg-J7ii9l8CinwMlwsQ .node circle,#mermaid-svg-J7ii9l8CinwMlwsQ .node ellipse,#mermaid-svg-J7ii9l8CinwMlwsQ .node polygon,#mermaid-svg-J7ii9l8CinwMlwsQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-J7ii9l8CinwMlwsQ .rough-node .label text,#mermaid-svg-J7ii9l8CinwMlwsQ .node .label text,#mermaid-svg-J7ii9l8CinwMlwsQ .image-shape .label,#mermaid-svg-J7ii9l8CinwMlwsQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-J7ii9l8CinwMlwsQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-J7ii9l8CinwMlwsQ .rough-node .label,#mermaid-svg-J7ii9l8CinwMlwsQ .node .label,#mermaid-svg-J7ii9l8CinwMlwsQ .image-shape .label,#mermaid-svg-J7ii9l8CinwMlwsQ .icon-shape .label{text-align:center;}#mermaid-svg-J7ii9l8CinwMlwsQ .node.clickable{cursor:pointer;}#mermaid-svg-J7ii9l8CinwMlwsQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-J7ii9l8CinwMlwsQ .arrowheadPath{fill:#333333;}#mermaid-svg-J7ii9l8CinwMlwsQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-J7ii9l8CinwMlwsQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-J7ii9l8CinwMlwsQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J7ii9l8CinwMlwsQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-J7ii9l8CinwMlwsQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J7ii9l8CinwMlwsQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-J7ii9l8CinwMlwsQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-J7ii9l8CinwMlwsQ .cluster text{fill:#333;}#mermaid-svg-J7ii9l8CinwMlwsQ .cluster span{color:#333;}#mermaid-svg-J7ii9l8CinwMlwsQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-J7ii9l8CinwMlwsQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-J7ii9l8CinwMlwsQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-J7ii9l8CinwMlwsQ .icon-shape,#mermaid-svg-J7ii9l8CinwMlwsQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J7ii9l8CinwMlwsQ .icon-shape p,#mermaid-svg-J7ii9l8CinwMlwsQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-J7ii9l8CinwMlwsQ .icon-shape .label rect,#mermaid-svg-J7ii9l8CinwMlwsQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J7ii9l8CinwMlwsQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-J7ii9l8CinwMlwsQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-J7ii9l8CinwMlwsQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 栈上
堆上
是
否
是
否
需要复制一个值
类型存储在...
实现 Copy?
需要独立副本?
隐式拷贝,无开销
移动语义
使用 .clone
使用引用,下篇见
重要规则
- 移动后不能使用原变量
- Copy 类型不会移动
- Clone 是显式深拷贝,有性能开销
- 所有需要释放内存的类型都不能实现 Copy
十一、思考题
-
以下代码能编译吗?为什么?
rustfn main() { let s = String::from("hello"); let s2 = s; let s3 = s2.clone(); println!("{} {}", s2, s3); } -
设计一个结构体 Point,包含两个 i32 字段,能否实现 Copy?为什么?
-
以下代码的输出是什么?
rustfn main() { let mut x = 10; let y = x; x += 5; println!("x = {}, y = {}", x, y); }