作为程序员都知道,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 mutableprintln!("{}", 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 程序的关键。