所有权
所有权规则
- 每个值都有一个变量,这个变量是该值的所有者
- 每个值在任意时刻有且只能有一个所有者
- 当所有者超出作用域(scope)时,该值将被删除(调用
drop并回收内存) - 所有权可以转移(move)
- 赋值/传参/函数返回时,如果类型未实现
Copy,所有权将发生转移 - 基本数据类型(如整数、布尔、浮点、不可变引用
&T)实现了Copy,会自动复制,不会转移所有权,设计原因与性能有关
核心特性: Rust 通过所有权机制在编译时保证内存安全,避免了垃圾回收开销和野指针问题
复制 copy 和移动 move
1. 基本数据类型
存储在 stack 上,赋值给其他变量不会转移所有权,因为实现了 Copy
rust
let a = 1;
let b = a; // 直接进行了复制操作,没有发生所有权转移
println!("{a}, {b}"); // 不会出现错误基本数据类型自动实现了
Copy,本质也遵循所有权规则,只是编译器自动进行了复制原因: 复制这些微小数据(基本数据类型)的开销甚至比处理它们的指针引用还要低,因此编译器默认开启自动复制
2. 复杂数据类型
存储在 heap 上,直接赋值给变量会转移所有权
rust
let x = String::from("hello");
let y = x; // 浅拷贝 + x 失效,x 此时移动所有权到 y;修复: 1.let y = x.clone() 或 2.let y = &x
println!("{x}, {y}"); // 报错: x value borrowed here after move
let y = x这一步是将 stack 里的指针(ptr)、数据长度(len)、数据总大小(capacity)这些信息复制给 y(浅拷贝),并且将 x 回收,避免最后二次释放同一个指针的数据(二次释放)本质是在没有垃圾回收器的情况下,实现垃圾回收的一种解决方案,保证内存安全
3. 函数传参
- 同样遵循前所有权规则,不是复制就是移动或者借用(引用传递)
rust
fn main() {
let s = String::from("hello");
takes_ownership(s); // s 的所有权被转移到函数内,函数结束后 s 被释放
// println!("{s}"); // 报错: s value borrowed here after move
let x = 5;
makes_copy(x); // x 是基本数据类型,被复制到函数内,函数结束后 x 仍然有效
println!("{x}"); // 正常输出
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 离开作用域,被释放
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integer 离开作用域,被释放4. 函数返回值
- 同样会发生所有权的转移
rust
fn main() {
let s1 = gives_ownership(); // s1 从函数获得所有权
println!("{s1}"); // 正常输出
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 的所有权被转移到函数内,函数返回后 s3 获得所有权
println!("{s3}"); // 正常输出
fn gives_ownership() -> String {
let some_string = String::from("hello");
some_string // 返回 some_string 的所有权
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 返回 a_string 的所有权
}
}克隆 Clone
- 主要针对
heap数据的深拷贝(heap 生成 2 份相同的数据,stack 上 2 份不同的指针指向自己的 heap),数据较大时比较消耗资源
克隆字符串
rust
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝,生成两份独立的 heap 数据
println!("{s1}, {s2}"); // 正常输出引用 Reference: &
是一个语法概念,指的是引用某些值而不获得所有权
- 本质是创建一个指向原数据的指针
- 引用本身是一个指针,占用固定大小的内存空间
- 通过引用访问数据时,编译器会自动解引用,因此无需显式使用
* - 变量被引用后,原变量就处于锁定状态,无法再直接使用,需要等引用结束后才能使用
语法:
&: 不可变引用,不可修改变量的内容&mut: 可变引用,可以修改变量的内容,同作用域内对同一变量独占引用(不可与其他引用共存)
rust
// 不可变引用示例
let x = String::from("hello");
let y = &x; // y 是对 x 的引用,x 仍保有所有权
println!("{x}, {y}"); // 正常输出,x 依然有效
// 可变引用示例
let mut s = String::from("hello");
let r1 = &mut s; // r1 是 s 的可变引用
*r1 = String::from("world"); // 通过 r1 修改 s 的内容
println!("{s}"); // 输出 "world",s 的内容被修改引用 vs 所有权转移: 引用只是借用,原变量保持所有权;而直接赋值会转移所有权
借用
是使用引用的一种行为,指的是通过引用来访问数据而不获取所有权的过程,当你创建了一个引用,就是在借用数据
借用规则
同作用域、同变量时,只能满足下列条件之一:
- 一个可变引用(防止数据竞争)
- 无限个不可变引用
rust
let mut x = 5;
let r1 = &x; // 不可变引用
let r2 = &x; // 不可变引用(√ 可以多个)
let r3 = &mut x; // ✗ 错误: 不能同时有可变和不可变引用引用位置语法
在函数定义中,& 永远加在类型前面,而不是变量名前面:
rust
// ✓ 正确: & 加在类型 T 前面
fn execute<T>(item: &T) { ... }
// ✗ 错误: 不能加在变量名前面
// fn execute<T>(&item: T) { ... }解引用 *
- 解引用后可以读取到真实的原数据
- 这是编译器提供的语法糖,背后是通过实现
Deref Trait来实现的 - 对不可变引用解引用:
*y获取原数据的不可变访问 - 对可变引用解引用:
*y可以修改原数据
rust
let x = 5;
let y = &x;
println!("{}", *y); // 输出 5,解引用获取原数据
let mut a = 10;
let b = &mut a;
*b = 20; // 解引用并修改原数据
println!("{}", a); // 输出 20自动解引用: 在大多数情况下,编译器会自动解引用.例如调用方法时,无需显式使用
*;但在某些场景(如数值比较、打印输出)需要显式解引用
重借用
从已有的可变引用创建新的引用(重借用)也是允许的,但原始变量也必须是 mut 的,也必须遵守借用规则
rust
let mut s = String::from("hello");
let r1 = &mut s; // 可变引用
let r2 = &mut *r1; // 重借用 r1 创建 r2,也是可变引用
*r2 = String::from("world"); // 通过 r2 修改 s 的内容
// *r1 = String::from("world123"); // 错误: 遵循借用规则,不能同时有多个可变引用
println!("{s}"); // 输出 "world",s 的内容被修改这里可以通过非词法作用域生命周期规则来修改代码
悬垂引用 Dangling Reference
指引用指向一个已被释放或已重分配的内存地址
- Rust 编译器会防止悬垂引用的产生,在编译阶段就捕获这类错误
- 如果函数返回一个引用,该引用必须指向有效的数据
rust
// ✗ 错误示例: 悬垂引用
fn dangle() -> &String {
let s = String::from("hello");
&s // 错误: s 在函数结束时被释放,返回的引用指向已释放内存
}
// ✓ 修复: 返回所有权而不是引用
fn no_dangle() -> String {
let s = String::from("hello");
s // 转移所有权给调用者
}编译器保证: Rust 的生命周期检查确保引用总是指向有效的数据,不会产生悬垂引用问题