缘起
从 C++ 转 Rust,最不适应的不是语法,而是一种全新的思维模式。
Rust 的所有权系统(Ownership)是语言最核心的创新,也是最陡峭的学习曲线。
C++ 的惯性思维
在 C++ 里,我们习惯了这样的写法:
std::string get_name() {
return "BvBeJ"; // 编译器会处理返回值优化
}
void process() {
std::string name = get_name();
std::string alias = name; // 拷贝?还是引用?
// ...
} // name 和 alias 都会析构
直觉告诉我们这里发生了拷贝。但在 Rust 里,同样的思维会让你碰壁。
Rust 的所有权规则
Rust 遵循三条简单规则:
- 每个值有一个所有者(Owner)
- 同一时间只有一个所有者
- 当所有者离开作用域,值被丢弃(Dropped)
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 被"移动"到 s2
// println!("{}", s1); // ❌ 编译错误!s1 已经无效
println!("{}", s2); // ✅
} // s2 离开作用域,内存被释放
“移动"语义取代了 C++ 的拷贝——这是最大的思维转变。
借用:不用所有权的艺术
如果每次都要转移所有权,编程会非常麻烦。于是有了借用(Borrow):
fn calculate_length(s: &String) -> usize {
s.len()
} // s 离开作用域,但并不拥有所有权,所以不释放
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 借用,而不是移动
println!("'{}' 的长度是 {}", s1, len); // ✅ s1 仍然有效
}
借用就像是指针,但更安全。Rust 编译器会确保:
- 要么多个不可变引用
&T - 要么一个可变引用
&mut T - 二者不能同时存在
可变引用的风险
fn main() {
let mut s = String::from("hello");
let r1 = &s; // ✅
let r2 = &s; // ✅
println!("{} and {}", r1, r2);
// r1 和 r2 在这里之后不再使用
let r3 = &mut s; // ✅ 没有问题,因为 r1, r2 已经不再使用
r3.push_str(" world");
}
这叫非词法作用域生命周期(Non-Lexical Lifetimes, NLL),Rust 2018 edition 引入的优化。
实际例子:用 Rust 重写 C++ 的配置解析
// C++ 版本
class Config {
std::string path_;
std::unique_ptr<Parser> parser_;
public:
Config(const std::string& path) : path_(path) {
parser_ = std::make_unique<Parser>(path_);
}
};
// Rust 版本
struct Config {
path: String,
}
impl Config {
fn new(path: &str) -> Result<Self, Box<dyn Error>> {
let path = path.to_string();
// 不需要 Box<dyn Parser>,直接在作用域内处理
let _parser = Parser::new(&path)?;
Ok(Config { path })
}
}
不需要 Box、unique_ptr,Rust 编译器帮我们管理生命周期。
我的心得
| C++ | Rust |
|---|---|
手动管理 new/delete | 编译器推导生命周期 |
shared_ptr 到处飞 | 借用检查器静态保证 |
| 悬垂指针是运行时bug | 编译时就能发现 |
| 析构顺序靠经验 | 作用域决定析构 |
Rust 教会我一件事:很多运行时错误,可以在编译时避免。
代价是学习曲线陡峭。但一旦理解了这套系统,你会对内存管理有全新的认识。
下一篇文章聊聊 Rust 的 Trait 对象与动态分发。