Skip to content

智能指针

Rust 中的指针是包含内存地址的变量,该地址指向另一块数据。最常见的指针是普通引用 &T,它只是借用数据,不持有所有权。智能指针在此基础上更进一步: 它是一个结构体,既保存了指向数据的地址,也拥有数据的所有权,并通过实现 DerefDrop 两个 trait,使其能像普通引用一样使用,同时在离开作用域时自动执行清理逻辑。

你其实早已接触过智能指针——StringVec<T> 本质上都是智能指针: 它们在堆上管理动态内存,持有该内存的所有权,并在离开作用域时自动释放。与之对应的 &str&[T] 只是普通的胖指针,借用数据但不拥有所有权。

特性普通引用 &T智能指针(BoxRc 等)
本质内存地址包含地址 + 额外元数据的结构体
所有权借用,不持有数据拥有(或共同拥有)数据
自动清理不负责清理离开作用域时自动释放内存
额外能力引用计数、内部可变性等

智能指针区别于普通结构体的核心在于它实现了 Deref trait(让其能像引用一样使用)和 Drop trait(定义离开作用域时的清理逻辑)。

Deref Trait: 解引用运算符

实现 Deref trait 的类型可以重载解引用运算符 *,使其能像普通引用一样被解引用。这是智能指针的核心 trait 之一——没有 DerefBox<T> 就只是一个普通结构体,*box_val 将无法编译。

实现 Deref

rust
use std::ops::Deref;

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> { MyBox(x) }
}

// 实现 Deref,使 MyBox<T> 能像 &T 一样使用
impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T {
        &self.0 // 返回内部值的引用(而非值本身,以免转移所有权)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);
    assert_eq!(5, *y); // *y 等价于 *(y.deref())
}

当你写 *y 时,编译器实际上展开为 *(y.deref())——先调用 deref() 得到一个常规引用,再用 * 解引用。deref() 返回引用而非值,是为了避免将内部数据的所有权转移给调用者。

* 不会无限递归替换: 从 *y*(y.deref()) 只发生一次。

Deref 隐式转换

对于函数和方法的传参,Rust 提供了隐式 Deref 转换: 当实参类型与参数签名不匹配,但实参类型实现了 Deref 时,编译器会自动沿着 Deref 链反复调用 deref() 直到类型匹配:

rust
fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
    // 隐式转换链(编译期完成,零运行时开销):
    //   &MyBox<String> --[MyBox 的 Deref]--> &String
    //   &String        --[String 的 Deref]--> &str  ✅
}

若没有隐式转换,同样的调用需要写成 hello(&(*m)[..]),可读性大幅下降。& 是触发隐式 Deref 转换的信号——只有引用类型的实参才会触发自动解引用。

引用归一化

&T 本身也实现了 Deref<Target=T>,因此 Rust 编译器会自动将多重引用 &&&&T 归一化为 &T:

rust
struct Foo;
impl Foo {
    fn method(&self) { println!("called"); }
}

let f = &&Foo;
f.method();          // ✅ &&Foo 自动归一化为 &Foo
(&&&&f).method();    // ✅ 同理,任意层级的引用都能自动解引用

三种 Deref 转换规则

条件转换
T: Deref<Target=U>&T&U
T: DerefMut<Target=U>&mut T&mut U
T: Deref<Target=U>&mut T&U

第三条允许可变引用隐式转为不可变引用,但反向不行——如果不可变引用能转为可变,就会与其他已有的不可变引用发生冲突,破坏借用规则。

DerefMut 的用法与 Deref 类似,只是针对可变引用:

rust
use std::ops::{Deref, DerefMut};

struct MyBox<T> { v: T }

// 实现 Deref 使 MyBox<T> 能像 &T 一样使用
impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T { &self.v }
}
// 实现 DerefMut 使 MyBox<T> 能像 &mut T 一样使用
impl<T> DerefMut for MyBox<T> {
    fn deref_mut(&mut self) -> &mut T { &mut self.v }
}

fn append(s: &mut String) {
    s.push_str(", world");
}

fn main() {
    let mut m = MyBox { v: String::from("hello") };
    append(&mut m); // &mut MyBox<String> → &mut String(DerefMut 隐式转换)
    println!("{}", m.v); // "hello, world"
}

Drop Trait: 自动清理逻辑

Drop trait 类似于其他语言的析构函数,定义了值离开作用域时自动执行的清理代码。几乎所有涉及资源管理的类型(文件句柄、网络连接、堆内存等)都应实现 Drop:

rust
struct CustomPointer {
    data: String,
}

// 实现 Drop,在 CustomPointer 离开作用域时自动执行清理逻辑
impl Drop for CustomPointer {
    fn drop(&mut self) {
        println!("清理 CustomPointer,data = {}", self.data);
    }
}

fn main() {
    let c = CustomPointer { data: String::from("重要资源") };
    let d = CustomPointer { data: String::from("另一个资源") };
    println!("创建完毕");
}
// 输出:
// 创建完毕
// 清理 CustomPointer,data = 另一个资源  ← d(后创建,先释放)
// 清理 CustomPointer,data = 重要资源    ← c(先创建,后释放)

Drop 的顺序

变量按照创建顺序的逆序(后进先出)释放;结构体内部字段按照定义顺序依次释放:

rust
struct A;
struct B;
impl Drop for A { fn drop(&mut self) { println!("drop A"); } }
impl Drop for B { fn drop(&mut self) { println!("drop B"); } }

struct Pair { first: A, second: B }
impl Drop for Pair { fn drop(&mut self) { println!("drop Pair"); } }

fn main() {
    let _p = Pair { first: A, second: B }; // 先创建
    let _b = B;                             // 后创建
    println!("运行中");
}
// 输出:
// 运行中
// drop B      ← _b(后创建的变量,先释放)
// drop Pair   ← _p 自身的 drop 实现
// drop A      ← _p.first(按字段定义顺序)
// drop B      ← _p.second

即使 Pair 没有实现 Drop,Rust 也会自动为其字段依次调用 drop

提前释放: std::mem::drop

禁止直接调用 .drop() 方法(编译器会阻止,因为这会导致二次释放(double free))。若需提前手动释放,使用标准库的 drop(value) 函数:

rust
fn main() {
    let lock = CustomPointer { data: String::from("锁资源") };
    // lock.drop(); // ❌ 编译错误: explicit use of destructor method
    drop(lock);     // ✅ 转移所有权,函数结束时触发清理
    println!("锁已提前释放,其他代码可以获得锁");
}

std::mem::drop 的实现极其简单,仅靠所有权规则来触发 drop:

rust
pub fn drop<T>(_x: T) {} // 拿走所有权,函数结束时 _x 被 drop

CopyDrop 互斥

一个类型不能同时实现 CopyDropCopy 类型会被隐式复制,难以预测析构函数执行的时机,因此 Rust 明确禁止:

rust
#[derive(Copy)]
struct Foo;

impl Drop for Foo { // ❌ 编译错误: the trait `Copy` may not be implemented for
    fn drop(&mut self) { println!("dropping"); } // this type; the type has a destructor
}

Box<T>: 将数据分配到堆上

Box<T> 是 Rust 中最简单的智能指针,它将值存储在上,在上保留一个指向堆数据的固定大小指针。除了堆分配本身的开销,Box<T> 没有任何额外运行时代价。

基本用法

  • Box::new(value) 创建一个新的 Box<T> 智能指针,将 value 存储在堆上。
rust
fn main() {
    let b = Box::new(5); // 5 存储在堆上,b(指针)在栈上
    println!("b = {}", b); // Box 实现了 Deref,可以直接打印
} // b 离开作用域,堆上的 5 被自动释放

场景一: 递归类型

Rust 在编译时必须知道每个类型的精确大小。递归类型(在自身定义中包含同类型值)会导致大小无限递归,编译器无法确定:

rust
enum List {
    Cons(i32, List), // ❌ 错误: recursive type `List` has infinite size
    Nil,
}

Box<T> 是固定大小的指针(在 64 位系统上为 8 字节),将递归部分装箱即可打破这个无限递归:

rust
enum List {
    Cons(i32, Box<List>), // ✅ Box 是固定大小的指针
    Nil,
}

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

场景二: 大数据零拷贝转移

在栈上转移所有权时,Rust 默认会深拷贝整个数据(从一个栈帧复制到另一个)。对于大型数据,这非常耗时。将数据放在堆上,转移所有权时只需复制栈上的指针(8 字节):

rust
fn main() {
    // 栈上数组: 转移所有权时发生深拷贝(整个数组被复制一份)
    let arr1 = [0u32; 1000];
    let arr2 = arr1;
    println!("{}", arr1.len()); // ✅ arr1 仍可用(因为 [u32; N] 实现了 Copy)

    // 堆上数据: 转移所有权时只复制栈上指针(8 字节),堆数据不动
    let large = Box::new([0u32; 1000]);
    let large2 = large; // 所有权转移,仅复制指针
    // println!("{}", large.len()); // ❌ large 已失效
    println!("{}", large2.len()); // ✅
}

场景三: Trait 对象(类型擦除)

当你需要在集合中同时存储不同具体类型,但这些类型都实现了同一个 trait 时,Box<dyn Trait> 是最常用的解决方案。Vec 要求元素大小一致,不同具体类型大小不同;而 Box 本身大小固定(一个指针),完美解决这个问题:

rust
trait Draw {
    fn draw(&self);
}

struct Button { id: u32 }
impl Draw for Button {
    fn draw(&self) { println!("绘制按钮 {}", self.id); }
}

struct Select { id: u32 }
impl Draw for Select {
    fn draw(&self) { println!("绘制选择框 {}", self.id); }
}

fn main() {
    // Button 和 Select 大小不同,但 Box 大小固定
    let widgets: Vec<Box<dyn Draw>> = vec![
        Box::new(Button { id: 1 }),
        Box::new(Select { id: 2 }),
    ];
    for w in &widgets {
        w.draw();
    }
}

Box::leak: 创建 'static 引用

Box::leak 消耗掉 Box,使其指向的值不再受 Drop 管理,从而获得 'static 生命周期的引用。这在需要"运行时初始化、全局有效"的配置或单例场景中很有用:

rust
fn get_config() -> &'static str {
    let mut s = String::new();
    s.push_str("database_url=localhost");
    Box::leak(s.into_boxed_str()) // 返回 &'static str
}

fn main() {
    let config = get_config();
    println!("{}", config); // "database_url=localhost"
}

Box::leak 并不是"内存泄漏"的不良实践,而是一种有意为之的设计,将运行期动态生成的值提升为全局有效。相比 Rc/Arc,它的性能开销更低。

Rc<T>: 引用计数智能指针

Rust 的所有权规则要求一个值只能有一个所有者。但在图结构、多个组件共享配置等场景中,一个数据需要被多处同时持有。Rc<T>(Reference Counted)通过引用计数解决了这个问题: 每多一个所有者,计数加 1;计数降为 0 时,数据才被释放。

Rc<T> 仅适用于单线程场景,因为引用计数操作没有原子性保证。多线程场景请使用 Arc<T>(见无畏并发章节)。

基本用法

  • Rc::new(value) 创建一个新的 Rc<T> 智能指针。
  • Rc::clone(&rc) 增加引用计数,而不是进行深拷贝。
  • Rc::strong_count(&rc) 获取当前的强引用计数。
rust
use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("共享数据"));
    println!("引用计数 = {}", Rc::strong_count(&a)); // 1

    let b = Rc::clone(&a); // 增加引用计数,不深拷贝
    println!("引用计数 = {}", Rc::strong_count(&a)); // 2

    {
        let c = Rc::clone(&a);
        println!("引用计数 = {}", Rc::strong_count(&a)); // 3
    } // c 离开作用域,计数减 1

    println!("引用计数 = {}", Rc::strong_count(&a)); // 2
} // b 和 a 离开作用域,计数归 0,内存释放

Rc::clone(&a) 不进行深拷贝,只是增加引用计数并返回一个新的指向相同数据的指针,代价极低。使用 Rc::clone 而非 a.clone() 是一种惯例,明确提示读者"这里不做深拷贝"。

实际场景: 多所有者共享数据

rust
use std::rc::Rc;

struct Owner { name: String }
struct Gadget { id: i32, owner: Rc<Owner> }

fn main() {
    let owner = Rc::new(Owner { name: "Gadget Man".to_string() });
    let g1 = Gadget { id: 1, owner: Rc::clone(&owner) };
    let g2 = Gadget { id: 2, owner: Rc::clone(&owner) };

    drop(owner); // 释放原始变量(引用计数从 3 降到 2),数据仍然有效

    println!("Gadget {} owned by {}", g1.id, g1.owner.name); // 仍可访问
    println!("Gadget {} owned by {}", g2.id, g2.owner.name);
} // g1、g2 离开作用域,计数归 0,Owner 才被释放

只读限制

Rc<T> 只提供对数据的不可变引用(&T)。这符合 Rust 的借用规则: 多个所有者同时存在,意味着有多个引用,必须都是不可变的。若需要修改,需配合 RefCell<T> 使用(见下节)。

RefCell<T> 与内部可变性

内部可变性(Interior Mutability)允许在持有不可变引用的情况下修改数据。RefCell<T> 实现了这一模式,它将借用规则的检查从编译期推迟到运行时: 若在运行时违反借用规则(如同时持有可变和不可变引用),程序会 panic

Box<T>RefCell<T>
借用检查时机编译期运行时
违规后果编译错误运行时 panic
可变性遵循普通借用规则即使外部不可变也能获得可变借用

基本用法

  • RefCell::new(value) 创建一个新的 RefCell<T> 智能指针。
  • borrow() 获取不可变借用,返回 Ref<T>
  • borrow_mut() 获取可变借用,返回 RefMut<T>

经典场景: Mock 测试

外部库定义了一个 Messenger trait,其 send 方法签名是 &self(不可变)。你需要在自己的 MockMessenger 实现中记录发送的消息,但 &self 不允许修改字段:

rust
struct MockMessenger { sent: Vec<String> }

pub trait Messenger {
    fn send(&self, msg: &str); // 签名固定,无法修改
}

// ❌ 无法在 &self 方法中调用 push
struct BadMock { sent: Vec<String> }
impl Messenger for BadMock {
    fn send(&self, msg: &str) {
        self.sent.push(msg.to_string()); // 错误: self 是不可变引用
    }
}

将字段包裹在 RefCell<T> 中,通过 borrow_mut() 在不可变 self 内部获得可变访问:

rust
use std::cell::RefCell;

struct MockMessenger {
    sent: RefCell<Vec<String>>, // ✅ 使用 RefCell 包裹 Vec<String>,允许内部可变
}

pub trait Messenger {
    fn send(&self, msg: &str); // 签名固定,无法修改
}

impl Messenger for MockMessenger {
    fn send(&self, msg: &str) {
        self.sent.borrow_mut().push(msg.to_string()); // ✅ 内部可变性
    }
}

fn main() {
    let m = MockMessenger { sent: RefCell::new(Vec::new()) };
    m.send("hello");
    m.send("world");
    println!("发送了 {} 条消息", m.sent.borrow().len()); // 2
}

RefCell<T> 的两个核心方法:

  • borrow() → 返回 Ref<T>(不可变借用,可同时存在多个)
  • borrow_mut() → 返回 RefMut<T>(可变借用,同一时刻只能有一个,且与不可变借用互斥)

运行时 panic

rust
use std::cell::RefCell;

fn main() {
    let data = RefCell::new(5);
    let _a = data.borrow();        // 不可变借用
    let _b = data.borrow_mut();    // ❌ 可编译,但运行时 panic!违反借用规则
    // thread 'main' (12) panicked at src/main.rs:6:19: RefCell already borrowed
}

RefCell<T> 并不绕过借用规则,只是将检查推迟到运行时。当你确信逻辑上不会发生冲突,但编译器无法静态推断时,才使用它。

Cell<T>: Copy 类型的轻量内部可变

Cell<T> 是比 RefCell<T> 更轻量的内部可变性工具,仅适用于实现了 Copy 的类型(如 i32bool&str)。

通过 .get().set() 直接操作值,不涉及引用,也没有借用状态检查,零运行时开销:

rust
use std::cell::Cell;

fn main() {
    let x = Cell::new(5);
    let a = x.get();  // 取值(Copy 语义)
    x.set(10);        // 修改值,即使 x 是不可变绑定
    let b = x.get();
    println!("{a}, {b}"); // 5, 10
}
Cell<T>RefCell<T>
适用类型Copy 类型任意类型
访问方式.get() / .set()(值操作).borrow() / .borrow_mut()(引用操作)
运行时开销有(借用计数检查)
违规行为不会 panic(无引用概念)运行时 panic

需要内部可变性时,优先选 Cell<T>;只有类型没有实现 Copy 时,才选 RefCell<T>

Rc<T> + RefCell<T> 组合

这是最常见的智能指针组合: Rc<T> 提供多所有权,RefCell<T> 提供内部可变性,合起来实现多个所有者共享同一块可变数据:

rust
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let shared = Rc::new(RefCell::new("初始值".to_string()));

    let a = Rc::clone(&shared);
    let b = Rc::clone(&shared);

    a.borrow_mut().push_str(",A 修改了");
    b.borrow_mut().push_str(",B 也修改了");

    // 所有所有者看到的是同一份数据
    println!("{}", shared.borrow());
    // "初始值,A 修改了,B 也修改了"
}

这种组合的性能开销很低: 内存上仅多分配 3 个 usize/isize(Rc 的强/弱计数 + RefCell 的借用状态),CPU 损耗也非常小。

引用循环与内存泄漏

同时使用 Rc<T>RefCell<T> 时存在一个陷阱: 引用循环。当两个 Rc 互相持有对方时,引用计数永远不会降为 0,造成内存泄漏:

rust
use std::rc::Rc;
use std::cell::RefCell;

struct Node {
    next: RefCell<Option<Rc<Node>>>,
}

fn main() {
    // 1. 创建节点 a
    let a: Rc<Node> = Rc::new(Node { next: RefCell::new(None) });
    // 2. 创建节点 b,并让 b 指向 a
    let b: Rc<Node> = Rc::new(Node { next: RefCell::new(Some(Rc::clone(&a))) });

    // 3. 让 a 指向 b —— 此时形成了闭环:a -> b -> a
    *a.next.borrow_mut() = Some(Rc::clone(&b));

    // 此时 a 和 b 的强引用计数都是 2
    // 当 main 函数结束时,a 和 b 的变量被销毁,计数减 1,但仍各剩 1
    // 内存永远不会被回收,发生了内存泄漏!
}

使用 Weak<T> 打破循环

Weak<T>弱引用: 它不增加强引用计数(strong_count),只增加弱引用计数(weak_count),不持有所有权,也不阻止数据被释放。访问数据时必须先调用 upgrade() 方法,返回 Option<Rc<T>>——数据存在则为 Some,已释放则为 None

Rc<T>Weak<T>
增加计数类型strong_countweak_count
持有所有权
阻止数据释放
访问方式直接解引用必须先 .upgrade() 获取 Option<Rc<T>>

基本用法

  • Weak::new() 创建一个空的 Weak<T> 指针。
  • Rc::downgrade(&rc) 创建一个 Weak<T> 指向 Rc<T>,但不增加强引用计数。
  • weak.upgrade() 尝试获取一个 Rc<T>,如果原数据仍然存在(强引用计数 > 0),返回 Some(Rc<T>);如果数据已被释放(强引用计数 = 0),返回 None
rust
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    // 将此处改为 Weak 以打破循环
    next: RefCell<Option<Weak<Node>>>,
}

fn main() {
    // 1. 创建节点 a
    let a = Rc::new(Node { next: RefCell::new(None) });

    // 2. 创建节点 b,并让 b 指向 a
    // 注意: 这里 b -> a 使用了 downgrade 将 Rc 转为 Weak
    let b = Rc::new(Node {next: RefCell::new(Some(Rc::downgrade(&a)))});

    // 3. 让 a 指向 b
    // a -> b 保持强引用,b -> a 是弱引用,不再形成闭环
    *a.next.borrow_mut() = Some(Rc::downgrade(&b));

    // 验证引用计数:
    // a 的强引用计数为 1(变量 a),弱引用计数为 1(b.next)
    // b 的强引用计数为 1(变量 b),弱引用计数为 1(a.next)
    println!("a strong count: {}", Rc::strong_count(&a)); // 1

    // 当 main 结束,a 和 b 的强引用计数归零,内存将被正常回收.
}

实例: 树形结构中的父子引用

典型用法是父节点通过 Rc 持有子节点(强引用),子节点通过 Weak 反向引用父节点(弱引用),从而避免循环:

rust
use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,       // 弱引用: 指向父节点,不持有所有权
    children: RefCell<Vec<Rc<Node>>>,  // 强引用: 持有子节点
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    {
        let branch = Rc::new(Node {
            value: 5,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });
        // 子节点通过 Weak 反向引用父节点
        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        println!("branch: strong={}, weak={}", Rc::strong_count(&branch), Rc::weak_count(&branch));
        // strong=1(只有变量 branch),weak=1(leaf.parent 持有)
        println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
        // Some(Node { value: 5, ... })
    }
    // branch 离开作用域,strong_count 归 0,branch 被释放

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
    // None(branch 已被释放,Weak 自动失效)
}

对于树/图等层级结构中的反向引用(子 → 父),始终使用 Weak正向引用(父 → 子)使用 Rc。这是避免循环引用的通用模式。

基于 MIT 协议发布