首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >22-Rust 教程 - 智能指针

22-Rust 教程 - 智能指针

作者头像
LarryLan
发布2026-05-20 10:24:40
发布2026-05-20 10:24:40
740
举报

智能指针

Box、Rc、Arc 三剑客:堆上数据的"智能管家"

🎬 引入

还记得上篇我们说的吗?堆上数据就像自由市场,随便圈地但得自己收拾。

但 Rust 说:"太麻烦了,我给你请几个管家吧!"

于是就有了智能指针。它们比普通指针多了啥?多了"智能"——知道什么时候该分配内存,什么时候该释放,还能处理各种复杂的所有权关系。

我第一次看到 BoxRcArc 这三个家伙时,脑子里全是:"这仨有啥区别?我到底该用哪个?"

别急,今天咱们就把这仨管家摸得明明白白。

📌 核心概念

智能指针是啥?

普通指针就像个地址标签,只告诉你东西在哪儿。

智能指针像是个快递柜

  • 📦 帮你存东西(分配内存)
  • 🔑 给你取件码(访问数据)
  • 🧹 到期自动清理(释放内存)
  • 📋 还能记录谁取过(所有权追踪)
代码语言:javascript
复制
use std::rc::Rc;

fn main() {
    let data = Rc::new();  // 智能指针
    // data 离开作用域时,自动检查还有没有人用
    // 没人用了就自动释放
}

Box:最朴实的管家

Box 的人设: "我就帮你把东西放堆上,其他不管。"

特点:

  • 📦 独占所有权:只能有一个人拥有
  • 🗑️ 自动释放:离开作用域自动清理
  • 📏 固定大小:编译时知道大小
  • 💰 零开销:除了分配内存没额外成本
代码语言:javascript
复制
fn main() {
    let b = Box::new();  // 5 放在堆上
    println!("b = {}", b);
    // b 离开作用域,堆上的 5 自动释放
}

什么时候用 Box?

  1. 类型太大,不想放栈上
  2. 需要递归类型(比如链表)
  3. trait 对象(后面会讲)

Rc:引用计数管家

Rc 的人设: "东西大家共享,最后一个走的记得关灯。"

Rc = Reference Counting(引用计数)

特点:

  • 👥 共享所有权:多个人可以拥有
  • 🔢 引用计数:记录有多少人在用
  • 🚫 单线程:只能在同一个线程里用
  • 🧹 自动释放:计数为 0 时释放
代码语言:javascript
复制
use std::rc::Rc;

fn main() {
    let data = Rc::new();
    let data2 = Rc::clone(&data);  // 计数 +1
    let data3 = Rc::clone(&data);  // 计数 +1
    
    println!("计数:{}", Rc::strong_count(&data));  // 输出:3
    
    // data3 离开作用域,计数 -1
    // data2 离开作用域,计数 -1
    // data 离开作用域,计数 -1,变成 0,释放数据
}

什么时候用 Rc?

  • 多个部分需要读取同一份数据
  • 单线程环境
  • 比如:图结构、共享配置

Arc:线程安全的 Rc

Arc 的人设: "跟 Rc 一样,但我能多线程。"

Arc = Atomic Reference Counting(原子引用计数)

特点:

  • 👥 共享所有权:多个人可以拥有
  • 🔢 原子计数:线程安全的引用计数
  • 多线程:可以跨线程使用
  • 💸 有开销:原子操作比普通计数慢
代码语言:javascript
复制
use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new();
    let data_clone = Arc::clone(&data);
    
    let handle = thread::spawn(move || {
        println!("线程里:{}", data_clone);
    });
    
    handle.join().unwrap();
    println!("主线程:{}", data);
}

什么时候用 Arc?

  • 多线程共享数据
  • 并发读取同一份数据
  • 比如:线程池、共享缓存

Deref 特质:智能指针的"解包"能力

Deref 的人设: "我虽然是指针,但你可以当我就是数据本身。"

代码语言:javascript
复制
use std::ops::Deref;

struct MyBox<T>(T);

impl<T> Deref for MyBox<T> {
    type Target = T;
    
    fn deref(&self) -> &Self::Target {
        &self.
    }
}

fn main() {
    let mb = MyBox();
    println!("{}", *mb);  // 可以解引用
    println!("{}", mb);   // 甚至可以不解引用(自动 deref)
}

Rust 的自动 deref:

代码语言:javascript
复制
let s = String::from("hello");
let slice: &str = &s;  // 自动 deref,String → &str

Drop 特质:离开前的"遗言"

Drop 的人设: "我要走了,临走前有些事得处理。"

代码语言:javascript
复制
struct Cleaner {
    name: String,
}

impl Drop for Cleaner {
    fn drop(&mut self) {
        println!("{} 正在清理资源...", self.name);
        // 这里可以释放文件、网络连接等
    }
}

fn main() {
    let c = Cleaner {
        name: String::from("清洁工"),
    };
    println!("清洁工开始工作");
    // c 离开作用域,自动调用 drop()
}
// 输出:
// 清洁工开始工作
// 清洁工正在清理资源...

手动提前 drop:

代码语言:javascript
复制
fn main() {
    let c = Cleaner {
        name: String::from("清洁工"),
    };
    println!("开始工作");
    drop(c);  // 手动提前释放
    println!("工作结束");
    // 这里不会再 drop 一次
}

💻 代码示例

示例 1:Box 的基本使用

代码语言:javascript
复制
fn main() {
    // 基本用法
    let b = Box::new();
    println!("b = {}", b);  // 输出:5
    
    // Box 里的数据可以修改(如果是可变的)
    let mut b2 = Box::new();
    *b2 = ;  // 解引用修改
    println!("b2 = {}", b2);  // 输出:20
    
    // Box 可以嵌套
    let nested = Box::new(Box::new(Box::new("深度嵌套")));
    println!("{}", ***nested);
}

示例 2:Box 用于递归类型

代码语言:javascript
复制
// 链表节点:每个节点指向下一个节点
enum List {
    Cons(i32, Box<List>),  // Box 让递归类型成为可能
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let list = Cons(, 
        Box::new(Cons(, 
            Box::new(Cons(, 
                Box::new(Nil))))));
    
    // 打印链表
    print_list(&list);
}

fn print_list(list: &List) {
    match list {
        Cons(val, next) => {
            print!("{} -> ", val);
            print_list(next);
        }
        Nil => println!("END"),
    }
}
// 输出:1 -> 2 -> 3 -> END

为什么需要 Box? 没有 Box 的话,编译器不知道 List 有多大(因为它可以无限嵌套)。Box 是个固定大小的指针,编译器就知道怎么分配内存了。

示例 3:Rc 的共享所有权

代码语言:javascript
复制
use std::rc::Rc;

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let a = Rc::new(Cons(, 
        Rc::new(Cons(, 
            Rc::new(Nil)))));
    
    // b 和 c 共享 a 的所有权
    let b = Cons(, Rc::clone(&a));
    let c = Cons(, Rc::clone(&a));
    
    println!("计数:{}", Rc::strong_count(&a));  // 输出:3
    
    // 可以遍历共享的列表
    print_list(&b);
    print_list(&c);
}

fn print_list(list: &List) {
    match list {
        Cons(val, next) => {
            print!("{} -> ", val);
            print_list(next);
        }
        Nil => println!("END"),
    }
}

示例 4:Arc 多线程共享

代码语言:javascript
复制
use std::sync::Arc;
use std::thread;
use std::time::Duration;

fn main() {
    let data = Arc::new(vec![, , ]);
    
    let mut handles = vec![];
    
    for i in .. {
        let data_clone = Arc::clone(&data);
        
        let handle = thread::spawn(move || {
            println!("线程 {} 读取:{:?}", i, data_clone);
            thread::sleep(Duration::from_millis());
        });
        
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("主线程:计数 = {}", Arc::strong_count(&data));
}

示例 5:Drop 的实际应用

代码语言:javascript
复制
struct DatabaseConnection {
    url: String,
}

impl Drop for DatabaseConnection {
    fn drop(&mut self) {
        println!("断开数据库连接:{}", self.url);
        // 实际代码里这里会真的断开连接
    }
}

fn main() {
    {
        let db = DatabaseConnection {
            url: String::from("mysql://localhost:3306"),
        };
        println!("数据库连接已建立");
        // db 在这里离开作用域,自动断开连接
    }
    
    println!("作用域结束,连接已断开");
}
// 输出:
// 数据库连接已建立
// 断开数据库连接:mysql://localhost:3306
// 作用域结束,连接已断开

错误示例:Rc 不能多线程

代码语言:javascript
复制
use std::rc::Rc;
use std::thread;

fn main() {
    let data = Rc::new();
    let data_clone = Rc::clone(&data);
    
    let handle = thread::spawn(move || {
        println!("线程里:{}", data_clone);
    });
    
    handle.join().unwrap();
}

编译器错误:

代码语言:javascript
复制
error[E0277]: `Rc<{integer}>` cannot be sent between threads safely
  --> src/main.rs:7:30
   |
7  |     let handle = thread::spawn(move || {
   |                  ------------- ^------
   |                  |             |
   |  ________________|_____within this `{closure@...}`
   | |                |
   | |                required by a bound introduced by this call
   |
   = help: the trait `Send` is not implemented for `Rc<{integer}>`

翻译: "Rc 不能跨线程!要用 Arc!"

🐛 常见坑点

坑点 1:混淆 Box、Rc、Arc

代码语言:javascript
复制
// ❌ 错误选择
use std::rc::Rc;

fn share_across_threads() {
    let data = Rc::new();
    thread::spawn(move || {
        println!("{}", data);  // ❌ 编译错误!
    });
}

// ✅ 正确选择
use std::sync::Arc;

fn share_across_threads() {
    let data = Arc::new();
    thread::spawn(move || {
        println!("{}", data);  // ✅ 可以
    });
}

选择指南:

  • 独占所有权 → Box
  • 单线程共享 → Rc
  • 多线程共享 → Arc

坑点 2:循环引用导致内存泄漏

代码语言:javascript
复制
use std::rc::{Rc, RefCell};
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    next: RefCell<Option<Rc<Node>>>,
}

fn main() {
    let a = Rc::new(Node {
        value: ,
        next: RefCell::new(None),
    });
    
    let b = Rc::new(Node {
        value: ,
        next: RefCell::new(Some(Rc::clone(&a))),
    });
    
    // ❌ 循环引用!
    a.next.borrow_mut().replace(Rc::clone(&b));
    
    println!("a 计数:{}", Rc::strong_count(&a));  // 2
    println!("b 计数:{}", Rc::strong_count(&b));  // 2
    
    // 即使离开作用域,计数也不会变成 0,内存泄漏!
}

解决方案:Weak 弱引用(下篇讲内部可变性时细说)

坑点 3:忘记解引用

代码语言:javascript
复制
fn main() {
    let b = Box::new();
    
    // ❌ 有些情况需要显式解引用
    let x: i32 = *b;  // 取出值
    
    // ✅ 但很多情况 Rust 自动 deref
    fn print_num(n: &i32) {
        println!("{}", n);
    }
    print_num(&b);  // 自动 deref
}

🎯 实战案例

案例 1:树形结构用 Rc

代码语言:javascript
复制
use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct TreeNode {
    value: i32,
    children: RefCell<Vec<Rc<TreeNode>>>,
}

fn main() {
    let root = Rc::new(TreeNode {
        value: ,
        children: RefCell::new(vec![]),
    });
    
    let child1 = Rc::new(TreeNode {
        value: ,
        children: RefCell::new(vec![]),
    });
    
    let child2 = Rc::new(TreeNode {
        value: ,
        children: RefCell::new(vec![]),
    });
    
    // 添加子节点
    root.children.borrow_mut().push(Rc::clone(&child1));
    root.children.borrow_mut().push(Rc::clone(&child2));
    
    println!("根节点:{:?}", root);
    println!("根节点有 {} 个子节点", root.children.borrow().len());
}

案例 2:线程池用 Arc

代码语言:javascript
复制
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // 共享计数器
    let counter = Arc::new(Mutex::new());
    let mut handles = vec![];
    
    for _ in .. {
        let counter = Arc::clone(&counter);
        
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += ;
        });
        
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("最终计数:{}", *counter.lock().unwrap());
    // 输出:最终计数:10
}

🧠 思维导图

22-智能指针
22-智能指针

📝 小结

核心要点:

  1. Box 最朴实:独占所有权,把数据放堆上,零开销
  2. Rc 单线程共享:引用计数,最后一个走的关灯
  3. Arc 多线程共享:线程安全的 Rc,有原子操作开销
  4. Deref 自动解包:智能指针可以当普通引用用
  5. Drop 自动清理:离开作用域前执行清理逻辑

选择指南:

  • 就一个人用 → Box
  • 多个人用但单线程 → Rc
  • 多个人用且多线程 → Arc

下篇预告:

等等,刚才树形结构的例子里,RefCell 是个啥?为什么 Rc 还要配个 RefCell?这就涉及到 Rust 的内部可变性了——明明是不可变的,怎么又能改了?下篇咱们来揭开这个"精神分裂"的谜团!

互动问题:

你觉得智能指针和普通指针最大的区别是啥?有没有被循环引用坑过?评论区聊聊!

🔗 参考资料

  • Rust Book - Smart Pointers
  • Rust By Example - Box
  • std::rc::Rc
  • std::sync::Arc
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Larry的Hub 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 智能指针
    • 🎬 引入
    • 📌 核心概念
      • 智能指针是啥?
      • Box:最朴实的管家
      • Rc:引用计数管家
      • Arc:线程安全的 Rc
      • Deref 特质:智能指针的"解包"能力
      • Drop 特质:离开前的"遗言"
    • 💻 代码示例
      • 示例 1:Box 的基本使用
      • 示例 2:Box 用于递归类型
      • 示例 3:Rc 的共享所有权
      • 示例 4:Arc 多线程共享
      • 示例 5:Drop 的实际应用
      • 错误示例:Rc 不能多线程
    • 🐛 常见坑点
      • 坑点 1:混淆 Box、Rc、Arc
      • 坑点 2:循环引用导致内存泄漏
      • 坑点 3:忘记解引用
    • 🎯 实战案例
      • 案例 1:树形结构用 Rc
      • 案例 2:线程池用 Arc
    • 🧠 思维导图
    • 📝 小结
    • 🔗 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档