Rust Box<T>
Box<T> 是堆上分配智能指针。 普通变量存在栈;Box 把数据放到堆内存,栈上只存一个指向堆的指针。
Rust Box 核心作用
把数据挪到堆上,栈只保留指针,减小栈占用
实现递归类型(比如链表),编译器需要知道类型大小,栈上递归类型会无限大,用 Box 包装就可以
转移所有权,把大对象移动,不用拷贝整块数据
Rust Box 基础用法
fn main() {
// 栈上 i32
let a: i32 = 100;
// 数据100存堆,栈存指针b
let b: Box<i32> = Box::new(100);
// Box自动解引用,可以直接当普通值用
println!("{}", b);
}
Box::new(值):在堆分配内存,存入值,返回 Box 智能指针。
所有权移动
fn main() {
let x = Box::new(500);
// 所有权move,x失效
let y = x;
// println!("{}", x); // 编译报错,x已经被移走
println!("{}", y);
}Box 遵循 Rust 所有权规则,移动语义,不会自动拷贝。
解引用 *
fn main() {
let b = Box::new(99);
// *取出堆里面真实的值
let real_val = *b;
println!("{}", real_val);
}*b 访问堆内部的数据;Box 实现了 Deref,大部分场景写代码可以省略*直接使用。
递归结构体(链表)
下面这种直接写会报错,编译器算不出大小:
// ❌ 报错,递归类型,大小无限
struct Node {
val:i32,
next: Node
}用Box包装,next 变成指针,大小固定,编译通过:
// ✅ 正确
struct Node {
val: i32,
next: Option<Box<Node>>
}Box 什么时候释放?
Box 离开作用域时,自动调用 Drop:
销毁栈上的 Box 指针
自动释放堆上面那块内存,不用手动 free。
Rust Box vs 普通变量对比
| 项目 | 普通变量 | Box |
|---|---|---|
| 存放位置 | 栈 | 数据在堆,指针在栈 |
| 复制 | Copy 类型直接拷贝 | 默认 move 移动所有权 |
| 内存释放 | 出作用域栈自动回收 | 出作用域自动释放堆内存 |
什么时候用 Box
✅适合:
很大的数据,不想占栈空间
递归类型(链表、树)
需要把所有权转移,不想拷贝大对象
❌不适合:
- 很小基础类型 (i32,bool),堆分配会有开销,直接栈上变量更好。
小补充
Box 是最基础智能指针,没有引用计数,只有一个所有者。 如果需要多份共享,要用 Rc<T> / Arc<T>。
Box 与 Rc
Box,单一所有权
fn main() {
let a = Box::new(1000);
let b = a; // 所有权转移,a失效
// println!("{}", a); // 编译报错,a已经失去所有权
println!("b = {}", b);
}Box:只能有一个拥有者,赋值直接move,不能共享。
Rc<T> 多所有权(单线程)
Rc = Reference Count,引用计数,实现多份共享所有权,只能用于单线程。
use std::rc::Rc;
fn main() {
let x = Rc::new(2000);
// clone 只是复制指针,不会复制堆上数据,引用计数+1
let y = Rc::clone(&x);
let z = Rc::clone(&x);
println!("计数:{}", Rc::strong_count(&x)); // 3
println!("x={}, y={}, z={}", x, y, z);
}离开作用域每销毁一个变量,计数减1;计数归0,堆内存释放。
Arc<T> 多线程共享
Arc = Atomic Rc,原子版,支持多线程。
use std::sync::Arc;
use std::thread;
fn main() {
let num = Arc::new(888);
let num_clone = Arc::clone(&num);
let handle = thread::spawn(move || {
println!("子线程:{}", num_clone);
});
handle.join().unwrap();
println!("主线程:{}", num);
}Rust Box RC ARC 三者简单总结
Box<T>:独有所有权,堆分配,单所有者,不能共享,开销最小。Rc<T>:单线程共享,引用计数,不能跨线程。Arc<T>:多线程共享,原子计数,可以跨线程,轻微性能开销。
注意:Rc / Arc 只负责所有权共享,不提供可变性;想要修改内部数据,需要搭配
RefCell/Mutex。
Rust Box 的 Deref(解引用)
Box 实现了 Deref trait,所以很多时候不用手动写 *,编译器会自动解引用。
手动解引用 *
fn main() {
let b = Box::new(66);
// * 取出堆里面原始值
let v = *b;
println!("v = {}", v);
}自动隐式解引用(不用写*)
fn main() {
let b = Box::new(77);
// 直接打印Box,不需要 *,Deref自动生效
println!("b = {}", b);
}函数传参自动解引用
fn show(n: i32) {
println!("n: {}", n);
}
fn main() {
let b = Box::new(88);
show(*b); // 手动解引用
// show(b); // 这里不能直接传,i32是栈类型,需要取出里面的值
}Rust Box包裹结构体,访问成员自动解引用
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Box::new(Point { x: 10, y: 20 });
// 不用写 (*p).x, . 运算符会自动deref
println!("x={}, y={}", p.x, p.y);
}重点:
.访问结构体字段的时候,Rust会自动做解引用,这是日常写Box最常用的特性。
Box 小知识点
Box<T>本质就是指针,指向堆上T;栈上存这个指针。- 当Box离开作用域,会同时释放栈上指针 + 堆上T的内存。
- Box只是智能指针,内部数据默认不可变;想要修改,需要用
let mut。
// 可变Box示例
fn main() {
let mut b = Box::new(10);
*b = 100;
println!("{}", b);
}Deref 与 DerefMut
Deref:获取不可变引用,对应&TDerefMut:获取可变引用,对应&mut T
Box 两个 trait 都实现了。
Deref(不可变)
fn main() {
let b = Box::new(100);
// *b 等价于 *(b.deref())
let r: &i32 = &*b;
println!("{}", r);
}DerefMut(可变)
fn main() {
let mut b = Box::new(100);
// *b = 200 等价于 *(b.deref_mut()) = 200
*b = 200;
println!("{}", b);
}重点:点运算符自动解引用
. 会自动调用 deref() / deref_mut(),不用手动写*
struct S {
val: i32,
}
fn main() {
let mut s = Box::new(S { val: 1 });
// 自动deref_mut,不需要 (*s).val
s.val = 999;
println!("{}", s.val);
}函数调用:解引用强制转换(Deref coercion)
把 &Box<T> 自动转为 &T,函数传参很方便。
fn print_num(v: &i32) {
println!("{}", v);
}
fn main() {
let b = Box::new(500);
// &Box<i32> 自动转成 &i32
print_num(&b);
}只会发生在引用上,不会把 Box 本身移动或者拷贝内部数据。
简单区分记忆
| Trait | 作用 | 返回 |
|---|---|---|
| Deref | 只读解引用 | &T |
| DerefMut | 可修改解引用 | &mut T |
注意一个坑
Deref coercion 只对引用生效。
fn take(v: i32) {}
fn main() {
let b = Box::new(10);
// take(b); // ❌不行,这里是move,不是解引用
take(*b); // ✅ 手动取出值
}和 Drop 的关系
Box 离开作用域:
- 先调用
drop,释放堆上的T - 再销毁栈上Box指针
Deref 只是访问里面的数据,不负责内存释放,释放是 Drop trait 的工作。
Box::into_inner
作用:把Box包裹的堆里面的值,取出来转移到栈上,销毁Box,不再占用堆内存。
注意:会发生所有权移动,堆内存被释放,数据搬到栈。
基础使用
fn main() {
let b = Box::new(1234);
// 从堆取出值,放到栈;Box被消耗掉
let val = Box::into_inner(b);
// println!("{}", b); // ❌ b已经被into_inner消耗,失效
println!("栈上的值:{}", val);
}结构体场景
struct User {
id: i32,
}
fn main() {
let box_user = Box::new(User { id: 66 });
let user = Box::into_inner(box_user);
println!("user id = {}", user.id);
}和 * 的区别(重要)
fn main() {
let b = Box::new(100);
// *b:把堆数据拷贝一份到栈(i32是Copy类型),Box还活着
let v1 = *b;
println!("b依然可用:{}", b);
// into_inner(b):消耗掉Box,把所有权移出来,Box直接报废
let v2 = Box::into_inner(b);
println!("v2 = {}", v2);
}*b:Copy类型只是复制,Box继续存在Box::into_inner(b):吃掉Box本身,Box变量直接失效,堆内存释放
如果内部类型不是Copy,
*b会直接move,也会让Box失效,容易混淆。
实际什么时候用 into_inner
- 本来放在堆的数据,后续不需要堆了,拿出来放到栈,减少堆开销
- 某些第三方API返回Box,但是你想要原始数据,不要智能指针包装
反面例子:大对象不建议
如果是很大的结构体,into_inner 把数据搬到栈,栈会爆栈,这种情况就继续保留Box不要取出来。
// 伪代码,很大数组,不要into_inner
let big = Box::new([0u8; 100000]);
// let arr = Box::into_inner(big); // ❌巨大数组塞栈,栈溢出panic小结:
- 小类型:into_inner 没问题
- 大数组、大结构体:不要into_inner,继续用Box持有在堆
Box::leak
Box::leak(box_val):消耗Box,堆内存不会释放,返回 &'static 引用。
内存不会自动回收,程序退出操作系统才回收,属于内存泄漏,是有意为之的泄漏。
基础用法
fn main() {
let s = Box::new("hello world".to_string());
// leak 吃掉Box,堆内存保留,返回static生命周期引用
let static_str: &'static str = Box::leak(s);
println!("{}", static_str);
}执行后:
- Box智能指针被销毁
- 堆上的字符串内存不会执行Drop,不会释放
- 拿到
&'static引用,可以到处传,不受作用域约束
结构体 leak
struct Config {
port: u16,
}
fn main() {
let cfg = Box::new(Config { port: 8080 });
let static_cfg: &'static Config = Box::leak(cfg);
println!("port: {}", static_cfg.port);
}什么时候用 Box::leak
✅合理场景:
- 程序全局配置,启动一次性初始化,程序跑全程,不需要释放
- 写FFI,给C语言返回一个Rust字符串,C负责使用,Rust不能回收
- 某些需要
'static生命周期的API,但是数据运行时才生成
❌不要滥用: 普通业务逻辑不要用,内存永久占住,大量循环leak会内存持续上涨。
leak 之后还能不能回收这块内存?
可以用 Box::from_raw 把裸指针重新转回Box,恢复所有权,离开作用域就释放内存。
fn main() {
let b = Box::new(999);
// leak得到static引用
let s: &'static i32 = Box::leak(b);
// 转回裸指针
let ptr = s as *const i32;
// 不安全:重新包装成Box,拿回所有权
let b2 = unsafe { Box::from_raw(ptr as *mut i32) };
println!("{}", b2);
// b2出作用域,堆内存正常释放
}⚠️
from_raw是unsafe,必须保证指针是leak出来的,不能随便传别的指针。
对比小结
| 方法 | 行为 | 堆内存 |
|---|---|---|
| Box::new() | 堆分配,Box持有 | 出作用域自动释放 |
| Box::into_inner() | 取出内部值,销毁Box | 堆内存释放 |
| Box::leak() | 销毁Box,返回&'static | 不释放,故意泄漏 |
小面试考点
Q:Box::leak 一定是内存泄漏bug吗? A:不一定。Rust中这是受控操作。如果你后续用
Box::from_raw把指针重新包装回Box,内存还可以释放;如果不去处理,就是内存泄漏。
Box<dyn Trait> 特征对象(动态分发)
Box<dyn Trait> 把实现了某个 trait 的不同类型,统一放到 Box 里,存到堆上,实现多态。
Rust 的 trait 对象必须满足两个条件:
- 放在指针后面:
Box<dyn>/&dyn/&mut dyn- trait 对象需要对象安全(不是所有trait都能变成dyn)
普通泛型 <T: Trait> 是静态分发:编译期为每个类型生成一套代码。 Box<dyn Trait> 是动态分发:运行时查表调用函数。
最简例子
trait Speak {
fn say(&self);
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn say(&self) {
println!("汪汪");
}
}
impl Speak for Cat {
fn say(&self) {
println!("喵喵");
}
}
fn main() {
// 堆上存不同类型,统一类型为 Box<dyn Speak>
let animal1: Box<dyn Speak> = Box::new(Dog);
let animal2: Box<dyn Speak> = Box::new(Cat);
animal1.say();
animal2.say();
}放到Vec里面,存多种不同类型
普通泛型Vec,一个Vec只能存同一种具体类型; Vec<Box<dyn Speak>> 可以同时存Dog、Cat,各种实现Speak的类型。
trait Speak {
fn say(&self);
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn say(&self) { println!("汪汪"); }
}
impl Speak for Cat {
fn say(&self) { println!("喵喵"); }
}
fn main() {
let mut list: Vec<Box<dyn Speak>> = Vec::new();
list.push(Box::new(Dog));
list.push(Box::new(Cat));
for item in list {
item.say();
}
}静态分发 vs 动态分发对比
静态分发(泛型 T)
fn talk<T: Speak>(x: T) {
x.say();
}- 编译阶段:为 Dog、Cat 分别生成两份talk机器码
- 运行时:直接调用函数,没有额外开销,速度快
- 缺点:Vec不能同时放Dog和Cat
动态分发(Box<dyn Speak>)
fn talk(x: Box<dyn Speak>) {
x.say();
}- 堆分配,Box内部存两份指针:数据指针 + vtable虚表指针
- 运行时查虚表,找到对应函数调用,有微小运行时开销
- 优点:Vec可以存放各种不一样的类型
Box<dyn Trait>内存布局
Box<dyn Speak> 栈上存两个指针:
- 数据指针:指向堆上真实对象(Dog/Cat)
- vtable虚表指针:指向一张表,里面存各个trait方法的函数地址
所以
Box<dyn Trait>大小固定,不管内部是什么类型,都是两个usize。
注意坑:对象安全
不是所有 trait 都能写成 dyn Trait ❌下面这种不能做 trait 对象:
trait BadTrait {
fn new() -> Self; // 返回Self,不能dyn
}规则:
- 方法不能返回
Self - 不能有泛型参数
什么时候选 Box<dyn Trait>
✅适合:
- 集合(Vec)里面放多种不同实现的类型
- 插件模式、事件回调、多种处理器
❌不适合:
- 类型在编译期全部已知,优先用泛型静态分发,性能更好。
补充:&dyn Trait,不用堆分配
不一定非要Box,引用也可以动态分发,不需要堆:
trait Speak { fn say(&self); }
struct Dog;
impl Speak for Dog {fn say(&self){println!("dog");}}
fn main() {
let d = Dog;
let r: &dyn Speak = &d;
r.say();
}区别:
Box<dyn T>:所有权在手上,堆分配&dyn T:借用,不拥有所有权,不需要堆