趣味学RUST基础篇(智能指针1)

发布于:2025-09-10 ⋅ 阅读:(20) ⋅ 点赞:(0)

这一节,我们来学习一下Rust中另外一个核心的知识点,智能指针,它和普通指针有什么区别嫩?我们打个比方,普通指针(比如 Rust 里的 & 引用)就像你朋友写的便条:“东西在302房间。” 它告诉你数据在哪,但仅此而已——不拥有它,也不管它干不干净。

智能指针呢?它是带脑子的“超级便条”!不仅能指路,还能:

  • 自己拥有这份数据,
  • 记录有多少人在用它,
  • 用完自动打扫卫生(释放内存),
  • 甚至还能说:“现在不能改,有人在读!”

换句话说:智能指针 = 指针 + 智能管家

其实,你早就见过它们了!比如 StringVec<T>,它们可不是普通变量——它们是堆上的“包租公”,管着一片内存,还能自己扩容、保证数据安全。

智能指针的两个“超能力”:

  1. Deref:让它能像普通引用一样被使用(比如 *ptr 解引用),无缝兼容现有代码。
  2. Drop:一出作用域,立刻自动清理现场,绝不留垃圾。

Rust 标准库里的三大“明星智能指针”:

  • Box<T>:最简单的“独居管家”,把数据丢到堆上,自己负责收尾。
  • Rc<T>:社交达人!允许多个人共享同一份数据,谁也不抢,用完自动退群。
  • RefCell<T>:特立独行的“灵活管家”,编译时不让改?没关系,我运行时检查!搭配 Ref<T>RefMut<T> 实现“内部可变性”。

智能指针不是 Rust 发明的,C++ 也有。但在 Rust 里,它们和所有权系统配合得天衣无缝,既安全又强大!当然也有坑:比如 A 指着 B,B 又指着 A —— 谁都不肯走,内存就“ leaks”了(内存泄漏)。后面我们会教你如何避开这些“死循环”。

Box<T>:给数据找个“大房子”住 —— 堆内存入门

想象一下你家很小,但你要搬一个超大的沙发进来。客厅放不下怎么办?
答案是:租个仓库!

在 Rust 里,Box<T> 就是你租的“私人小仓库”——它把数据存在堆(heap)上,而不是默认的“小客厅”(栈,stack)。

为什么需要“堆”?

Rust 默认把变量存在“栈”里,特点是:快、整齐、但空间小
而“堆”就像一个大仓库:空间大,但取东西稍微慢一点。

大多数时候,Rust 自动帮你决定放哪。但有些情况,你得手动说:“这东西太大了,放堆上吧!”
这时候,就轮到 Box<T> 登场了!


什么是 Box<T>

Box<T> 是 Rust 标准库提供的一种智能指针,它的任务很简单:

把类型为 T 的数据,放到堆上存储,自己在栈上留个“小地图”(指针)来找到它。

用法也很简单:

let x = 42;
let boxed_x = Box::new(x); // 把 42 搬到堆上,boxed_x 是个“指针+管家”
println!("{}", boxed_x);   // 输出: 42 —— 看起来就像普通变量!

虽然 boxed_x 是个指针,但它实现了 Deref,所以你可以像用普通值一样用它(比如打印、计算)。

而且!当 boxed_x 离开作用域,Box 会自动调用 Drop堆上的 42 就被清理了——不用你动手,绝不内存泄漏!

安全 + 自动 + 省心,这就是 Rust 的魅力!


Box<T> 能干啥?三个经典场景

1.当你有一个特别大的数据,不想塞满栈

栈空间有限,放太大的数组或结构体可能“爆栈”。
Box 把它扔堆上,栈只留个指针,轻装上阵!

let huge_data = Box::new([0; 1000000]); // 一百万个 0,堆上放,栈上轻松
2.实现递归类型——自己包含自己的类型

这是 Box 最酷的用法!先看个问题:

enum List {
    Cons(i32, List), //  错!编译不过!
    Nil,
}

这个“链表”看起来没问题,但它在作死:
Cons 包含一个 List,而 List 又包含 Cons…… 类型大小无限嵌套,Rust 根本算不出它有多大!

编译错误:size_of::<List>() 无穷大,不合法!

怎么办?用 Box 打破无限循环:

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

因为 Box<List> 是个指针,大小固定(比如 8 字节),不再需要知道 List 到底多大。
这样,类型大小就“收敛”了,编译通过!

举个栗子:

use List::{Cons, Nil};

let list = Cons(1,
            Box::new(Cons(2,
                Box::new(Cons(3, Box::new(Nil))))));

这就像一串火车车厢:

  • 第一节车厢(Cons 1)拉着一根“绳子”(Box),连到下一节;
  • 每节都用“绳子”连下去,最后一节是 Nil(空)。

数据在堆上串成链,栈上只存第一个指针,清爽!


Box<T> 是谁?

特性 说明
类型 智能指针
存哪 数据在堆,指针在栈
优点 自动释放、控制内存布局、支持递归类型
缺点 有轻微性能开销(间接访问)
适用场景 大数据、递归结构、性能不敏感的地方

小变量放家里(栈),大物件放仓库(堆)——Box 就是你的私人搬运工+保洁员!

它不花哨,但实用;不快,但安全;它是你进入 Rust 堆内存世界的第一把钥匙!


Deref:让智能指针“伪装”成普通引用的“变装大师”

假设你有一把万能钥匙,它长得像房卡,但其实是一把高级智能锁的控制器。你想开门,只需要像刷普通房卡一样“嘀”一下——它看起来是房卡,干的也是房卡的活,但它背后功能可多了。

在 Rust 里,Box<T>Rc<T>String 这些“智能指针”就像是这种“智能钥匙”。
它们不只是指向数据,还能自动管理内存、记录引用次数……但最关键的是:

它们能“装”成普通引用(&T),让你用起来毫无违和感!

这背后的“变装术”,就是今天的主角:Deref trait


先复习一下:什么是“解引用”?

在 Rust 中,& 是“取引用”,* 是“解引用”。

举个栗子:

let x = 5;
let y = &x;  // y 是个引用,指向 x

assert_eq!(5, *y); // 必须加 * 才能“解开”引用,拿到里面的 5

如果你写 assert_eq!(5, y),会报错:

“你不能拿一个整数和一个‘指向整数的指针’比较!”

所以:*y 的作用就是“顺着箭头找到背后的值”

智能指针也想用 *?没问题,但得“报备”!

我们自己写个“山寨版 Box”叫 MyBox<T>

struct MyBox<T>(T);

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

然后试试用它:

let x = 5;
let y = MyBox::new(x);

assert_eq!(5, *y); //  编译失败!

Rust 怒吼:

MyBox<i32> 不能被解引用!你没注册 Deref trait!”

原因很简单:
Rust 只知道怎么对 & 引用做 * 解引用。
你想让自定义类型也能用 *必须主动申请“解引用权限”——实现 Deref trait!


上演变装术:实现 Deref

只需要加这么一段:

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0  // 返回元组结构体里的第一个元素的引用
    }
}

解释一下:

  • deref(&self):当别人对 MyBox*y 时,Rust 会自动调用这个方法。
  • 它返回一个 &T —— 正是 * 操作符想要的“引用”。
  • &self.0 是因为 MyBox<T> 是个元组结构体,.0 就是里面那个值。

现在再写 *y,就能顺利通过了!

*y 实际上等价于 *(y.deref()) —— Rust 自动帮你完成了“变装 + 解引用”两步。


魔术升级:Deref 强制转换(Deref Coercion)

这才是 Deref 最酷的地方!

想象你有个函数,只接受 &str

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

现在你有个 MyBox<String>

let m = MyBox::new(String::from("Rust"));
hello(&m); //  居然能通过!

咦?&m&MyBox<String>,而函数要的是 &str,类型不匹配啊?

别急,Rust 开启了“Deref 强制转换”魔法:

  1. 它发现 MyBox<T> 实现了 Deref<Target=String>→ 所以 &MyBox<String> 可以转成 &String
  2. 又发现 String 实现了 Deref<Target=str>→ 所以 &String 可以转成 &str
  3. 最终成功匹配函数参数!

整个过程像这样:

&MyBox<String> 
   → (deref) → &String 
   → (deref) → &str

Rust 自动帮你“一路解引用”,直到类型匹配为止!

为什么这很重要?

如果没有 Deref 强制转换,上面那句得写成:

hello(&(*m)[..]); //  又臭又长,看不懂!
  • *m:先解引用成 String
  • [..]:取整个字符串的 slice
  • &:再取引用

而有了 Deref,你只需要:

hello(&m); //  干净利落,像用普通引用一样自然

这就是智能指针的“隐形铠甲”:
它们功能强大,但用起来却像普通引用一样简单。


小贴士:Deref 强制转换的三大规则

Rust 在这三种情况下会自动尝试转换:

情况 是否允许
&T&U(如果 T: Deref<Target=U>)
&mut T&mut U(如果 T: DerefMut<Target=U>)
&mut T&U(如果 T: Deref<Target=U>) 是(只读转换)

但反过来不行!比如 &T 不能转成 &mut T,否则会破坏借用规则。


网站公告

今日签到

点亮在社区的每一天
去签到