目录
[02 所有权与借用](#02 所有权与借用)
[2.1 什么是所有权(Ownership)](#2.1 什么是所有权(Ownership))
[Rust 为什么需要所有权](#Rust 为什么需要所有权)
[String 类型与栈/堆](#String 类型与栈/堆)
[移动(Move)------ 核心机制](#移动(Move)—— 核心机制)
[克隆(Clone)------ 深拷贝](#克隆(Clone)—— 深拷贝)
[Copy Trait ------ 栈上拷贝](#Copy Trait —— 栈上拷贝)
[2.2 引用与借用(References & Borrowing)](#2.2 引用与借用(References & Borrowing))
[可变引用(Mutable Reference)](#可变引用(Mutable Reference))
[可变引用的限制 ------ 同一时刻只能有一个](#可变引用的限制 —— 同一时刻只能有一个)
[不可变引用 vs 可变引用的互斥](#不可变引用 vs 可变引用的互斥)
[悬垂引用(Dangling References)------ Rust 在编译期防止](#悬垂引用(Dangling References)—— Rust 在编译期防止)
[2.3 切片(Slices)](#2.3 切片(Slices))
[字符串切片的危险性 ------ UTF-8 边界](#字符串切片的危险性 —— UTF-8 边界)
[2.4 所有权决策树(速查)](#2.4 所有权决策树(速查))
02 所有权与借用
2.1 什么是所有权(Ownership)
Rust 为什么需要所有权
|-------------|--------------------|-------------------------|
| 语言 | 内存管理方式 | 问题 |
| C/C++ | 手动 malloc/free | 忘记释放 → 内存泄漏;重复释放 → 悬垂指针 |
| Java/Go | 垃圾回收(GC) | 运行时开销;GC 暂停影响延迟 |
| Rust | 所有权系统(编译期检查) | 零运行时开销,编译器保证安全 |
所有权三条铁律
1. Rust 中每一个值都有一个被称为其 所有者(owner)的变量
2. 一次只能有一个所有者
3. 当所有者离开作用域时,这个值将被丢弃(drop)
作用域(Scope)
rust
{
let s = String::from("hello"); // s 从这里开始有效
// 使用 s
} // 作用域结束,s 被丢弃(drop),内存自动释放
String 类型与栈/堆
rust
let s1 = String::from("hello"); // 堆上分配,可变长度
let s2 = "hello"; // 栈上(&str 切片类型),不可变
// String 由三部分组成(存储在栈上):
// - 指向堆数据的指针(ptr)
// - 长度(len)
// - 容量(capacity)
移动(Move)------ 核心机制
rust
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权"移动"到 s2
// println!("{s1}"); // 编译错误!s1 已失效
println!("{s2}"); // OK
// 原因:如果 s1 和 s2 都指向同一块堆内存,
// 作用域结束时两者都会尝试释放 → 双重释放 → 内存不安全
// Rust 选择在移动时使原变量失效来避免此问题
克隆(Clone)------ 深拷贝
rust
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝,堆数据独立
println!("s1 = {s1}, s2 = {s2}"); // 两者都有效
Copy Trait ------ 栈上拷贝
rust
let x = 5;
let y = x; // i32 实现了 Copy trait,直接复制值(栈拷贝)
println!("x = {x}, y = {y}"); // 两者都有效
// 实现 Copy trait 的类型(全部存储在栈上):
// - 所有整数类型(i8, u32, usize 等)
// - 所有浮点类型(f32, f64)
// - 布尔 bool
// - 字符 char
// - 元组(仅当所有元素都是 Copy 时,如 (i32, f64))
函数传参与所有权转移
rust
fn main() {
let s = String::from("hello");
takes_ownership(s);
// s 已移动,不可再使用
let x = 5;
makes_copy(x);
// x 仍可用(i32 是 Copy 类型)
}
fn takes_ownership(some_string: String) {
println!("{some_string}");
} // some_string 离开作用域,drop 被调用,内存释放
fn makes_copy(some_integer: i32) {
println!("{some_integer}");
} // some_integer 离开作用域,无特殊操作
函数返回值与所有权转移
rust
fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string // 所有权移动给调用者
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 原样返回所有权
}
fn main() {
let s1 = gives_ownership();
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
// s2 已移动到函数内,不可再用;s3 拿到了返回的所有权
}
2.2 引用与借用(References & Borrowing)
问题:所有权转移导致使用不便
rust
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length) // 必须同时返回所有权,才能继续使用
}
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("'{}' 的长度是 {}", s2, len);
}
解决方案:引用(Reference)
rust
fn calculate_length(s: &String) -> usize { // 借用 String(不获取所有权)
s.len()
}
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 创建指向 s1 的引用
println!("'{}' 的长度是 {}", s1, len); // s1 仍然有效!
}
|----------------------------------|--------------|
| 术语 | 含义 |
| 借用 ( Borrowing ) | 创建一个引用的行为 |
| 借用者 | 拿到引用的函数/变量 |
| 所有者 | 原始变量,始终拥有所有权 |
可变引用(Mutable Reference)
rust
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{s}"); // "hello, world"
}
fn change(s: &mut String) {
s.push_str(", world");
}
可变引用的限制 ------ 同一时刻只能有一个
rust
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // 编译错误!不能同时有两个可变引用
// println!("{r1} {r2}");
// 好处:在编译期防止数据竞争(data race)
// 数据竞争的三个条件(Rust 在编译期全部排除):
// 1. 两个或更多指针同时访问同一数据
// 2. 至少有一个指针在写入
// 3. 没有同步机制
不可变引用 vs 可变引用的互斥
rust
let mut s = String::from("hello");
let r1 = &s; // 不可变引用 OK
let r2 = &s; // 多个不可变引用 OK
println!("{r1} and {r2}");
// r1, r2 的最后一次使用结束于此
let r3 = &mut s; // 可变引用 OK(r1/r2 已不再使用)
println!("{r3}");
// 关键:NLL(Non-Lexical Lifetimes,非词法生命周期)
// 作用域结束 ≠ 引用失效,引用在最后一次使用后就失效了
悬垂引用(Dangling References)------ Rust 在编译期防止
rust
// 这段代码在 Rust 中无法编译(其他语言允许,是常见 bug 源)
fn dangle() -> &String {
let s = String::from("hello");
&s // 返回指向局部变量的引用
} // s 在此被 drop,引用变成悬垂指针
// 编译器报错:missing lifetime specifier
// 正确写法:直接返回所有权
fn no_dangle() -> String {
let s = String::from("hello");
s // 所有权移动出去,不会有悬垂引用
}
引用规则总结
1. 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用
2. 引用必须始终有效(不能悬垂)
2.3 切片(Slices)
字符串切片
rust
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
let whole = &s[..]; // 等价于 &s[0..s.len()]
let half = &s[..5]; // 等价于 &s[0..5]
// 切片类型:&str(字符串字面量也是 &str)
let s: &str = "hello"; // 类型是 &str(不可变)
字符串切片的危险性 ------ UTF-8 边界
rust
let s = String::from("你好");
// let h = &s[0..1]; // 运行时 panic!'你' 占 3 字节,不是 1 字节
// 正确做法:
let h = &s[0..3]; // "你"(3 字节)
函数参数中的字符串切片
rust
// 接受 &String(仅持有 String 引用时可用)
fn first_word(s: &String) -> &str { ... }
// 接受 &str(&String 和字符串字面量都能用!更通用)
fn first_word(s: &str) -> &str { ... }
fn main() {
let s = String::from("hello world");
let word = first_word(&s); // 传入 &String,自动解引用为 &str
let word2 = first_word("literal"); // 传入 &str 字面量,也 OK
}
最佳实践:函数参数优先使用 &str****而非 &String**,更通用。**
数组切片
rust
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // &[i32] 类型,值为 [2, 3]
切片在内存中的表示
String:
┌─────────┬─────┬───────────┐
│ ptr │ len │ capacity │ ← 栈上
├─────────┼─────┼───────────┤
│ │ │ │
│ ┌───────────────────┐ │
│ │ h e l l o \0 │ │ ← 堆上
│ └───────────────────┘ │
└─────────┼─────┼───────────┘
&str 切片:
┌─────┬─────┐
│ ptr │ len │ ← 栈上(没有 capacity!)
└─────┴─────┘
│
└──→ 指向 String 堆数据的一部分
2.4 所有权决策树(速查)
需要传递数据给函数吗?
├── 函数需要修改数据?
│ └── 是 → 传 &mut T(可变引用)
└── 函数只需要读取?
├── 数据较小(Copy 类型)→ 传值(自动复制)
└── 数据较大 → 传 &T(不可变引用)
需要从函数返回数据吗?
├── 在函数内创建的数据 → 直接返回(所有权转移)
└── 调用方仍需持有 → 先 clone 再传
不确定该用什么?
→ 默认用引用(&T),编译器会告诉你何时需要所有权
学习目标检验
- 能清晰解释所有权三条铁律
- 能区分"移动"和"复制",知道哪些类型是 Copy
- 能解释为什么 Rust 要禁止悬垂引用
- 能写出正确的可变引用和不可变引用组合
- 理解 NLL(非词法生命周期)的含义
- 能正确使用字符串切片
&str作为函数参数 - 理解
Stringvs&str的区别