Rust OS 前置:从 C 到 Rust——变量、类型与 struct
学 Rust OS 时会密集碰到
struct、引用、裸指针、unsafe、生命周期这些概念。如果此前没有
Rust 基础,直接读 OS 代码会很吃力。这篇先用 C/C++
作对照,把你已经会的东西在 Rust 里怎么写过一遍,建立一张 Rust 地图。
本文是 Rust OS 学习系列的前置篇。后续会接着讲引用与借用、属性与内存布局,并把它们综合到《Rust OS 入门:VGA 字符模式》的
Writer代码上。
📚 基本概念速读
| 名称 | 含义 | 省流 |
|---|---|---|
let |
声明变量 | 默认不可变 |
let mut |
声明可变变量 | 想改就加 mut |
: T |
变量名后写类型 | let x: u8 = 10; |
struct |
把多个数据组织在一起 | 类似 C 的 struct |
impl |
给 struct 添加方法 | 类似 C++ 的 class 方法 |
pub |
允许模块外访问 | 默认模块内私有 |
[T; N] |
定长数组类型 | [[ScreenChar; 80]; 25] |
match |
模式匹配 | 更强大的 switch |
Option<T> |
有值/无值 | Some(v) / None |
Result<T, E> |
成功/失败 | Ok(v) / Err(e) |
trait |
约定一组方法,类型去实现它 | 类似 interface |
<T> |
泛型:类型当参数 | Volatile<ScreenChar> |
enum |
一组固定的选项,变体还能带数据 | Option / Result 就是它 |
for / 迭代器 |
遍历一列值 | for x in 0..4 |
mod / use |
分组代码、给路径起短名字 | 对应 C 的 #include + 命名空间 |
🗺️ 先建一张 Rust 地图
如果熟悉 C,Rust 可以这样对照着看:
1 | C |
Rust 也有这些东西,只是在它们之上加了一套所有权与借用的思想:
1 | Rust |
学习 OS 时的最大难点往往不是 Rust 语法本身,而是所有权 + 借用 + 引用 + 生命周期这套思想。本篇先讲 C 里也有、只是写法不同的部分。
📦 let:变量与不可变性
Rust 声明变量用 let:
1 | let x = 10; // 类型自动推断为 i32 |
可以对照 C 的 int x = 10;。语法上类型写在变量名后:
1 | let x: u8 = 10; |
Rust 默认变量不可变——这是和 C 的一个关键差异:
1 | let x = 10; |
要修改必须显式声明可变:
1 | let mut x = 10; |
简单记:let 是创建不可变变量,let mut
是创建可变变量。VGA 的 Writer 里会看到:
1 | let mut column_position = 0; |
🏷️ 类型:写在变量名后
OS 开发里最常见的整数类型:
1 | u8 无符号 8 位 |
裸机代码里出现频率最高的是
usize、u8、u16、u32、u64——它们直接对应寄存器、地址和
MMIO 宽度。
🧱 struct:和 C 很像
1 | struct ScreenChar { |
基本对应 C:
1 | struct ScreenChar { |
创建与访问:
1 | let c = ScreenChar { |
还有更简短的写法——元组结构体,字段不写名字,只按位置:
1 | struct ColorCode(u8); // 只包一个 u8 |
这种”只包一层”的写法也叫
newtype:给某个类型换个有语义的名字,同时内存里仍然只是那一个字节(后面
VGA 那篇的 ColorCode 就是它)。
🏷️ enum:一组固定的选项
C 的 enum 只给一组整数取名字;Rust 的 enum
是”一个值,只能是这几种之一”,而且每个变体还能自带数据。
1 | pub enum Color { |
使用时必须带上枚举名:
1 | let c = Color::Yellow; // 不是裸写 Yellow |
变体后面还能挂数据——这是 C 的 enum 没有的:
1 | enum Token { |
“每个变体带的数据还不同”这类值,配 match
就能逐个分支安全处理。Option 和 Result
本身就是这种 enum:
1 | Option<T> = Some(T) | None |
🚪 pub:谁能看到
pub 表示 public,允许模块外访问;不加 pub
的结构体默认只在当前模块内可用:
1 | pub struct Writer { // 外部能看见 Writer |
📦 mod 与 use:代码怎么分组,路径怎么写
C 用 #include 把别的文件搬进来;Rust
不用,它的组织单位叫模块——文件本身就是一层分组,编译器按项目结构自动找到全部代码。
1 | mod vga_buffer; // 声明一个子模块,对应 vga_buffer.rs |
层级关系是三层套下去:
1 | crate(一次编译的整体,也对应一个依赖包) |
三个词各管一件事:
| 词 | 是什么 |
|---|---|
| crate | 一次编译的单位,也是依赖的单位:Cargo.toml 里
[dependencies] 的每一项就是一个 crate |
| mod | crate 内部的模块,用来把代码分堆 |
use |
给某个路径起短名字,省得每次写全 |
use 的路径怎么读——第一段永远是 crate
名,后面跟着它导出的东西:
1 | use core::fmt; // crate :: 模块 |
两点容易踩:
- 没在
Cargo.toml里声明过的 crate,use不到(core、std这类随工具链来的除外)。 use不产生任何代码,只是编译期的名字解析;它写在文件哪个位置都行,作用范围都是整个模块——后面 VGA 的Writer里use core::fmt;就写在文件中部。
🛠️ impl:给 struct 加方法
C 里要写 writer_write(writer, ...) 这类散装函数;Rust
把方法挂在类型上:
1 | impl Writer { |
调用:
1 | writer.write_byte(b'A'); |
可以理解为 struct + impl = 类型 + 它的方法,类似 C++ 的 class,只不过 Rust 把数据(struct)和方法(impl)分开声明。
🧩 trait:约定一组方法,类型去实现它
impl 是”给某个类型加方法”;trait
是”约定某一类类型都必须提供哪些方法”。C 里没有对应物,最接近的是 C++
的纯虚函数或 Java 的 interface,但 Rust 的 trait
更轻——它只描述能力,不涉及继承关系。
1 | trait Write { |
某个类型要”接入”这份约定,就写 impl 契约 for 类型:
1 | impl Write for Writer { |
1 | trait Write ← 约定:谁有资格"写出字符串" |
规则两条:
- 必须实现的方法一个都不能少;带默认实现的可以省略,省略时就用默认行为。
impl一完成,这个类型就自动获得了那些带默认实现的方法。
还有一个容易忽略的前提:调用 trait 方法之前,trait 本身得先
use 进当前作用域,编译器才知道这个方法存在。
1 | use core::fmt::Write; // 先把它带进作用域 |
这套 trait 定义 + 默认实现 + 类型各自实现
的写法,就是后面 VGA 那篇第七层的全部内容。
📐 数组与索引
1 | let a = [1, 2, 3, 4]; // 类型 [i32; 4] |
类型里的两个数字分别表示元素类型与个数:
1 | [i32; 4] |
VGA 里的屏幕缓冲正是二维数组:
1 | chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT] |
1 | ┌───────────────────┐ |
索引从 0 开始,和 C 一样:buffer.chars[24][0] 表示第 24
行第 0 列。
🔁 for 循环与迭代器
C 的 for 是”初始化 → 判断 → 自增”三段式;Rust 的
for 是遍历一列值:
1 | for i in 0..4 { /* ... */ } // 0, 1, 2, 3(不含右端) |
0..4 这种写法叫区间。for
右边要的是”一列值”,能给出这一列值的就叫迭代器:
1 | for byte in s.bytes() { /* ... */ } // &str 按 UTF-8 字节逐个给出 |
同一个区间还能当 match
的模式用,两种写法经常一起出现:
1 | match byte { |
一句话:for ... in ...
右边产出一列值,左边接住每一个。产出规则由迭代器决定,不用自己写下标和自增。
🧬 泛型
<T>:类型也能当参数
C 里想写个”装什么都行”的容器,只能用
void *,用的时候再强转;Rust
用泛型:把类型本身当成参数,写进尖括号。
1 | struct Volatile<T> { // T 是类型参数,先不指定具体是什么 |
读法:Volatile<u8> 读作”装着 u8 的
Volatile”,尖括号里就是填进去的那个类型。
函数和 trait 也能带类型参数,而且常常给它加要求:
1 | fn largest<T: PartialOrd>(a: T, b: T) -> T // T 必须能比较大小 |
T: PartialOrd 里的 PartialOrd 是一个
trait——意思是”填进来的类型必须实现这项能力”。所以上一节讲的
trait,除了自己定义自己实现,也用来给泛型参数立规矩。
OS 代码里泛型几乎无处不在,后面会反复见到:
1 | Volatile<ScreenChar> 一个屏幕格子的 volatile 包装 |
和 C 的 void *
比,泛型的关键差别是:填进去什么类型,编译器就为它生成对应的一份代码,运行时不必转型,也不损失类型检查。
🔒 const
1 | const BUFFER_HEIGHT: usize = 25; |
类似 C 的 #define,但 Rust 的 const
带类型、参与类型检查。
🧮 函数与最后一行是返回值
1 | fn add(a: i32, b: i32) -> i32 { |
Rust 函数体的最后一个表达式就是返回值。如果写成
a + b;,加了分号就变成语句,返回 ()。这引出了
Rust 和 C 一个很大的思维差异——Rust 喜欢表达式:
1 | let x = if condition { |
这里的 if 本身产生一个值并赋给 x。这也是
Rust 代码看起来和 C 不一样的原因之一。
🔀 match:更强大的 switch
1 | match number { |
match 是模式匹配,后面学
Option/Result/枚举时它是核心工具,可暂时理解为加强版
switch。
🟢 Option 与 Result:不靠 NULL 和 -1
C 里常用 NULL 表示没有值、用 return -1
表示失败。Rust 不喜欢这种方式,改用两个显式类型:
1 | Option<T> = Some(T) 有值 | None 没有值 |
1 | let x: Option<i32> = Some(10); |
Some/None/Ok/Err 加上
match,就能强制调用方处理可能没有值或可能失败的情况——这是
Rust 错误处理的基本形态,OS 代码里读取 MMIO
寄存器、解析结构时常会遇到。
✅ 总结
从 C 到 Rust,变量、类型、
struct、数组、函数这些零件大多能一一对应,差异主要在三点:变量默认不可变(mut)、类型写在变量名后、最后一个表达式即返回值。真正的分水岭是下一篇要讲的引用与所有权。
这一篇里还有几样值得单独记住的东西:enum(可带数据的固定选项)、trait(约定一组方法)、泛型
<T>(类型当参数)、for
与迭代器(遍历一列值)、mod /
use(代码分组与路径)。后面每一篇 OS
文章都会反复用到它们。
下一篇接着讲这条核心主线:&(借用)→
&mut(可变借用)→ 裸指针 → unsafe →
生命周期,把内存到底归谁管讲清楚。
Happy Hacking! 🎉