Rust 中的引用、裸指针与智能指针
从引用派生的裸指针继承其访问路径的来源和权限。如果该访问路径被后续的冲突借用或访问失效,再使用裸指针就是 UB。旧代码让裸指针经过了一次短期的可变重借用,随后继续使用原始引用,可能使裸指针失效;新代码用 &raw 直接构造裸指针,避免了这次额外重借用,因此规避了这一类 UB。
raw pointer 就是 裸指针,某些地方也叫 原始指针。
先建立一个总框架:Rust 里常说的"指针"大致分三类:
- 引用:
&T、&mut T - 裸指针:
*const T、*mut T - 智能/所有权指针:
Box<T>、Rc<T>、Arc<T>、NonNull<T>、Pin<P>等
其中 &raw const ... 和 &raw mut ... 不是新的指针类型,而是"直接创建裸指针"的表达式。
1. 什么是 Rust 裸指针
Rust 的裸指针类型有两个:
rust
*const T // 指向 T 的 const raw pointer
*mut T // 指向 T 的 mutable raw pointer
例如:
rust
let mut value = 42;
let p1: *const i32 = &raw const value;
let p2: *mut i32 = &raw mut value;
它们之所以叫"裸"指针,是因为编译器基本不替你保证:
- 指针非空;
- 指针没有悬空;
- 指向的内存已经初始化;
- 地址满足对齐要求;
- 对象还活着;
- 当前允许读或写;
- 没有违反 aliasing 规则。
所以裸指针本身可以合法地保存一些不能解引用的值:
rust
let null: *mut i32 = core::ptr::null_mut();
创建或保存裸指针通常不需要 unsafe,但解引用通常需要:
rust
let mut value = 42;
let p = &raw mut value;
unsafe {
*p = 100;
}
assert_eq!(value, 100);
这里的 unsafe 不是"关闭 Rust 的安全规则",而是程序员向编译器承诺:
我已经确认
p非空、未悬空、对齐、指向有效内存,并且这次写入没有违反 aliasing 规则。
*const T 和 *mut T
它们最直接的区别是:
rust
*const T // 通常用于读取
*mut T // 可以表达读取或写入的意图
但是要注意:
*mut T不意味着一定允许写。*const T转换成*mut T也不会凭空获得写权限。*mut T不像&mut T那样自动表示独占访问。
例如:
rust
let value = 42;
let p1: *const i32 = &raw const value;
let p2: *mut i32 = p1.cast_mut();
p2 的类型虽然是 *mut i32,但这不代表通过它修改原本不可变的 value 就合法。
2. 引用和裸指针的区别
&T:共享引用
rust
let value = 42;
let r: &i32 = &value;
println!("{}", *r);
它具有比较强的保证:
- 必须非空;
- 必须正确对齐;
- 必须指向一个有效的
T; - 对象必须在引用生命周期内保持有效;
- 可以存在多个共享引用;
- 通常不能通过它修改对象。
解引用是安全的:
rust
let n = *r;
&mut T:独占引用
rust
let mut value = 42;
let r: &mut i32 = &mut value;
*r = 100;
它除了必须有效之外,还表达了独占访问:
当这个
&mut T被使用时,不应同时使用其他冲突的访问路径访问同一块内存。
因此 &mut 更准确的理解不是"可变指针",而是"独占引用"。
这两者差别很重要:
rust
&mut T // 安全、受生命周期约束、表达独占访问
*mut T // 裸指针、不自动表达独占关系、解引用需要 unsafe
简单对比:
| 类型 | 非空保证 | 生命周期检查 | 安全解引用 | 独占语义 | 拥有对象 |
|---|---|---|---|---|---|
&T |
是 | 是 | 是 | 否,共享 | 否 |
&mut T |
是 | 是 | 是 | 是 | 否 |
*const T |
否 | 否 | 否 | 否 | 否 |
*mut T |
否 | 否 | 否 | 不自动保证 | 否 |
NonNull<T> |
是 | 否 | 否 | 不自动保证 | 否 |
Box<T> |
是 | 是 | 是 | 所有权控制 | 是 |
3. 先分清"类型语法"和"表达式语法"
假设:
rust
let mut value = 42_i32;
let mut pointer: *mut i32 = &raw mut value;
下面这些写法看起来很像,但意义完全不同。
*mut i32
这是一个类型:
rust
let pointer: *mut i32;
表示变量 pointer 保存一根指向 i32 的可变裸指针。
这里的 *mut 不能单独作为普通表达式理解。
*pointer
这是解引用操作,表示 pointer 指向的那个 i32:
rust
unsafe {
*pointer = 100;
}
这里:
text
pointer 是 *mut i32
*pointer 是 pointer 指向的 i32 place
从裸指针解引用需要 unsafe。
4. &mut pointer 是什么
仍然假设:
rust
let mut pointer: *mut i32 = ...;
那么:
rust
let pp = &mut pointer;
pp 的类型是:
rust
&mut *mut i32
它不是指向 i32 的引用,而是指向"裸指针变量 pointer 本身"的可变引用。
内存关系是:
text
pp
│
▼
pointer ──────────> i32
因此通过 pp 可以修改 pointer 保存的地址:
rust
let mut x = 10;
let mut y = 20;
let mut pointer: *mut i32 = &raw mut x;
{
let pp: &mut *mut i32 = &mut pointer;
*pp = &raw mut y;
}
unsafe {
*pointer = 30;
}
assert_eq!(x, 10);
assert_eq!(y, 30);
*pp = ... 修改的是 pointer,让它从指向 x 改成指向 y。
5. &mut *pointer 是什么
如果:
rust
pointer: *mut i32
那么:
rust
unsafe {
let r: &mut i32 = &mut *pointer;
}
执行了两步:
text
*pointer 定位 pointer 指向的 i32
&mut (...) 为这个 i32 创建独占引用
结果是:
rust
&mut i32
这比单纯使用裸指针提出了更强的要求。创建 r 时你必须保证:
pointer非空;- 地址正确对齐;
- 对象已经初始化为有效
i32; - 对象仍然存活;
- 这次创建
&mut i32不违反独占访问要求。
所以不要为了"方便取地址"而随便写:
rust
unsafe { &mut *pointer }
因为这真的创建了一个 &mut T。
6. &raw mut *pointer 是什么
如果:
rust
pointer: *mut i32
那么:
rust
let another: *mut i32 = unsafe {
&raw mut *pointer
};
它也分两部分:
text
*pointer 描述 pointer 指向的 place
&raw mut (...) 直接取得这个 place 的裸指针
结果仍然是:
rust
*mut i32
关键区别是:
rust
&mut *pointer
产生 &mut i32,建立独占引用。
而:
rust
&raw mut *pointer
产生 *mut i32,不会为最终 place 创建 &mut i32。
对比:
rust
let r: &mut i32 = unsafe { &mut *pointer };
let p: *mut i32 = unsafe { &raw mut *pointer };
这里 r 是引用,p 是裸指针。
如果只是取得同一个对象的裸指针,通常直接复用 pointer 即可:
rust
let another = pointer;
&raw mut *pointer 更常用于字段投影:
rust
struct Node {
value: i32,
next: *mut Node,
}
let node_ptr: *mut Node = ...;
let next_ptr: *mut *mut Node = unsafe {
&raw mut (*node_ptr).next
};
这样可以直接获得 next 字段的裸指针,而不需要先创建:
rust
&mut *node_ptr
7. &raw mut pointer 和 &raw mut *pointer 也不同
假设:
rust
pointer: *mut i32
那么:
rust
&raw mut pointer
指向的是局部变量 pointer 本身,类型为:
rust
*mut *mut i32
而:
rust
&raw mut *pointer
指向的是 pointer 所指向的 i32,类型为:
rust
*mut i32
图示:
text
&raw mut pointer
*mut *mut i32
│
▼
pointer ──────> i32
&raw mut *pointer
*mut i32
│
▼
pointer ──────> i32
是否有 *,决定你取的是"指针变量的地址"还是"被指向对象的地址"。
8. 如果 pointer 本身是 &mut T
再看 PR 中的情况:
rust
me: &mut ListHead<T, A>
这里 me 不是裸指针,而是独占引用。
&raw mut *me
rust
let p: *mut ListHead<T, A> = &raw mut *me;
含义是:
*me表示me指向的ListHead。&raw mut直接取得这个ListHead的裸指针。- 不额外创建一个
&mut ListHead重借用。
因为 me 是有效引用,所以这个操作本身不需要 unsafe。
&raw mut me
如果写成:
rust
let p = &raw mut me;
得到的不是节点指针,而是指向局部引用变量 me 的指针:
rust
*mut &mut ListHead<T, A>
目标完全不同。
&mut *me
rust
let another: &mut ListHead<T, A> = &mut *me;
这会创建一次新的可变重借用,正是 PR 在某些位置希望避免的东西。
9. &raw const 和 &raw mut
它们分别构造:
rust
&raw const place // *const T
&raw mut place // *mut T
例如:
rust
let mut value = 42;
let read_ptr: *const i32 = &raw const value;
let write_ptr: *mut i32 = &raw mut value;
它们只取地址:
- 不读取
value; - 不写入
value; - 不为最终 place 创建
&T或&mut T。
但这不代表以后解引用一定合法:
rust
unsafe {
let n = *read_ptr;
*write_ptr = 100;
}
是否合法仍然取决于对象生命期、初始化、对齐和 aliasing。
10. NonNull<T> 是什么
当前链表中大量使用:
rust
NonNull<ListHead<T, A>>
NonNull<T> 可以理解为:
保证地址不为 null 的裸指针包装器。
它:
- 保证非空;
- 可以复制;
- 不拥有对象;
- 不携带对象生命周期;
- 不保证对象仍然存活;
- 解引用仍然需要
unsafe。
例如:
rust
let mut value = 42;
let p = unsafe {
NonNull::new_unchecked(&raw mut value)
};
由于 value 是一个真实对象,它的地址非空,所以这里满足 new_unchecked 的非空要求。
但下面依旧可能悬空:
rust
fn bad() -> NonNull<i32> {
let mut value = 42;
unsafe { NonNull::new_unchecked(&raw mut value) }
} // value 在这里死亡,返回的 NonNull 悬空
因此 NonNull 只保证"非 null",不保证"永远有效"。
11. 常见智能指针
Box<T>
rust
let b = Box::new(42);
特点:
- 拥有
T; - 通常把
T放在堆上; Box销毁时自动销毁和释放T;- 可以安全解引用。
rust
println!("{}", *b);
Rc<T>
rust
let a = Rc::new(42);
let b = Rc::clone(&a);
单线程引用计数共享所有权。最后一个 Rc 被销毁时,内部对象才被释放。
默认不能直接可变访问共享对象,通常配合:
rust
Rc<RefCell<T>>
Arc<T>
Arc<T> 是原子引用计数版本,适合跨线程共享:
rust
Arc<Mutex<T>>
Arc<RwLock<T>>
Arc<T> 本身解决的是所有权和生命周期,不会自动让 T 的并发修改安全。
Pin<P>
Pin<P> 是对另一个指针类型的包装,例如:
rust
Pin<Box<T>>
Pin<&mut T>
它的主要目的不是"提供另一种地址",而是限制对象被移动。
这对自引用对象和侵入式容器很重要,因为对象一旦把自身字段地址存进链表,再移动对象就会让旧地址失效。
12. 最容易混淆的几个 mut
看这句:
rust
let mut pointer: *mut T = ...;
这里出现了两个 mut:
text
let mut pointer
^^^
允许修改变量 pointer 保存的地址
*mut T
^^^
表示可变裸指针类型
它们互相独立。
例如:
rust
let pointer: *mut i32 = ...;
虽然变量绑定没有 mut,仍然可能通过指针修改目标:
rust
unsafe {
*pointer = 42;
}
只是不能修改 pointer 自己:
rust
pointer = another_pointer; // 不允许,pointer 绑定不是 mut
再看:
rust
&mut pointer
这里的 mut 表示创建对变量 pointer 的独占引用。
而:
rust
&raw mut *pointer
这里的 mut 表示构造 *mut T,不是创建 &mut T。
13. 实用判断方法
看到复杂指针表达式时,可以从右向左读。
例如:
rust
&raw mut (*node_ptr).next
从右向左:
node_ptr:节点裸指针。*node_ptr:它指向的节点 place。(*node_ptr).next:节点的next字段 place。&raw mut:取得该字段的可变裸指针。
结果是:
rust
*mut NextFieldType
而:
rust
&mut (*node_ptr).next
最后一步是创建 &mut NextFieldType,要求更强的独占引用条件。
14. 推荐的使用顺序
日常 Rust 代码优先使用:
text
&T / &mut T
需要拥有堆对象时:
text
Box<T>
需要共享所有权时:
text
Rc<T> / Arc<T>
只有在这些场景才通常需要裸指针:
- 内核与驱动;
- FFI;
- 内存分配器;
- intrusive list/tree;
- 自引用结构;
- MMIO;
- packed 或未初始化字段;
- 实现底层同步原语。
如果最终只需要裸指针,就直接创建裸指针:
rust
&raw const place
&raw mut place
不要无意义地绕一圈:
text
&mut place // 先创建引用
↓
*mut T // 再转成裸指针
不过,项目中的 raw-pointer skill 也特别强调:如果 r: &mut T 已经是现成引用,普通的一次性转换:
rust
NonNull::from_mut(r)
通常仍然清晰合理。这个 PR 使用 &raw mut *r,是因为侵入式容器会长期保存该指针,同时继续重借用节点,需要特意避免那次额外的短期 &mut 重借用。