首页 / Rust 入门教程 / Box<T> 堆上数据

Rust 入门教程

Box<T> 堆上数据

本教程共 78 篇 · 第 60 篇 · 更新于 2026-08-08 · 约 10 分钟阅读

RustRust 入门教程Box智能指针所有权递归类型特征对象

本节目标:理解 Box 如何把值放到堆上、栈上只留一个指针,结合所有权看堆数据转移时只拷指针不拷数据,并掌握 Box 的四种典型用法(大值上堆、避免栈拷贝、递归类型、特征对象)。

前面讲所有权时我们提过栈和堆。绝大多数 Rust 值默认在栈上,简单高效。但有些场景栈hold不住,这时就需要 Box<T>——Rust 里最简单也最常见的智能指针。

1-1 为什么需要堆

栈和堆的根本区别在内存管理方式。栈由系统自动管理、分配释放极快,但空间有限(main 线程默认 8MB),而且存放的数据编译期就得知道大小。堆空间大得多(只受物理内存限制),但分配慢一点,且数据得有”主人”负责释放。

Rust 里堆上的数据也遵循所有权规则:赋值时发生的是所有权转移,只把栈上的指针(或智能指针)浅拷贝一份,底层数据不动。这和 C/C++ 里手动 malloc/free 天差地别——Rust 靠所有权自动回收堆内存,不会漏也不会重复释放。

Note

Box<T> 本身就是”把值搬上堆”的最轻量封装,除了多一次间接寻址,几乎没有别的性能损耗。它功能单一,所以也最安全、最好懂。

1-2 Box 的基本用法

把值放到堆上,用 Box::new

fn main() {
    let a = Box::new(3);
    println!("a = {}", a);
}

a 是个智能指针,指向堆上的 3。打印能正常出 3,是因为 Box 实现了 Deref(下一章细讲),println! 自动解引用拿到了里面的值。

但下面这行会报错:

// let b = a + 1; // 不能把 Box<{integer}> 当整数直接加

表达式里不会自动解引用,得显式用 *let b = *a + 1;。这个 * 就是解引用操作符。

Warning

把一个 i32 这种小值用 Box 搬上堆纯属多此一举——栈上寄存器、CPU 缓存都能直接吃它,性能更好、可读性也更高。Box 的价值在”大”或”大小不确定”的数据上。

1-3 转移所有权时只拷指针

同样是转移所有权,栈上和堆上表现完全不同。看对比:

fn main() {
    // 在栈上建一个长度 1000 的数组
    let arr = [0; 1000];
    let arr1 = arr;          // 栈上:真的深拷贝了一份数据
    println!("{}", arr.len());   // 正常,arr 还在
    println!("{}", arr1.len());

    // 在堆上建同样大小的数组,用 Box 指向它
    let arr = Box::new([0; 1000]);
    let arr1 = arr;          // 堆上:只拷贝了智能指针,底层数据没动
    println!("{}", arr1.len());
    // println!("{}", arr.len());   // 报错,arr 已失去所有权
}

栈上赋值是把整块数据复制过去,新旧两份各自独立;堆上赋值只复制了指向数据的指针,arr 把”所有权”交给了 arr1,自己失效。这正是大块数据该上堆的原因:转移几乎零成本。

1-4 用法一:把大值存到堆上

当某个值很大(比如大数组、大结构体),而且要在函数间频繁传递时,用 Box 包起来,转移时只挪指针,避免反复深拷贝。这是 Box 最朴素的价值。

1-5 用法二:递归类型(编译期大小未知)

Rust 编译期必须知道每个类型占多少空间。但”递归类型”会无限嵌套,大小算不出来。比如函数式里的 Cons 链表:

enum List {
    Cons(i32, List),
    Nil,
}

Cons 里又包含一个 List,理论上能无限套下去,编译器直接报错:recursive type List has infinite size

解决办法是用 Box 把内嵌的 List 放到堆上,断开无限嵌套:

enum List {
    Cons(i32, Box<List>),
    Nil,
}

Box<List> 的大小是固定的(就是一个指针),于是整个 List 大小可知。运行时每嵌套一层,就在堆上分配一个节点。这是 Rust 实现递归数据结构的标配手法。

Tip

凡是”类型定义里直接包含自身”的情况,多半需要 Box(或其它智能指针)来打断无限大小。这是 Box 在数据结构里最不可替代的用武之地。

1-6 用法三:特征对象

有时你想把不同类型的东西放进同一个集合。枚举能凑,但限制多;更通用的办法是特征对象(trait object)。特征本身是”动态大小类型”(编译期不知道具体多大),用 Box 包一下就变成固定大小的指针:

trait Draw {
    fn draw(&self);
}

struct Button { id: u32 }
impl Draw for Button {
    fn draw(&self) { println!("按钮 {}", self.id); }
}

struct Select { id: u32 }
impl Draw for Select {
    fn draw(&self) { println!("选择框 {}", self.id); }
}

fn main() {
    let elems: Vec<Box<dyn Draw>> =
        vec![Box::new(Button { id: 1 }), Box::new(Select { id: 2 })];
    for e in elems {
        e.draw();
    }
}

Box<dyn Draw> 就是特征对象:dyn Draw 是 DST,Box 把它变成固定大小的智能指针,于是不同具体类型能放进同一个 Vec。这是”多态集合”的常用写法。

1-7 内存布局与 Box::leak

Vec<i32> 本身也是智能指针:栈上一个小结构,指向堆上的数组。Vec<Box<i32>> 则是栈上结构→堆上数组,数组每个元素又是个 Box,各自再指向堆上的值。取元素时要做两次解引用 **first

Box 还提供一个 Box::leak:消费掉 Box,强制让值”泄漏”出不走正常 Drop 流程,从而拿到一个 'static 生命周期的引用:

fn gen_static_str() -> &'static str {
    let s = String::from("hello");
    Box::leak(s.into_boxed_str())
}

用处是:某个在运行期才构造、但希望和程序活得一样久的值(比如全局配置),用 Box::leak 转成 'staticRc/Arc 更省。但这是刻意为之的”泄漏”,普通场景别乱用。

1-8 小结

Box<T> 背后由系统分配器管理堆内存,你不用手动 free。它简单、零额外元数据,主要用在:大值上堆省拷贝、递归类型定大小、特征对象做多态。它是所有智能指针的起点——理解了 Box,再看后面带引用计数、带内部可变性的智能指针就顺了。

Note

Box 指向的数据是不可变的吗?不是。let mut b = Box::new(5); *b = 6; 完全合法——可变性来自 mut 绑定,和堆不堆无关。Box 只是个装值的盒子。

1-9 什么时候不该用 Box

讲完用法,也得说反例,免得你遇事就 Box。以下几种情况,Box 帮倒忙:

一是小值没必要上堆。一个 i32f64 或小的结构体,栈上寄存器直接处理,比堆上快还省心。硬用 Box 只会增加一次间接寻址和一次堆分配的开销,可读性还下降。

二是可以用引用就别用 Box。如果函数只是临时借用一下数据、不打算长期持有,用 &T 就好,所有权都不用动。Box 适合”要长期拥有这份堆数据”的场景,不是”借一下”的场景。

Note

区分”借用”和”拥有”是 Rust 基本功:临时看一眼用引用,要替它管生命周期用智能指针。Box 是后者里最轻的一种。

三是别拿 Box 当逃逸所有权的万能胶。有些报错是设计问题(比如生命周期没理清楚),靠 Box::leak 强转 'static 能编过,但那是掩盖问题。先想清楚数据到底该活多久、谁拥有它,再决定要不要用 Box。

不过话说回来,Box 是你接触智能指针的第一块踏脚石。它简单、零额外元数据、行为可预测,先把”堆上值 + 栈上指针 + 所有权转移只拷指针”这套心智模型建立牢,后面 RcRefCell 那些花活才好理解。

下一章我们看两个让智能指针”像引用一样好用”的 trait:Deref 和 Drop。