unsafe Rust 与原始指针
本教程共 78 篇 · 第 77 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:理解 unsafe 不是”关掉检查”,而是”开发者自己担保安全”,认识它的几种超能力(尤其是裸指针),学会用安全函数包裹 unsafe 代码缩小风险面,并知道 2024 Edition 对 unsafe 语法的收紧。
Rust 以安全著称,但它也留了一扇”后门”——unsafe。很多人误以为 unsafe 就是”让编译器闭嘴、随便写”。大错特错。unsafe 的真实含义是:编译器不再替你保证安全,安全责任交给你。这一章讲清楚这扇门后面到底有什么、怎么用才负责任。
1-1 unsafe 到底是什么
写 unsafe 不意味着你的代码就错了,也不意味着内存安全问题被允许——它只是把”证明安全”的义务从编译器转移到了你身上。在 unsafe 块里,你能做几件安全 Rust 做不到的事(后面列),但只要你做错了(比如解引用空指针),就是未定义行为(UB),后果比普通 bug 更严重:可能崩溃,也可能静默地数据错乱。
所以铁律第一条:能不用 unsafe 就别用。Rust 的标准库和生态已经把绝大多数需要 unsafe 的地方包成了安全接口,你日常写业务基本碰不到。
1-2 裸指针:像 C 的指针
裸指针(raw pointer)是 unsafe 里最常见的角色,长这样:*const T(不可变)和 *mut T(可变)。它跟引用很像,但更”野”:
- 它可以同时存在一个数据的多个可变指针,直接绕过借用规则。
- 它不保证指向合法内存,可以是空指针
null。 - 它没有自动释放(drop),要自己管内存。
和引用、智能指针不同,裸指针是”危险但灵活”的代表。
创建裸指针本身是安全的;只有解引用裸指针才是 unsafe:
fn main() {
let mut num = 5;
let r1 = &num as *const i32; // 基于引用创建,安全
let r2 = &mut num as *mut i32; // 安全
unsafe {
println!("r1 指向: {}", *r1); // 解引用,必须 unsafe
println!("r2 指向: {}", *r2);
}
}
Warning千万别凭空捏造内存地址当裸指针,比如
let r = 0x012345usize as *const i32;。访问任意地址几乎必然段错误或未定义行为。裸指针应该基于已有的合法数据(引用、智能指针、真实地址)来创建。
1-3 调用 unsafe 函数
用 unsafe fn 定义的函数,调用时也必须包在 unsafe 块里,提醒调用者”这个函数有前提条件,你自己保证满足”:
unsafe fn dangerous() {}
fn main() {
unsafe {
dangerous();
}
}
如果直接调,编译器报错:call to unsafe function is unsafe。这个强制要求很妙:它让每个调用点都”亮红灯”,逼你去看文档、确认自己没遗漏前提。
Note在
unsafe fn函数体内部,不需要再套unsafe {}——函数本身已经声明了”此处不安全”,这就是所谓的”无需俄罗斯套娃”。但普通安全函数里想用 unsafe 操作,就要写unsafe {}。
1-4 用安全抽象包裹 unsafe
高手用 unsafe 的最高境界,是把它锁在小范围里,对外暴露一个安全的函数。标准库的 split_at_mut 就是典范:它要同一个切片上做两个可变借用(安全 Rust 不允许),于是内部用裸指针实现,但函数本身仍是安全函数——因为实现保证了两个借用绝不重叠。
use std::slice;
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();
assert!(mid <= len); // 这个断言保证了裸指针合法
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
注意 unsafe 块被缩到最小,且前面有 assert! 保证指针合法。使用者调用 split_at_mut 时完全不需要 unsafe——危险被封装起来了。这是写 unsafe 的范本:unsafe 范围越小、前置保证越明确,越安全。
1-5 unsafe 的其它超能力
除了解引用裸指针和调用 unsafe 函数,unsafe 还允许你做几件事(了解即可,初学很少手写):
- 实现 unsafe trait:某个 trait 的方法含有编译器无法验证的内容,实现时要用
unsafe impl。第 69 章给裸指针手搓Send就是例子。 - 访问可变静态变量:
static mut允许全局可变状态,但多线程下极危险;2024 Edition 已禁止直接对它取引用(见下文)。 - 访问 union 字段:
union所有字段共享同一块内存(主要为了和 C 交互),读哪个字段编译器无法校验类型,所以访问是unsafe。
1-6 2024 Edition 对 unsafe 的收紧
我们全书以 2024 Edition 为基线,这一版明显让 unsafe 更”显眼、可审计”(来源:Rust 1.85.0 发布说明):
extern块现在必须标unsafe。调用其它语言(如 C)的函数属于 unsafe 操作,2024 Edition 要求写成unsafe extern "C" { ... },明确警示。第 78 章讲 FFI 时你会看到这个写法。unsafe函数体内也要显式写unsafe {}。以前在unsafe fn里直接做 unsafe 操作就行,现在新增的unsafe_op_in_unsafe_fnlint 默认警告,要求把真正的 unsafe 操作再用unsafe {}框出来,方便审计”到底哪几行不安全”。- 引用
static mut变为 deny 级错误。直接对可变静态变量取引用不再允许,逼你用更安全的方案(如OnceLock、Mutex保护的全局)。
Tip这些改动不改变 unsafe 的能力,只是让”哪里不安全”在代码里一目了然。迁移老项目时
cargo fix --edition能自动补上大部分unsafe关键字。
1-7 何时该用 unsafe:一条决策线
写业务时,先问自己:这件事安全 Rust 能不能做?绝大多数时候能——标准库和生态已经把需要 unsafe 的地方包成了安全接口。只有几类情况才逼你出手:要和 C/C++ 对话(FFI,下一章)、要做极底层的性能优化(手动管内存、用裸指针)、要实现标准库没覆盖的 unsafe trait(如手写 Send)。
决策线很简单:能写安全代码就绝不写 unsafe;非写不可时,把 unsafe 锁进最小范围,用安全函数把它包起来,并写清楚”为什么这里安全”的注释。代码评审时,unsafe 块是重点照顾对象,每一个都要能说清不变量。记住,unsafe 不是炫技,是责任——它把编译器的担保换成了你自己的担保。
Tip一个好用的习惯:把 unsafe 操作封进一个安全函数,函数文档里写明”前置条件”和”为什么安全”。使用者完全不需要碰 unsafe,安全性由你一个人把关,比到处散落 unsafe 块安全得多。
1-8 小结
unsafe 不是免检通道,而是”责任转移”:编译器不再担保,由你证明安全。裸指针和 unsafe 函数是它最常见的两样工具,用安全抽象把 unsafe 锁进最小范围是正确姿势。2024 Edition 进一步要求 unsafe extern 块和函数体内的 unsafe {},让不安全代码更可审计。下一章我们看 unsafe 最重要的用武之地——FFI 跨语言互操作。