讲解
智能指针是「行为像指针、但带额外能力」的结构体。普通引用 &T 只是借用,智能指针通常拥有数据。标准库里三个最常用:Box、Rc、RefCell,分别解决三个不同的问题。
Box 最简单:把数据放到堆上,指针本身在栈上,唯一所有权。两个典型用途:一是递归类型(链表、树)——编译器需要知道类型大小,而递归嵌套大小无限,Box 的固定大小指针打破循环(enum List { Cons(i32, Box), Nil });二是大数据想避免栈上拷贝时。解引用用 *,和普通引用体验一致(Deref trait 的功劳)。
Rc(reference counted)提供同一数据多个所有者:Rc::clone 增加引用计数(注意是克隆计数不是克隆数据),计数归零才释放。只适合单线程(多线程用 Arc,第 24 章)。RefCell 则把借用规则从编译期挪到运行期检查:borrow()/borrow_mut() 拿到引用,规则违反时不是编译报错而是运行时 panic——这叫内部可变性(interior mutability),用于编译器分析不出、但你确知安全的场景。Rc<RefCell> 的组合=「多个所有者 + 可修改」,是图结构、回调共享状态的经典配方,代价是规则靠运行期兜底,写的时候要自己守住纪律。
示例
Box:堆分配与递归类型:
enum List {
Cons(i32, Box<List>),
Nil,
}
fn sum(list: &List) -> i32 {
match list {
List::Cons(value, next) => value + sum(next),
List::Nil => 0,
}
}
fn main() {
let b = Box::new(5);
assert_eq!(*b, 5); // 解引用和普通引用一样
// 没有 Box,这个递归类型大小无限,编译不过
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
assert_eq!(sum(&list), 3);
println!("链表求和:{}", sum(&list));
}
Rc 共享所有权 + RefCell 内部可变性:
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
// Rc:多个所有者共享一份数据
let data = Rc::new(String::from("共享配置"));
let reader1 = Rc::clone(&data);
let reader2 = Rc::clone(&data);
assert_eq!(Rc::strong_count(&data), 3);
println!("{reader1} / {reader2}");
// RefCell:运行期借用检查,编译期看不出的可变
let log: RefCell<Vec<String>> = RefCell::new(Vec::new());
log.borrow_mut().push(String::from("启动"));
{
let mut guard = log.borrow_mut();
guard.push(String::from("就绪"));
} // guard 在这里归还借用
assert_eq!(log.borrow().len(), 2);
println!("日志条数:{}", log.borrow().len());
}
常见坑
- Rc 造成循环引用:A 持有 Rc、B 持有 Rc,计数永远到不了零——内存泄漏(是泄漏不是崩溃)。用 Weak 降级其中一个方向。
- RefCell 重复可变借用:同一时刻两次 borrow_mut()(或 borrow 与 borrow_mut 重叠)直接 panic;借用守卫用完尽快离开作用域。
- 把 Rc/RefCell 带进线程:两者都不是线程安全的,跨线程用编译报错——多线程版是 Arc 与 Mutex(下一章)。
- 拿 Box 当共享指针用:Box 是唯一所有权;想共享用 Rc。三个指针各司其职,别混。
小结
Box 堆分配破递归;Rc 单线程共享所有权;RefCell 运行期借用检查;Rc<RefCell> 共享且可改。下一章把借用规则的价值推到顶点:并发。