Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 语言中学习内存管理时,你亲手写 mallocfree,能直观感受到”这块内存是我申请的,我得负责还回去”。但 Rust 刻意隐藏了这些操作——Box::new(v) 背后发生了什么?堆上怎么分配的?drop 到底做了什么?这些细节被封装在了标准库的实现中。

这种隐藏是一把双刃剑

  • 好的方面:程序员不需要关心底层细节,写出来的代码默认就是内存安全的。
  • 不好的方面:初学者难以建立”内存在堆上还是在栈上”的直观认知,也难以理解所有权规则为什么要那么严格——因为你感受不到”我申请了内存,不还就会泄漏”的紧迫感。

正是因为 Rust 隐藏了 malloc / free,所以本教程在讲解所有权(上一章)和智能指针时,会反复借用 C/C++ 的例子来”揭开盖子”——让你看到这些 Rust 类型背后实际在做的事情,建立起”内存到底在哪里、谁在管”的心智模型。


3. 为什么需要智能指针——从所有权系统的局限性出发

回顾上一章学到的所有权核心原则:

  1. 每个值有一个所有者
  2. 同一时刻只能有一个所有者(默认独占)
  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 + Syncstd::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 核心接口

创建 BoxBox::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 核心接口

创建 RcRc::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 核心接口

创建空 WeakWeak::new

功能:创建一个不指向任何数据的空弱引用(等价于已经“过期“的弱引用)。

接口签名

#![allow(unused)]
fn main() {
pub fn new() -> Weak<T>
}
  • 返回值Weak<T> — 空的弱引用,upgrade() 永远返回 None

Rc 创建 WeakRc::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> 几乎完全一致,可以对照学习:

创建 ArcArc::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 BoxDrop — 观察堆内存的自动释放

#![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 释放堆内存
}
}
  • 实验:定义一个实现了 Drop trait 的 Data 结构体,用 Box::new 将它的实例分配在堆上。访问数据后不做任何手动释放。
  • 现象:运行程序,在 demo() 函数结束时会自动打印 drop data: hello world
  • 分析Box::new 在堆上分配了 Data 实例,Box 成为这片内存的唯一所有者。当 Box 离开作用域时,编译器自动调用 Drop::drop(释放 String 内部的堆内存),然后回收 Box 指向的堆空间。整个过程没有手写 free 调用,但释放是确定的,并覆盖正常作用域退出路径。这正是 RAII 在 Rust 中的体现:资源获取在 Box::new,资源释放由编译器在作用域结束时自动插入
  • 结论Box<T> 让堆上数据也享有“自动释放“的待遇。配合 Drop trait,你可以精确观察和控制释放时机。

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 创建多个共享句柄(str1str2str3),在不同作用域中观察 Rc::strong_count 的变化。
  • 现象
    • 初始计数 = 1
    • 在内部作用域中,4 个句柄同时存在,计数 = 4
    • 离开内部作用域后,str2str3 被 drop,计数降为 2
    • 函数结束时,str1str 依次 drop,计数从 2 → 1 → 0,数据被释放
  • 分析Rc::clone 不拷贝 String 内部的堆数据——只是复制了指针并递增了引用计数。计数归零时,Rust 自动释放堆上的 StringRcInner 结构。整个过程明确且可追踪:每个 Rc 句柄都是一个所有者,最后一个所有者离开时负责释放——这是“共享所有权,明确规则“的完美演示。
  • 结论Rc<T> 用引用计数实现了多所有权共享。Rc::clone 是轻量操作(不拷贝数据),引用计数的增减是自动且确定的。