rust 智能指针
1. 什么是智能指针
在 Rust 中,普通指针(引用 &T / &mut T)只是借用数据,不拥有数据,生命周期受限于被引用值。
智能指针则是一种”升级版”的指针:它拥有指向的数据,并在此基础上附加了自动化的内存管理能力。使用体验上,智能指针和普通指针一样——可以通过 * 解引用访问内部数据,也可以通过 . 调用内部类型的方法。但在幕后,智能指针自动处理了内存的分配和释放,程序员不需要手动管理。
简单定义:智能指针 = 和普通指针一样的使用体验 + 半自动化的内存管理 + 额外的元数据和控制能力。
Rust 标准库提供了四种核心智能指针:
| 类型 | 一句话描述 |
|---|---|
Box<T> | 把数据分配在堆上,独占所有权 |
Rc<T> | 引用计数共享所有权,单线程 |
Weak<T> | Rc 的弱引用,打破循环引用 |
Arc<T> | 原子引用计数共享所有权,引用计数可跨线程安全使用 |
在深入每个智能指针之前,必须先理解它们共同的理论基础——RAII,以及它们要解决的所有权系统的局限性。
2. 智能指针的核心思想:RAII
2.1 什么是 RAII
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 提出的一种资源管理思想,Rust 完全继承并强化了它。
核心逻辑:
- 资源的获取(内存分配、文件打开、锁获取等)在对象**初始化(构造)**时完成。
- 资源的释放(内存释放、文件关闭、锁释放等)在对象**离开作用域(析构)**时自动完成。
- 资源绑定到某个值的生命周期上。当这个值离开作用域时,释放逻辑自动执行;如果多个局部值按作用域退出,释放顺序也会被确定地安排。
用一个 C++ 的例子来直观感受:
// 没有 RAII:手动管理,容易出错
void bad_example() {
int* p = new int(10); // 分配
// ... 如果这里发生异常或提前 return
delete p; // 这一行可能永远不会执行 → 内存泄漏
}
// 使用 RAII:std::unique_ptr
#include <memory>
void good_example() {
auto p = std::make_unique<int>(10); // 构造 = 分配
// ... 即使发生异常
} // 正常离开作用域或异常展开时,析构函数自动 delete
RAII 的核心价值在于:把”手动记得释放”变成了”作用域结束时自动释放”,大幅减少因忘记释放导致的泄漏。
2.2 Rust 中的 RAII:Drop trait
Rust 没有 C++ 那种构造函数 / 析构函数的语法,而是通过 Drop trait 来实现 RAII:
#![allow(unused)]
fn main() {
// Drop trait 的定义(标准库中)
pub trait Drop {
fn drop(&mut self); // 在值离开作用域时自动调用
}
}
当一个实现了 Drop 的值离开作用域时,编译器会自动调用 drop 方法,完成资源清理。这个过程是:
- 确定性的:你知道它一定会在作用域结束时执行。
- 覆盖正常退出路径:即使有提前
return、?提前传播错误,编译器也会在退出路径上插入drop调用。默认的 unwind panic 也会展开栈并执行析构;如果项目配置为panic = "abort",进程会直接终止,不会逐层运行析构。 - 只调用一次:编译器保证不会被重复释放。
以 Box<T> 为例:
fn main() {
let b = Box::new(42); // 堆上分配内存,b 是这片内存的所有者
// ... 使用 *b 访问数据
} // b 离开作用域 → Box 的 drop 自动被调用 → 堆内存被释放
2.3 Rust 对初学者隐藏了直接的内存操作
一个值得注意的事实:在 safe Rust 中,你无法直接调用类似 C 语言的 malloc / free 这样的内存分配函数。
在 C 语言中学习内存管理时,你亲手写 malloc 和 free,能直观感受到”这块内存是我申请的,我得负责还回去”。但 Rust 刻意隐藏了这些操作——Box::new(v) 背后发生了什么?堆上怎么分配的?drop 到底做了什么?这些细节被封装在了标准库的实现中。
这种隐藏是一把双刃剑:
- 好的方面:程序员不需要关心底层细节,写出来的代码默认就是内存安全的。
- 不好的方面:初学者难以建立”内存在堆上还是在栈上”的直观认知,也难以理解所有权规则为什么要那么严格——因为你感受不到”我申请了内存,不还就会泄漏”的紧迫感。
正是因为 Rust 隐藏了
malloc/free,所以本教程在讲解所有权(上一章)和智能指针时,会反复借用 C/C++ 的例子来”揭开盖子”——让你看到这些 Rust 类型背后实际在做的事情,建立起”内存到底在哪里、谁在管”的心智模型。
3. 为什么需要智能指针——从所有权系统的局限性出发
回顾上一章学到的所有权核心原则:
- 每个值有一个所有者
- 同一时刻只能有一个所有者(默认独占)
- 所有者离开作用域,值自动释放
这三条规则覆盖了大多数场景,而且是零成本的。但有些场景下,它们不够灵活:
场景一:需要在堆上分配数据,且大小在编译时无法确定
所有权规则本身不限制数据放在哪里——栈上的变量也会被自动管理。问题是,栈空间有限,且大小必须在编译时确定。递归数据结构(如链表节点、树节点)的大小是”递归的”,编译器算不出来;大对象放在栈上也可能爆栈。你需要显式地把数据放在堆上,而所有权系统需要一种”拥有堆上数据的栈上代理”。
→ 解决方案:Box<T>——把数据放堆上,Box 本身在栈上作为所有者,遵循所有权的所有规则。
场景二:多个地方需要共享同一个数据的所有权
默认所有权规则要求”同一时刻只有一个所有者”。但现实中,多个模块、多个数据结构可能需要共同持有一份数据,而且无法提前确定”谁最后一个用完”。比如一个 GUI 应用中,多个面板都需要访问同一个数据模型。
所有权转移(move)解决不了这个问题——移走了自己就没了。借用(reference)也解决不了——借用的生命周期被限制在原所有者的作用域内,无法”独立存活”。
→ 解决方案:Rc<T> / Arc<T>——引用计数允许多个所有者共享数据,当最后一个所有者离开作用域时数据自动释放。Rc 用于单线程,Arc 用于多线程。
场景三:共享所有权时可能产生循环引用
Rc<T> 的引用计数有一个致命弱点:如果 A 持有 B 的 Rc,B 也持有 A 的 Rc,它们的引用计数永远不为 0,导致内存泄漏。这在树结构(父节点 ↔ 子节点)、图结构、观察者模式等场景中非常常见。需要一个”不增加引用计数、但能访问数据”的引用。
→ 解决方案:Weak<T>——弱引用不增加强引用计数,可以被”升级”(upgrade)为临时的 Rc 来访问数据。如果原数据已经被释放,升级操作返回 None,安全地避免了悬垂指针。
总结:
所有权默认规则(独占) → 大部分场景,零成本
↓ 不够用
Box<T> → 堆分配 + 独占所有权
Rc<T> → 单线程共享所有权(引用计数)
Arc<T> → 多线程共享所有权(原子引用计数)
Weak<T> → 配合 Rc/Arc,打破循环引用
接下来,逐个深入这四个智能指针的接口和使用方式。
4. 四种智能指针概览
| 类型 | 作用 | 多所有权 | 线程安全 | 类比 C++ |
|---|---|---|---|---|
Box<T> | 堆上分配,独占所有权 | 否 | 是 | std::unique_ptr<T> |
Rc<T> | 引用计数,单线程共享所有权 | 是 | 否 | std::shared_ptr<T> |
Weak<T> | 配合 Rc 的弱引用,不增加强计数 | 是(弱引用) | 否 | std::weak_ptr<T> |
Arc<T> | 原子引用计数,多线程共享所有权 | 是 | 引用计数线程安全;共享要求 T: Send + Sync | std::shared_ptr<T> 的原子化共享场景 |
5. 四种智能指针详解
5.1 Box<T> —— 堆上分配的独占所有权
5.1.1 功能定位
Box<T> 是最简单的智能指针,它只做一件事:将数据分配在堆上,并独占所有权。Box<T> 本身在栈上(大小 = 一个指针),它指向的数据在堆上。当 Box 离开作用域时,堆上的数据自动被释放。
5.1.2 内部结构
Box<T> {
指针: *mut T // 指向堆上 T 类型数据的裸指针
}
Box<T> 的大小固定为 usize(一个指针的大小),无论 T 有多大。
5.1.3 核心接口
创建 Box — Box::new
功能:在堆上分配内存存放 x,返回的 Box<T> 成为这片内存的唯一所有者。
接口签名:
#![allow(unused)]
fn main() {
pub fn new(x: T) -> Box<T>
}
- 参数:
x— 任意类型的值,所有权移入Box。 - 返回值:
Box<T>— 指向堆上数据的智能指针。
简单调用示例:
#![allow(unused)]
fn main() {
let b = Box::new(42); // i32 分配在堆上
let s = Box::new(String::from("hello")); // String 本身也在堆上,Box 在栈上指向它
}
解引用访问内部数据 — *
功能:通过 * 解引用操作符访问 Box 内部的堆上数据,就像使用普通引用一样。Box<T> 实现了 Deref<Target = T> 和 DerefMut trait,因此 . 方法调用会自动穿透到内部类型。
简单调用示例:
#![allow(unused)]
fn main() {
let mut b = Box::new(10);
*b += 5; // 解引用修改堆上的值
println!("{}", *b); // 输出 15
let s = Box::new(String::from("hello"));
println!("{}", s.len()); // 自动解引用,直接调用 String 的 len()
}
消耗 Box 取出内部值 — *(解引用移动)
功能:对 Box<T> 使用 * 解引用,会将内部值移出(move out),Box 被消耗。这相当于把所有权从“堆上“移回“栈上“。
简单调用示例:
#![allow(unused)]
fn main() {
let b = Box::new(String::from("hello"));
let inner: String = *b; // 所有权从 Box 移出到 inner
// b 已经不再可用(被 consume 了)
println!("{}", inner);
}
5.1.4 典型使用场景
场景一:存放编译时大小未知的类型(递归类型)
#![allow(unused)]
fn main() {
// 链表节点的经典定义:Box 打破了递归大小的循环
enum List {
Cons(i32, Box<List>), // Box 让 Cons 的大小固定
Nil,
}
use List::*;
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
场景二:减少大对象在栈上的占用
#![allow(unused)]
fn main() {
let large_array = Box::new([0u8; 1_000_000]); // ~1MB,放堆上而不是栈上
}
场景三:trait 对象(动态分发)
#![allow(unused)]
fn main() {
let components: Vec<Box<dyn Draw>> = vec![
Box::new(Button { label: "OK".into() }),
Box::new(TextField { placeholder: "name".into() }),
];
}
5.2 Rc<T> —— 单线程引用计数共享所有权
5.2.1 功能定位
Rc<T>(Reference Counted)允许同一份数据同时有多个所有者。内部通过引用计数追踪所有者的数量:每次 clone 计数 +1,每个所有者离开作用域时计数 -1,计数归零时数据被释放。
Rc不是线程安全的(计数操作不是原子操作),只能用于单线程场景。
5.2.2 内部结构
Rc<T> {
指针: 指向堆上的 RcInner {
强引用计数: usize, // Rc::clone() 修改这里
弱引用计数: usize, // Weak 修改这里
数据: T,
}
}
5.2.3 核心接口
创建 Rc — Rc::new
功能:在堆上分配数据并包装为 Rc,初始强引用计数为 1。
接口签名:
#![allow(unused)]
fn main() {
pub fn new(value: T) -> Rc<T>
}
- 参数:
value— 需要共享的数据。 - 返回值:
Rc<T>— 初始强引用计数 = 1 的智能指针。
简单调用示例:
#![allow(unused)]
fn main() {
use std::rc::Rc;
let a = Rc::new(42); // 强引用计数 = 1
}
增加引用计数 — Rc::clone
功能:创建另一个指向同一份数据的 Rc<T>,强引用计数 +1。注意:这不是深度拷贝数据,只是复制指针并递增计数——成本极低(等同于拷贝一个指针 + 一次整数加法)。
接口签名:
#![allow(unused)]
fn main() {
pub fn clone(this: &Rc<T>) -> Rc<T>
}
- 参数:
this— 对已有Rc的引用。 - 返回值:
Rc<T>— 新的Rc句柄,指向同一份数据。
简单调用示例:
#![allow(unused)]
fn main() {
let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a); // 强引用计数变为 2(推荐写法)
let c = a.clone(); // 等价写法,但 Rc::clone(&a) 更明确
// a、b、c 指向完全相同的 Vec,没有发生数据拷贝
}
查看引用计数 — Rc::strong_count / Rc::weak_count
功能:查询当前 Rc 的强引用计数和弱引用计数(调试和验证用)。
接口签名:
#![allow(unused)]
fn main() {
pub fn strong_count(this: &Rc<T>) -> usize
pub fn weak_count(this: &Rc<T>) -> usize
}
简单调用示例:
#![allow(unused)]
fn main() {
let a = Rc::new(10);
println!("strong = {}", Rc::strong_count(&a)); // 1
let b = Rc::clone(&a);
println!("strong = {}", Rc::strong_count(&a)); // 2
}
创建弱引用 — Rc::downgrade
功能:从 Rc 创建一个 Weak(弱引用),不增加强引用计数。详见 5.3 节。
接口签名:
#![allow(unused)]
fn main() {
pub fn downgrade(this: &Rc<T>) -> Weak<T>
}
尝试取出内部值 — Rc::try_unwrap
功能:如果当前强引用计数为 1(你是最后一个持有者),返回 Ok(内部值),消耗 Rc 并归还数据;如果还有其他持有者,返回 Err(self)。
接口签名:
#![allow(unused)]
fn main() {
pub fn try_unwrap(this: Rc<T>) -> Result<T, Rc<T>>
}
简单调用示例:
#![allow(unused)]
fn main() {
let a = Rc::new(42);
let inner = Rc::try_unwrap(a).unwrap(); // 只有 1 个引用,成功取出
println!("{}", inner);
let a = Rc::new(42);
let b = Rc::clone(&a);
// Rc::try_unwrap(a).unwrap(); // panic:还有 b 持有引用
}
5.2.4 综合示例
use std::rc::Rc;
fn main() {
let shared = Rc::new(String::from("共享数据"));
// 多处"克隆" Rc 句柄,共享同一份数据
let owner1 = Rc::clone(&shared);
let owner2 = Rc::clone(&shared);
let owner3 = Rc::clone(&shared);
println!("强引用计数: {}", Rc::strong_count(&shared)); // 4
println!("内容: {}", owner1); // 自动解引用到 String
// owner1, owner2, owner3 各自离开作用域,计数递减
}
// shared 最后离开,计数 1→0,数据被释放
5.3 Weak<T> —— 弱引用,解决循环引用
5.3.1 功能定位
Weak<T> 是对 Rc<T>(或 Arc<T>)内部数据的非拥有型引用。它的核心作用是打破循环引用:
- 不增加强引用计数,因此不会阻止数据被释放。
- 访问数据前需要先 “升级”(upgrade)为临时的
Rc<T>——这一步可能失败(数据已释放时返回None),从而安全地处理了“数据可能不存在“的情况。
5.3.2 内部结构
Weak<T> {
指针: 指向 RcInner(和 Rc 指向同一个内部结构)
}
与 Rc<T> 的区别:Weak 不参与强引用计数的增减,只修改弱引用计数。
5.3.3 核心接口
创建空 Weak — Weak::new
功能:创建一个不指向任何数据的空弱引用(等价于已经“过期“的弱引用)。
接口签名:
#![allow(unused)]
fn main() {
pub fn new() -> Weak<T>
}
- 返回值:
Weak<T>— 空的弱引用,upgrade()永远返回None。
从 Rc 创建 Weak — Rc::downgrade
功能:从已有的 Rc 创建一个 Weak 弱引用,弱引用计数 +1,强引用计数不变。
接口签名:
#![allow(unused)]
fn main() {
pub fn downgrade(this: &Rc<T>) -> Weak<T>
}
- 参数:
this— 对已有Rc的引用。 - 返回值:
Weak<T>— 指向同一份数据的弱引用。
升级为强引用 — Weak::upgrade
功能:尝试将弱引用“升级“为临时的 Rc<T>。如果原数据还存在(强引用计数 > 0),返回 Some(Rc<T>),此时你可以安全访问数据;如果原数据已被释放(强引用计数 = 0),返回 None。
接口签名:
#![allow(unused)]
fn main() {
pub fn upgrade(&self) -> Option<Rc<T>>
}
- 参数:
&self。 - 返回值:
Option<Rc<T>>—Some表示数据仍存活,临时获得一个强引用;None表示数据已被释放。
简单调用示例:
#![allow(unused)]
fn main() {
use std::rc::{Rc, Weak};
let strong = Rc::new(42);
let weak = Rc::downgrade(&strong);
// 升级成功:数据还在
match weak.upgrade() {
Some(rc) => println!("数据: {}", *rc), // 输出 42
None => println!("数据已释放"),
}
drop(strong); // 释放唯一的强引用,数据被销毁
// 升级失败:数据已释放
match weak.upgrade() {
Some(_) => println!("还存在"),
None => println!("数据已释放"), // 输出这个
}
}
5.3.4 循环引用问题详解
没有 Weak 时会发生什么:
#![allow(unused)]
fn main() {
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
next: RefCell<Option<Rc<Node>>>,
}
let a = Rc::new(Node { next: RefCell::new(None) });
let b = Rc::new(Node { next: RefCell::new(Some(Rc::clone(&a))) });
*a.next.borrow_mut() = Some(Rc::clone(&b)); // a → b, b → a,循环引用!
// a 和 b 的强引用计数各有 2,离开作用域后各降为 1(互相持有)
// 永远不会归零 → 内存泄漏
}
用 Weak 打破循环(以树结构的父子关系为例):
#![allow(unused)]
fn main() {
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // 父节点用弱引用
children: RefCell<Vec<Rc<Node>>>, // 子节点用强引用
}
}
设计原则:父子关系中,父持有子的 Rc(强引用),子持有父的 Weak(弱引用)。这样当父被释放时,子的 Weak 升级失败,自然知道“父已消失“;而子被释放时,父的 Rc 计数减少,不会阻止父的释放。没有循环,引用计数正常归零。
完整示例见 src/study/smartpointer_demo.rs。
5.4 Arc<T> —— 多线程安全的原子引用计数
5.4.1 功能定位
Arc<T>(Atomic Reference Counted)是 Rc<T> 的线程安全版本。和 Rc 一样允许共享所有权,但内部引用计数使用原子操作(atomic),因此 Arc 句柄本身可以安全地在多线程之间传递和克隆。
需要注意:Arc 只保证“引用计数”本身是线程安全的,不会自动让内部数据变成可并发修改。如果多个线程需要修改同一份数据,通常要配合 Mutex<T>、RwLock<T> 或原子类型使用。
原子操作有性能开销(比普通整数操作慢),因此单线程中优先用
Rc,只在需要跨线程共享时才用Arc。
5.4.2 内部结构
Arc<T> {
指针: 指向堆上的 ArcInner {
强引用计数: AtomicUsize, // 原子操作
弱引用计数: AtomicUsize, // 原子操作
数据: T,
}
}
5.4.3 核心接口
Arc<T> 的接口与 Rc<T> 几乎完全一致,可以对照学习:
创建 Arc — Arc::new
功能:在堆上分配数据,初始强引用计数为 1,可以安全地在线程间共享。
接口签名:
#![allow(unused)]
fn main() {
pub fn new(data: T) -> Arc<T>
}
- 参数:
data— 需要共享的数据。 - 返回值:
Arc<T>— 初始强引用计数 = 1 的原子引用计数指针。
增加引用计数 — Arc::clone
功能:创建另一个指向同一份数据的 Arc,强引用计数原子地 +1。和 Rc::clone 一样,不拷贝数据本身,代价极低。
接口签名:
#![allow(unused)]
fn main() {
pub fn clone(this: &Arc<T>) -> Arc<T>
}
- 参数:
this— 对已有Arc的引用。 - 返回值:
Arc<T>— 新的共享句柄。
简单调用示例:
#![allow(unused)]
fn main() {
use std::sync::Arc;
let a = Arc::new(vec![1, 2, 3]);
let b = Arc::clone(&a); // 原子地增加计数
// a 和 b 可在不同线程中使用
}
查看引用计数 — Arc::strong_count / Arc::weak_count
功能:查询当前强/弱引用计数。注意:在多线程环境中,返回值是“瞬时快照“,在你使用这个值时计数可能已经变了。
接口签名:
#![allow(unused)]
fn main() {
pub fn strong_count(this: &Arc<T>) -> usize
pub fn weak_count(this: &Arc<T>) -> usize
}
创建弱引用 — Arc::downgrade
功能:从 Arc 创建 Weak<T>,用法与 Rc::downgrade 完全一致,也是线程安全的。
接口签名:
#![allow(unused)]
fn main() {
pub fn downgrade(this: &Arc<T>) -> Weak<T>
}
5.4.4 多线程示例
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let mut handles = vec![];
for i in 0..3 {
let data_clone = Arc::clone(&data); // 每个线程获得一份 Arc 句柄
let handle = thread::spawn(move || {
println!("线程 {} 看到的数据: {:?}", i, data_clone);
});
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
// 所有线程完成,最后一个 Arc 离开作用域,数据被释放
}
配合 Mutex / RwLock 实现多线程修改:
#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0)); // Arc<Mutex<i32>>
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1; // 互斥地修改
});
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
println!("最终计数: {}", *counter.lock().unwrap()); // 10
}
模式:
Arc<Mutex<T>>是 Rust 中最常见的多线程共享可变数据模式。Arc负责“让多个线程共享所有权“,Mutex负责“同一时刻只有一个线程能修改“。
6. Rust 智能指针 – 和 C++ 作对照
看到这里,如果你学过 C++,会发现 Rust 的智能指针思想和机制很接近,但 Rust 会把更多约束放进类型系统里。
一个典型区别是:C++ 中 std::shared_ptr<T> 的引用计数操作本身是线程安全的,但它不自动保证 T 的并发访问安全;Rust 则进一步区分单线程的 Rc<T> 和可跨线程的 Arc<T>,编译器不会让你把 Rc<T> 发送到其他线程。
| 功能 | C++ 智能指针 | Rust 智能指针 |
|---|---|---|
| 唯一所有权,堆上分配 | std::unique_ptr<T> | Box<T> |
| 多个所有权(引用计数) | std::shared_ptr<T> | Rc<T>(单线程) |
| 弱引用,解决循环引用 | std::weak_ptr<T> | Weak<T> |
| 原子引用计数(线程安全) | std::atomic<std::shared_ptr<T>> / 原子化 shared_ptr 操作 | Arc<T>(多线程) |
7. 实验代码示例
配套代码位于 src/study/smartpointer_demo.rs 。
7.1 Box 与 Drop — 观察堆内存的自动释放
#![allow(unused)]
fn main() {
use std::rc::Rc;
struct Data {
data: String,
}
// 为 Data 实现 Drop trait,在释放时打印日志
impl Drop for Data {
fn drop(&mut self) {
println!("drop data: {}", self.data);
}
}
pub fn demo() {
println!("...............智能指针示例开始.................");
// ===== 实验1:Box<T> 堆分配 + Drop 观察 =====
let data = Box::new(Data {
data: "hello world".to_string(),
});
println!("data: {}", data.data);
// data 离开作用域 → Drop::drop 被调用 → 打印 "drop data: hello world"
// → Box 释放堆内存
}
}
- 实验:定义一个实现了
Droptrait 的Data结构体,用Box::new将它的实例分配在堆上。访问数据后不做任何手动释放。 - 现象:运行程序,在
demo()函数结束时会自动打印drop data: hello world。 - 分析:
Box::new在堆上分配了Data实例,Box成为这片内存的唯一所有者。当Box离开作用域时,编译器自动调用Drop::drop(释放String内部的堆内存),然后回收Box指向的堆空间。整个过程没有手写free调用,但释放是确定的,并覆盖正常作用域退出路径。这正是 RAII 在 Rust 中的体现:资源获取在Box::new,资源释放由编译器在作用域结束时自动插入。 - 结论:
Box<T>让堆上数据也享有“自动释放“的待遇。配合Droptrait,你可以精确观察和控制释放时机。
7.2 Rc<T> — 观察引用计数的变化
#![allow(unused)]
fn main() {
pub fn demo() {
// ===== 实验2:Rc<T> 引用计数 =====
let str = std::rc::Rc::new("hello world".to_string());
println!("rc count is {}", Rc::strong_count(&str)); // 1
let str1 = str.clone(); // 克隆 Rc 句柄,计数 +1
{
let str2 = str.clone(); // 计数 +1
let str3 = str.clone(); // 计数 +1
println!("rc count is {}", Rc::strong_count(&str)); // 4
} // str2, str3 离开作用域,计数各 -1
println!("rc count is {}", Rc::strong_count(&str)); // 2
println!("...............智能指针示例结束.................");
} // str1 离开,计数 -1;str 离开,计数 1→0,数据被释放
}
- 实验:创建一个
Rc<String>,通过Rc::clone创建多个共享句柄(str1、str2、str3),在不同作用域中观察Rc::strong_count的变化。 - 现象:
- 初始计数 = 1
- 在内部作用域中,4 个句柄同时存在,计数 = 4
- 离开内部作用域后,
str2和str3被 drop,计数降为 2 - 函数结束时,
str1和str依次 drop,计数从 2 → 1 → 0,数据被释放
- 分析:
Rc::clone不拷贝String内部的堆数据——只是复制了指针并递增了引用计数。计数归零时,Rust 自动释放堆上的String和RcInner结构。整个过程明确且可追踪:每个Rc句柄都是一个所有者,最后一个所有者离开时负责释放——这是“共享所有权,明确规则“的完美演示。 - 结论:
Rc<T>用引用计数实现了多所有权共享。Rc::clone是轻量操作(不拷贝数据),引用计数的增减是自动且确定的。