16.1 案例研究:生命周期与借用 🟢
在这个案例研究中,我们将探讨 Rust 的生命周期和借用规则如何被用来防止在 C 和 C++ 中常见的内存错误。
1. 问题:使用后释放 (Use-After-Free)
在 C++ 中,很容易意外地使用一个对已经被销毁的对象的引用或指针。
#include <iostream>
#include <string>
std::string& get_greeting() {
std::string s = "你好";
return s; // 错误:返回对局部变量的引用
}
int main() {
std::string& greeting = get_greeting();
std::cout << greeting << std::endl; // 使用后释放!
}
2. Rust 的解决方案:生命周期 (Lifetimes)
在 Rust 中,编译器使用生命周期来确保引用永远不会比它们指向的数据存活得更长。
fn get_greeting() -> &String {
let s = String::from("你好");
&s // 错误:`s` 存活时间不够长
}
fn main() {
let greeting = get_greeting();
println!("{}", greeting);
}
Rust 编译器将在编译时捕获此错误,从而防止潜在的崩溃或安全漏洞。
3. 借用与可变引用 (Borrowing and Mutable References)
Rust 的借用规则还通过确保在任何给定时间你只能拥有任意数量的不可变引用,或恰好一个对某段数据的可变引用,来防止数据竞态。
fn main() {
let mut s = String::from("你好");
let r1 = &s; // 不可变借用
let r2 = &s; // 另一个不可变借用(正确)
// let r3 = &mut s; // 错误:不能将 `s` 借用为可变,因为它已经被借用为不可变了
println!("{}, {}", r1, r2);
}
4. 现实世界的影响
通过在编译时强制执行这些规则,Rust 消除了一整类在大型 C/C++ 代码库中众所周知难以追踪的 Bug。这允许开发人员专注于构建功能,而不是调试与内存相关的问题。
对于 C/C++ 开发者的总结
- 在 C/C++ 中:你必须手动跟踪对象生命周期,并确保你对指针和引用的使用是安全的。Valgrind 和 AddressSanitizer 等工具可以提供帮助,但它们只能在运行时捕获错误。
- 在 Rust 中:编译器就是你的“静态分析工具”,它在每次构建时都会运行。它迫使你预先思考所有权和生命周期,从而从一开始就编写出更健壮、更可靠的代码。