为什么要学 Rust
C++ 写多了,总会遇到这些问题:
- 悬空指针、野指针
- 内存泄漏
- 数据竞争(data race)
- 未定义行为(UB)
Rust 从语言层面就帮你杜绝了这些。它的所有权系统(Ownership)是核心——编译期检查,零运行时开销。
本文用 C++ 对比着讲 Rust,帮你快速上手。
基础类型:差不多
// C++
int x = 42;
double pi = 3.14;
bool flag = true;
char c = 'A';
auto* ptr = new int(42);
// Rust
let x: i32 = 42;
let pi: f64 = 3.14;
let flag: bool = true;
let c: char = 'A';
let mut ptr = Box::new(42); // 堆分配,Box 就是智能指针
关键区别:
- Rust 变量默认不可变,
let mut x才是可变的 i32、f64、bool这些名字和 C++ 相似- 没有裸指针,但有
*const T和*mut T(安全受限)
所有权:核心概念
这是 Rust 和 C++ 最核心的区别。
C++ 的问题
int* create_buffer() {
int* buffer = new int[100];
// ... 处理 ...
return buffer; // 谁负责释放?
}
void process() {
int* data = create_buffer();
// 用完了
delete[] data; // 忘了 delete 就内存泄漏
}
Rust 的解决方案:所有权转移
fn create_buffer() -> Vec<i32> {
let buffer = vec![0; 100]; // Vec 是堆数组,类似 std::vector
buffer // 所有权转移给调用方,函数结束后 buffer 不被 drop
}
fn process() {
let data = create_buffer();
// 用完了,data 离开作用域,自动释放(drop)
} // data 在这里被 drop,无需手动管理
Rust 三条规则:
- 每个值有且只有一个所有者(Owner)
- 值离开作用域时,自动调用
drop(类似析构函数) - 所有权可以转移(Move),转移后原变量失效
Move vs Copy
// 栈上类型:Copy,自动复制
let x: i32 = 42;
let y = x; // 复制,x 仍然有效
println!("{} {}", x, y);
// 堆上类型:Move,转移所有权
let v1 = vec![1, 2, 3]; // Vec 在堆上
let v2 = v1; // Move,v1 失效
// println!("{:?}", v1); // 编译错误!v1 已无效
// C++ 对比:vector 也是 Move 语义
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); // v1 失效
// std::cout << v1.size(); // UB 或空
借用(Borrowing):不用所有权的引用
每次都转移所有权太麻烦了,用引用借用一下:
fn print_length(s: &String) { // & 表示借用,不获取所有权
println!("Length: {}", s.len());
} // s 离开作用域,但值不被 drop,因为只是借用
fn main() {
let s = String::from("hello");
print_length(&s); // 传引用,不转移所有权
println!("{}", s); // s 仍然有效
}
借用规则(编译器强制):
- 要么多个不可变引用
&T - 要么一个可变引用
&mut T - 不能同时有可变引用和不可变引用
这从根本上避免了数据竞争!
let mut v = vec![1, 2, 3];
let r1 = &v; // OK
let r2 = &v; // OK,多个不可变引用可以共存
// let r3 = &mut v; // 错误!有不可变引用时不能可变借用
println!("{:?} {:?}", r1, r2);
结构和枚举:C++ 的 struct/class + enum
Struct
// C++
struct Point {
int x;
int y;
};
Point p = {10, 20};
p.x = 30;
struct Point {
x: i32,
y: i32,
}
let mut p = Point { x: 10, y: 20 };
p.x = 30; // 如果没有 mut,这里会报错
Enum:C++ 的 enum 升级版
C++ 的 enum 很弱,Rust 的 enum 强大得多:
// 枚举可以带数据
enum Message {
Quit, // 无数据
Move { x: i32, y: i32 }, // 匿名结构体
Write(String), // 带一个 String
ChangeColor(i32, i32, i32), // 多个值
}
fn process(msg: Message) {
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to {} {}", x, y),
Message::Write(text) => println!("Write: {}", text),
Message::ChangeColor(r, g, b) => println!("Color {} {} {}", r, g, b),
}
}
这其实就是代数数据类型(ADT),C++ 得用继承 + 虚函数 + visitor 模式才能实现类似效果。
Option:替代 null
C++ 的 null 是万恶之源:
// C++:不知道返回的是不是 null
User* find_user(int id) {
if (found) return &user;
else return nullptr; // 调用方可能忘记检查
}
auto* user = find_user(123);
user->do_something(); // 如果返回 nullptr,UB
Rust 没有 null,用 Option<T>:
enum Option<T> {
Some(T), // 有值
None, // 没有值
}
fn find_user(id: i32) -> Option<User> {
if found {
Some(user)
} else {
None
}
}
match find_user(123) {
Some(user) => user.do_something(),
None => println!("User not found"),
}
// 或者用 if let
if let Some(user) = find_user(123) {
user.do_something();
}
编译器强制你处理 None 的情况,想忽略?用 unwrap() 但这只是把问题延迟到运行时。
错误处理:Result 替代异常
C++ 用异常处理错误,问题是你不知道会抛什么异常:
try {
auto result = do_something();
result.process();
} catch (const std::exception& e) {
// 捕获所有异常,但不知道具体是哪种
log_error(e.what());
} catch (...) {
// 捕获未知异常...
}
Rust 用 Result<T, E>:
enum Result<T, E> {
Ok(T),
Err(E),
}
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut file = File::open(path)?; // ? 操作符:错误自动向上返回
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
fn main() {
match read_file("config.toml") {
Ok(contents) => println!("{}", contents),
Err(e) => eprintln!("Error: {}", e),
}
}
? 操作符是灵魂:如果是 Err,立即返回;如果是 Ok,取出值继续。
生命周期:Rust 的 borrow checker 加强版
C++ 的引用需要程序员自己保证有效性:
int& bad_reference() {
int x = 10;
return x; // 悬空引用!离开函数 x 就没了
}
Rust 用生命周期标注显式声明引用有效期:
// 告诉编译器:返回值的生命周期和输入参数相同
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
大多数时候编译器能自动推断,只有多参数、返回值有引用时才需要标注。
线程安全:C++ 的噩梦,Rust 的强项
C++ 数据竞争:
std::thread t1([&counter] {
for (int i = 0; i < 1000; ++i) {
++counter; // 数据竞争!三个线程同时读改写
}
});
Rust 的解决:Send + Sync trait 保证线程安全:
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
Arc<Mutex<T>>:
Arc:原子引用计数,多线程共享Mutex:互斥锁,保护数据- 锁的
lock()返回Result,必须处理
想数据竞争?编译不过。
泛型:比 C++ 模板更安全
// C++ 模板
template<typename T>
T max(T a, T b) {
return a > b ? a : b; // 编译期实例化,错误信息常常看不懂
}
// Rust 泛型
fn max<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
PartialOrd 是 trait(类似 C++ concept),约束 T 必须支持比较操作符。缺少约束时,编译期报错清楚明确。
实战对比:写一个简单 HTTP Server
C++(用 libcurl)
#include <curl/curl.h>
#include <iostream>
int main() {
CURL* curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_URL, "https://api.example.com/data");
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
return 0;
}
Rust(用 reqwest)
use reqwest;
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let resp = reqwest::get("https://api.example.com/data").await?;
println!("Status: {}", resp.status());
let body = resp.text().await?;
println!("Body: {}", body);
Ok(())
}
关键区别:
- Rust 版本有编译期保证:错误类型清晰,必须处理
- C++ 版本依赖返回值检查,容易遗漏
- Rust 用 async/await 并发模型,比 C++ 的回调简洁得多
C++ → Rust 速查表
| C++ | Rust | 说明 |
|---|---|---|
T* ptr |
&T / &mut T |
引用,借用 |
std::unique_ptr<T> |
Box<T> |
独占所有权 |
std::shared_ptr<T> |
Arc<T> |
引用计数共享 |
std::lock_guard |
Mutex<T> |
互斥锁 |
nullptr |
Option<T> |
空值 |
throw |
Result<T, E> |
错误处理 |
class |
struct / enum |
类型定义 |
| 异常 | Result + ? |
错误传播 |
| 模板 | 泛型 + trait | 多态 |
总结
Rust 学什么:
- 所有权系统:不用 GC,不用手动 free,编译器保证
- 借用规则:从根本上避免数据竞争
- Result + Option:告别 null 和异常
- 编译器是你的朋友:编译错误信息非常详细,照着改就行
入门曲线比 C++ 陡,但:
- 没有悬空指针
- 没有数据竞争
- 没有未定义行为
- 性能一样好(零成本抽象)
值得投入。
后续计划写 Rust 异步编程(Tokio)和生命周期详解,敬请期待。