跳转至

Rust 入门(写给 C++ 开发者)

Rust 与 C++ 目标相似:无垃圾回收、贴近硬件、可写系统软件。差别在于 Rust 把内存安全前移到了编译期——悬垂指针、越界、数据竞争在默认情况下无法通过编译。本篇按 C++ 的心智模型对照展开。示例在 WSL rustc/cargo 1.75 编译运行验证。

安装与第一个程序

# 推荐 rustup(官方多版本管理器);发行版 apt 的版本通常滞后
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo --version
cargo new hello && cd hello       # 生成 Cargo.toml + src/main.rs
cargo run                         # 编译并运行
cargo build --release             # 优化构建(对应 -O2)
cargo test                        # 运行内置测试框架

Cargo.toml 之于 Rust 就是 CMakeLists.txt + 依赖清单:

[package]
name = "hello"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = "0.8"                      # 一行加依赖,cargo build 时自动拉取

变量与类型:默认不可变

let x = 5;                  // 默认不可变(const 的语感,但有类型推导)
let mut y = 10;             // mut 才可以改
y += 1;
let z: f64 = 2.5;           // 显式类型;推导写 let z = 2.5;
let (a, b) = (1, "text");   // 解包元组

const MAX: u32 = 100_000;   // 编译期常量;数字分隔符与 C++14 相同

所有权:与 C++ 移动语义的本质区别

C++ 的移动是把"搬走后的对象"留在一个合法但未指定的状态,编译器不阻止你继续用它。Rust 的规则更严格:

let s1 = String::from("hello");
let s2 = s1;                  // 所有权移动(move),等价 C++ 的 std::move
// println!("{}", s1);        // ← 编译错误:s1 已失效,不存在"移动后可用"
let s3 = s1.clone();          // 真正深拷贝要显式写
let n1 = 5;
let n2 = n1;                  // 整数是 Copy 类型,按位复制,n1 仍可用

三条所有权规则:

  1. 每个值有唯一的所有者(变量);
  2. 同一时刻只能有一个所有者;
  3. 所有者离开作用域,值被释放(对应 C++ 的 RAII,析构函数这里叫 Drop)。

函数传参与返回同样移动所有权——所以下面的代码无法编译,这是新手遇到的第一堵墙:

fn length(s: String) -> usize { s.len() }

let s = String::from("hi");
// let n = length(s);        // 所有权交进函数后,s 在外层就没了
// println!("{}", s);        // ← 编译错误

借用:不交出所有权的访问

不想转移所有权时,用引用(借用):

fn length(s: &String) -> usize { s.len() }     // 只读借用

let s = String::from("hi");
let n = length(&s);            // 借出去,s 依然可用
println!("{} {}", s, n);       // 合法

借用规则(编译期检查,这就是"没有数据竞争"的来源):

  • 任意数量的不可变借用 &T,或者恰好一个可变借用 &mut T;
  • 二者不能同时存在——有写者时禁止读者,数据竞争在编译期被判死刑。

对比 C++:const T& ≈ &T;T& ≈ &mut T,但 C++ 不检查"写者独占",Rust 检查。代价是要改变习惯:遍历中修改集合这类操作需要重新设计(例如先收集下标再改)。

enum 与 match:比 switch 强得多的分支

enum Shape {
    Circle(f64),                       // 变体可以携带数据
    Rectangle(f64, f64),
    Point,
}

fn area(s: &Shape) -> f64 {
    match s {
        Shape::Circle(r) => 3.14159 * r * r,           // 解构出携带的值
        Shape::Rectangle(w, h) => w * h,
        Shape::Point => 0.0,                           // 必须穷尽所有变体
    }
}

match 要求覆盖全部情况(少一个变体编译失败),还能匹配区间、元组、多重模式。Option<T>(替代空指针)和 Result<T, E>(替代错误码/异常)是两个最重要的标准库枚举:

let maybe: Option<i32> = Some(3);      // 有值:Some(3);无值:None
match maybe {
    Some(v) => println!("got {}", v),
    None => println!("nothing"),       // 编译器强制处理 None 分支——空指针解引用在此绝迹
}

错误处理:Result 与 ? 运算符

Rust 没有异常。可能失败的操作返回 Result<T, E>(成功 Ok,失败 Err,语义同 C++ 的 std::expected):

use std::num::ParseIntError;
use std::fs;

fn read_number(path: &str) -> Result<i32, ParseIntError> {
    let text = fs::read_to_string(path)?;    // Err 时立即向上返回,Ok 时解出值
    let n = text.trim().parse::<i32>()?;     // ? 链式传播,层层少写代码
    Ok(n)
}

? 运算符就是 std::expected monadic 风格的语言级内置——错误传播从模板技巧变成了一等语法。

集合与迭代器

let mut v = vec![1, 2, 3];                 // Vec<T> ≈ std::vector
v.push(4);

let doubled: Vec<i32> = v.iter()           // 迭代器 ≈ C++20 ranges 管道
    .filter(|&&x| x % 2 == 0)
    .map(|&x| x * 2)
    .collect();                            // [4, 8]

for x in &v { println!("{}", x); }         // &v 借用遍历;for x in v 则移动

内置测试

fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);          // assert_eq!/assert_ne!/assert!
    }
}

cargo test 自动发现并运行——不需要第三方测试框架(对应 GoogleTest 与 pytest)。

与 C++ 概念对照表

C++ Rust 备注
std::move 后原对象状态未指定 move 后编译器禁止使用 安全性的来源
RAII / 析构函数 RAII / Drop trait 思想一致
const T& / T& &T / &mut T Rust 强制写者独占
std::unique_ptr 默认就是独占所有权 不需要智能指针
std::shared_ptr Rc<T> / Arc<T> Arc = 原子引用计数
std::optional / std::expected Option<T> / Result<T, E> 语言级支持 + ?
模板 / concepts 泛型 / trait trait ≈ concepts + 接口
继承 + 虚函数 trait 对象 / 泛型 Rust 没有继承,组合优先
#include / 模板实例化 crate + 模块系统 无头文件,编译模型完全不同
gtest / pytest 内置 #[test] 标准库自带
CMake + vcpkg cargo 一条命令管构建、测试、依赖、发布

常见坑

问题 说明
String vs &str String 是拥有所有权的字符串(≈ std::string),&str 是字符串切片(≈ string_view);函数参数优先收 &str
整型溢出 debug 构建直接 panic,release 静默回绕——跨平台代码别依赖回绕行为,显式用 checked_add 等
未使用变量警告 let _x = ... 或 _ = ... 显式忽略
与 C++ 互操作 unsafe + FFI 块;安全边界在 unsafe 段内由程序员负责
克隆成瘾 到处 .clone() 能让编译器闭嘴,但违背学习目的——先想清楚所有权该归谁

继续走

评论