作为程序员都知道,C/C++ 语言是手动内存管理,开发者需要手动的申请和释放内存资源。Java 语言是自动内存管理,开发者无须手动释放资源,但是会带来额外的性能开销,例如需要在内存识别哪些对象需要释放,以及内存碎片化等问题。

Rust 采用了一种独特的内存管理模式,它结合了编译时检查和运行时效率优化,以确保内存安全而无需垃圾回收器。Rust 引入所有权的概念。

所有权的三个原则

每个值都有一个所有者:在 Rust 中,每个值(如变量、数据结构等)都有一个变量作为其所有者,负责该值的内存管理。

fn main() {
let s = String::from("kelen.cc"); // s 是 "hello" 的所有者
}

同一时间只能有一个所有者:一个值在任何时候只能有一个所有者,这确保了内存的安全性和避免数据竞争。

fn main() {
let s1 = String::from("kelen.cc");
let s2 = s1; // 所有权从 s1 移动到 s2
// println!("{}", s1); // 这行会导致编译错误,因为 s1 不再拥有值
println!("{}", s2); // 这行可以正常工作,因为 s2 现在是所有者
}

如果执行 println!("{}", s1),则编译报错 borrow of moved value: s1

当所有者离开作用域时,值会被丢弃:当拥有值的变量离开其作用域时,Rust 会自动调用该值的 drop 方法来释放内存。

fn main() {
{
let s = String::from("kelen.cc"); // s 进入作用域
} // s 离开作用域,内存被自动释放
println!("{}", s); // 这行会导致编译错误,因为 s 已经离开作用域
}

上述代码会报错 help: the binding s is available in a different scope in the same function

引用

引用可以使用值而不获得所有权,有两种引用类型使用方法,分别是可变引用 &mut T 和不可变引用 &T

可变引用&mut T

可变引用允许你修改数据。然而,对于一个特定的数据,如果已经有了一个可变引用,那么就不能再创建任何其他的引用,直到这个可变引用不再被使用为止。这保证了在同一时间只有一个地方能够改变数据。

fn main() {
let mut y = 10;
{
let r = &mut y; // 创建一个可变引用
*r += 5; // 修改可变引用指向的数据
} // 可变引用在此作用域结束,y再次可用
println!("y的值: {}", y);
}

错误例子:

fn main() {
let mut y = 10;
let r = &mut y; // 创建一个可变引用
let r2 = &mut y; //报错: second mutable borrow occurs here
println!("{}", r);
}

不可变引用&T

不可变引用允许你读取数据但不能修改它。当你有一个不可变引用时,你可以有任意数量的其他不可变引用指向同一数据,但是不能有任何可变引用同时存在。这是为了确保线程安全和数据竞争不会发生。

let x = 5;
let r1 = &x; // 创建一个不可变引用
let r2 = &x; // 可以创建多个不可变引用
println!("r1: {}, r2: {}", r1, r2);

错误例子:

let x = 5;
let r1 = &x; // 创建一个不可变引用
let r2 = &x; // 创建一个不可变引用,r1和r2指向同一个地址
let r3 = &mut x; // 报错: cannot borrow `x` as mutable, as it is not declared as mutable
println!("{}", r1);

悬空引用

悬空引用(dangling reference)是指一个引用指向了已经被释放的内存位置。访问悬空引用所指向的内存可能会导致程序崩溃或产生未定义行为。

简单来说就是引用已经被内存释放了,再用这个引用变量就会报错。

fn main() {
let reference_to_nothing: &i32;
{
let x = 42; // x 在这个作用域中创建
reference_to_nothing = &x; // 引用 x
} // x 在这里离开作用域,被释放
println!("{}", reference_to_nothing); // 试图使用悬空引用
}

上述的代码会报错:x does not live long enough

借用

借用就是使用引用访问变量的值而不获取其所有权。

借用规则

  • 在任何时候,可以有多个不可变引用或一个可变引用。
  • 引用必须始终有效,不能指向已释放的内存。

所有权规则确保每个值在离开作用域时被正确释放,借用规则确保在借用期间数据不会被意外修改或释放。借用也区分可变借用和不可变借用。

不可变借用

不可变借用允许你读取数据但不能修改它。你可以拥有任意数量的不可变引用指向同一数据,只要没有可变引用存在。

fn main() {
let s1 = String::from("kelen.cc");
let len = calculate_length(&s1);
println!("'{}'的长度是{}", s1, len); // 'kelen.cc的长度是8
}
// `calculate_length` 接收一个字符串的不可变引用
fn calculate_length(s: &String) -> usize {
s.len()
}

错误的用法:

fn main() {
let s1 = String::from("kelen.cc");
let len = calculate_length(&s1);
println!("'{}'的长度是{}", s1, len);
}
fn calculate_length(s: &String) -> usize {
*s = String::from("hello"); // 报错: `s` is a `&` reference, so the data it refers to cannot be written
s.len()
}

可变借用

可变借用允许你修改数据。然而,在任何给定时间点,你只能拥有一个可变引用,并且此时不能有任何不可变引用存在。

fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s; // 创建一个可变引用
*r1 = String::from("world"); // 修改数据
} // `r1` 的作用域在这里结束
// `r1` 作用域结束后,可以创建新的引用
let r2 = &s;
println!("{}", r2); // 输出world,因为在r1的作用域结束后,s的值被修改为world
}

错误的用法:

fn main() {
let mut s = String::from("hello");
let r = &mut s; // 创建一个可变引用
{
let r1 = &mut s; // 报错: cannot borrow `s` as mutable more than once at a time`
*r1 = String::from("world");
}
let r2 = &s; // 报错: cannot borrow `s` as immutable because it is also borrowed as mutable
println!("{}, {}", r, r2);
}

总结

Rust 的所有权系统、引用和借用机制共同作用,确保了内存的安全性和性能。通过严格的所有权规则和编译时的借用检查,Rust 避免了常见的内存错误,如空指针、数据竞争和内存泄漏。理解这些核心概念是编写安全、高效的 Rust 程序的关键。