(done) RUST 指针详解

Rust 中的引用、裸指针与智能指针

从引用派生的裸指针继承其访问路径的来源和权限。如果该访问路径被后续的冲突借用或访问失效,再使用裸指针就是 UB。旧代码让裸指针经过了一次短期的可变重借用,随后继续使用原始引用,可能使裸指针失效;新代码用 &raw 直接构造裸指针,避免了这次额外重借用,因此规避了这一类 UB。

raw pointer 就是 裸指针,某些地方也叫 原始指针。

先建立一个总框架:Rust 里常说的"指针"大致分三类:

  1. 引用:&T、&mut T
  2. 裸指针:*const T、*mut T
  3. 智能/所有权指针: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;

含义是:

  1. *me 表示 me 指向的 ListHead。
  2. &raw mut 直接取得这个 ListHead 的裸指针。
  3. 不额外创建一个 &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

从右向左:

  1. node_ptr:节点裸指针。
  2. *node_ptr:它指向的节点 place。
  3. (*node_ptr).next:节点的 next 字段 place。
  4. &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 重借用。

相关推荐
geovindu2 小时前
rust: Template Method Pattern
开发语言·后端·设计模式·rust·行为模式·模板方法模
遇码4 小时前
Rust 桌面应用自托管自动更新:latest.json 清单 + minisign 签名校验的完整做法
人工智能·ai·rust·大模型·agent
geovindu4 小时前
rust: Template Method Pattern(续)
开发语言·后端·设计模式·rust·行为模式·板方法模式
niyongsheng18 小时前
多智能体连续时间内路径规划:AA-SIPP(m) 的 Rust 实现与算法
算法·rust·机器人
NetX行者1 天前
Rust 初体验:一个全栈工程师的编译器“毒打“笔记
开发语言·笔记·rust
Amos_Web1 天前
Rspack 源码解析(二十七):AsyncWasmParserAndGenerator 如何把二进制变成异步模块
前端·rust
敲代码的玉米C1 天前
菜单栏做出来了,内存预算却没过:Hostwatch 开发手记(二)
rust
敲代码的玉米C1 天前
电脑卡了,然后呢?我想用 Rust 做一个 Mac 诊断小工具
rust
GeminiJM1 天前
从 0 到 1 学 Codex 源码:Rust 基础(一)
rust·源码阅读·cargo·编程入门·codex