Rust中的智能指针
Rust 中,智能指针是管理堆内存的核心工具,它们通过封装指针并添加额外功能(如所有权管理、引用计数等)来提供更安全的内存管理。
智能指针
智能指针本质是 “拥有数据所有权的结构体”,通过实现以下两个关键 trait 模拟指针行为:
Dereftrait:允许智能指针像普通引用一样被解引用(如*ptr),简化使用。Droptrait:定义智能指针离开作用域时的 “清理逻辑”(如释放堆内存、减少引用计数),实现自动内存管理。
常见的智能指针:
| 智能指针 | 特点 | 所有权规则 |
|---|---|---|
Box<T> | 将数据分配在堆上 | 独占所有权(不可复制) |
Rc<T> | 引用计数共享 | 多个所有者共享数据,只读,单线程共享 |
Arc<T> | 原子引用计数 | 多线程共享、线程安全 |
RefCell<T> | 内部可变性 | 运行时借用检查,单线程中可变共享 |
Mutex<T> | 互斥锁封装 | 多线程中安全的可变共享 |
RwLock<T> | 读写锁封装 | 多读单写共享 |
Box
Box<T>(“盒子”)是最基础的智能指针,用于将数据存储在堆上,而Box自身(指针)存储在栈上:
- 独占所有权:一个
Box拥有堆数据的唯一所有权,转移Box会转移所有权。 - 自动释放:当
Box离开作用域时,会调用Drop释放堆上的数据。
使用场景:
- 编译时大小不确定的类型(如递归类型)
- 转移大量数据时避免栈复制(直接转移
Box指针,而非堆数据) - 实现 trait 对象(
dyn Trait),如集合中存储多种类型数据时
#[derive(Debug)]
enum MyList {Cons(i32, Box<MyList>), // 必须为Box,此处为递归,大小不确定Nil,
}use MyList::{Cons, Nil};
let list = Cons(1, Box::new(Cons(2, Box::new(Nil))));
println!("Recursive list: {:?}", list);
常用方法
| 方法 | 说明 |
|---|---|
Box::new(value) | 创建一个堆上分配的对象 |
*box | 解引用,访问内部值 |
Box::leak(box) | 将 Box 转为 'static 引用(泄露内存) |
Box::into_raw(box) | 转为裸指针(不再自动释放) |
Box::from_raw(ptr) | 从裸指针恢复(恢复自动释放) |
作为trait对象:
// 定义trait
trait Shape {fn area(&self) -> f64;
}// 实现trait的结构体
struct Circle { radius: f64 }
impl Shape for Circle {fn area(&self) -> f64 { std::f64::consts::PI * self.radius.powf(2.0) }
}struct Square { side: f64 }
impl Shape for Square {fn area(&self) -> f64 { self.side.powf(2.0) }
}fn main() {// 用Box<dyn Shape>存储不同类型的Shape实现let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { radius: 1.0 }),Box::new(Square { side: 2.0 }),];// 动态调用area方法(运行时确定具体类型)for shape in shapes {println!("面积:{:.2}", shape.area()); // 输出:3.14(圆)、4.00(正方形)}
}
Rc
Rc<T>(Reference Counted,引用计数)用于单线程中多个所有者共享同一份堆数据。它会在堆上维护一个 “引用计数”,当计数归零时自动释放数据。
- 共享所有权:通过
Rc::clone(&rc)创建新引用,引用计数 +1;每个引用离开作用域时计数 -1。 - 单线程限制:
Rc<T>的引用计数操作不是原子的,线程不安全,不能用于多线程。 - 只读访问:
Rc<T>只能提供不可变引用(避免数据竞争)。
常用方法:
| 方法 | 说明 |
|---|---|
Rc::new(value) | 创建一个引用计数智能指针 |
Rc::clone(&rc) | 增加引用计数(轻量) |
Rc::strong_count(&rc) | 获取当前强引用计数 |
Rc::weak_count(&rc) | 获取当前弱引用计数 |
Rc::downgrade(&rc) | 获取弱引用(不增加强计数) |
查看引用计数:
use std::rc::Rc;fn main() {let a = Rc::new(String::from("hello"));let b = Rc::clone(&a);let c = Rc::clone(&a);println!("count = {}", Rc::strong_count(&a)); // 输出 3println!("{}", b);
} // 所有 Rc 离开作用域后才释放堆内存
Arc
Arc<T>(Atomic Rc)是Rc<T>的线程安全版本,其引用计数操作通过原子指令实现,可用于多线程环境。
- 跨线程共享:允许在多个线程中共享数据(需配合
Send/Synctrait)。 - 原子操作:计数增减是原子的,避免多线程竞争问题(但性能略低于
Rc<T>)。
常用方法:
| 方法 | 说明 |
|---|---|
Arc::new(value) | 创建智能指针 |
Arc::clone(&arc) | 增加引用计数(原子操作) |
Arc::strong_count(&arc) | 当前强引用计数 |
Arc::downgrade(&arc) | 获取弱引用 |
多线程引用计数:
use std::sync::Arc;
use std::thread;pub fn arc_test() {let data = Arc::new(100); // 堆上的数据,原子引用计数=1let mut handles = vec![];// 创建3个线程共享datafor i in 0..3 {let d = Arc::clone(&data); // 计数+1(原子操作)handles.push(thread::spawn(move || {println!("i: {}", d);}));}println!("before ref-count: {:?}", Arc::strong_count(&data));for h in handles {h.join().unwrap();}println!("after ref-count: {:?}", Arc::strong_count(&data)); // 原子引用计数=1
}
RefCell
RefCell<T>用于编译期不满足借用规则,但运行时可安全修改数据的场景。它实现了 “内部可变性”(Interior Mutability):允许通过不可变引用修改数据,借用规则的检查推迟到运行时(违反时触发panic)。
- 运行时检查:通过
borrow()(不可变借用)和borrow_mut()(可变借用)获取内部数据的引用,运行时确保 “同一时间最多一个可变引用,或多个不可变引用”。 - 单线程限制:
RefCell<T>非线程安全,不能跨线程使用。
常用方法:
| 方法 | 说明 |
|---|---|
RefCell::new(value) | 创建一个内部可变容器 |
borrow() | 不可变借用(运行时检查) |
borrow_mut() | 可变借用(运行时检查) |
.try_borrow()/ .try_borrow_mut() | 尝试借用,返回 Result避免 panic |
在Rc中嵌套使用
use std::rc::Rc;
use std::cell::RefCell;pub fn refcell_test() {let shared_data = Rc::new(RefCell::new(0)); // 堆上的0,可共享且修改let a = Rc::clone(&shared_data);let b = Rc::clone(&shared_data);*a.borrow_mut() += 10; // a修改数据*b.borrow_mut() += 5; // b修改数据println!("{}", shared_data.borrow()); // 输出15
}
Mutex
多线程并发编程的核心同步原语之一,用于在多个线程之间安全地共享和修改数据;Mutex<T> 本身不提供共享所有权,一般需要将其包裹在 Arc<T>中,在在多个线程间共享:
- 多线程可变共享;
- 确保同一时间只有一个线程访问。
常用方法:
| 方法 | 说明 |
|---|---|
Mutex::new(value) | 创建互斥锁 |
lock() | 获取锁(阻塞) |
try_lock() | 尝试获取锁(立即返回 Result) |
into_inner() | 取出内部值(消耗锁) |
与Arc一起在多线程中使用:
use std::sync::{Arc, Mutex};
use std::thread;fn main() {let counter = Arc::new(Mutex::new(0));let mut handles = vec![];for _ in 0..5 {let c = Arc::clone(&counter);handles.push(thread::spawn(move || {let mut num = c.lock().unwrap();*num += 1;}));}for h in handles {h.join().unwrap();}println!("Result: {}", *counter.lock().unwrap());
}
RwLock
允许多个线程同时读取共享数据,但写入时必须独占访问,从而在保证线程安全的同时提升并发性能。
- 高并发读场景;
- 多线程下支持多个读取者或一个写入者。
- 多个读锁可同时存在;
- 写锁独占;
- 若写锁被持有,读锁将阻塞。
常用方法:
| 方法 | 说明 |
|---|---|
RwLock::new(value) | 创建读写锁 |
read() | 获取只读锁(可同时多个) |
write() | 获取写锁(独占) |
try_read()/ try_write() | 尝试非阻塞获取 |
多读少写场景:
use std::sync::RwLock;
use std::thread;let data = RwLock::new(0);// 启动一个写线程
let w_handle = thread::spawn(move || {let mut w = data.write().unwrap();thread::sleep(std::time::Duration::from_millis(100));*w = 42;
});// 启动多个读线程
let mut r_handles = vec![];
for _ in 0..3 {let r_data = data.clone();let handle = thread::spawn(move || {let r = r_data.read().unwrap(); // 会被写线程阻塞,直到写完成println!("Read: {}", *r);});r_handles.push(handle);
}w_handle.join().unwrap();
for h in r_handles { h.join().unwrap(); }
Weak
Weak<T>是Rc<T>/Arc<T>的弱引用,不增加强引用计数,用于打破循环引用,避免内存泄漏。
常用方法:
| 方法 | 说明 |
|---|---|
Weak::new() | 创建空的弱引用 |
Rc::downgrade(&rc) | 从Rc<T>创建Weak<T>(弱引用) |
weak.upgrade() | 将Weak<T>转为Option<Rc<T>>(强引用),若数据已释放则返回None |
Weak::strong_count(&weak) | 获取关联Rc<T>的强引用计数 |
Weak::weak_count(&weak) | 获取弱引用计数 |
Cow写时Copy
Clone-on-Write(写时克隆)是一个枚举类型,用于在“可能需要修改借用数据”时,避免不必要的复制。
- 如果只读,就直接借用(零拷贝)。
- 如果要改,就克隆一份(拥有所有权后修改)。
定义:Cow要么借用&T,要么拥有T(T 必须实现 ToOwned)。
enum Cow<'a, B: ?Sized + 'a> where B: ToOwned {Borrowed(&'a B),Owned(<B as ToOwned>::Owned),
}
常用方法:
| 方法 | 说明 |
|---|---|
Cow::Borrowed(&T) | 从借用创建 |
Cow::Owned(T) | 从拥有值创建 |
.to_mut() | 若为借用则克隆,返回可变引用 |
.into_owned() | 获取拥有所有权的值(可能克隆) |
.is_borrowed()/ .is_owned() | 判断当前状态 |
.as_ref() | 获取不可变引用 |
写时复制示例:
use std::borrow::Cow;fn main() {let s = "immutable data".to_string();let mut cow = Cow::Borrowed(s.as_str()); // 借用 &strprintln!("Before: {:?}", cow); // Borrowed("immutable data")// 调用 to_mut() 会检测当前是否为借用let data = cow.to_mut(); // 克隆一份(从 Borrowed -> Owned)data.push_str(" modified");println!("After: {:?}", cow); // Owned("immutable data modified")
}
Pin
Rust 的所有权系统保证了内存安全,但默认允许将值从一个内存位置移动到另一个位置(例如赋值或函数返回时)。 Pin用于防止内存中对象被移动(pinned in place); 即可以“钉住”一个值,使它在被销毁前一直位于同一内存地址 。
| 方法 / 操作 | 说明 |
|---|---|
Pin::new(pointer) | 安全创建Pin<P>,要求P指向的类型T实现Unpin(可安全移动)。 |
Pin::new_unchecked(pointer) | 不安全创建Pin<P>,不要求T: Unpin,但需开发者保证数据不会被移动(否则会导致未定义行为)。 |
pin.as_ref() | 获取Pin<&T>(不可变引用的 Pin)。 |
pin.as_mut() | 获取Pin<&mut T>(可变引用的 Pin)。 |
Pin::into_inner(pin) | 消费Pin<P>,返回内部的指针P(仅当T: Unpin时安全,否则可能导致移动)。 |
pin.get_mut() | 获取内部指针的&mut P(仅当T: Unpin时允许,否则编译错误)。 |
pin.get_ref() | 获取 &T |
unsafe fn get_unchecked_mut() | 获取 &mut T,不检查移动安全 |
Pin与Unpin
Pin<T>的出现就是为了强制数据在内存中 “固定”,确保其地址不会改变
Pin<P>:一个包装器类型,其中P是一个指针类型(如Box<T>、&mut T、Arc<T>等)。Pin<P>保证:被P指向的数据不会被移动(除非数据实现了Unpin)。Unpintrait:标记 trait,表明 “该类型的数据可以安全移动,即使被Pin包装”。大多数类型(如i32、String、Vec<T>等)默认自动实现Unpin,无需手动处理;而需要固定的类型(如自引用类型)则不实现Unpin,必须通过Pin确保不被移动。- 对于
T: !Unpin(不实现Unpin):Pin<P>会严格限制操作,不允许通过Pin获取能导致数据移动的接口(如&mut T),确保数据地址不变。
- 对于
Rust 中大多数类型默认都实现了 Unpin,这意味着它们可以被安全地移动(move)。而PhantomPinned 是标准库 std::marker 模块提供的一个标记类型(marker type;本身是一个零大小的结构体(ZST),没有字段,也不占用内存),其主要作用是阻止包含它的类型自动实现 Unpin trait。
自引用类型与Pin
自引用类型(如包含自身引用的结构体)是Pin的典型应用场景。没有Pin时,移动会导致悬垂引用;用Pin固定后,地址不变,引用安全。
use std::pin::Pin;struct SelfRef {data: String,ptr: *const String,
}impl SelfRef {fn new(txt: &str) -> Pin<Box<SelfRef>> {let mut boxed = Box::pin(SelfRef {data: String::from(txt),ptr: std::ptr::null(),});let ptr = &boxed.data as *const String;unsafe {let mut_ref = Pin::as_mut(&mut boxed);Pin::get_unchecked_mut(mut_ref).ptr = ptr;}boxed}fn show(&self) {unsafe {println!("data = {}, ptr = {}", self.data, &*self.ptr);}}
}fn main() {let pinned = SelfRef::new("hello");pinned.show();
}
