Rust 语言特性:智能指针 Box<T>
2026/6/11 7:35:51 网站建设 项目流程

智能指针是所有有指针的编程语言在最近十几年里都陆续建立起来的一种更完备机制,用于规避裸指针可能会导致的各种内存隐患。本文,我们系统地了解一下 Rust 中的智能指针。理解本文需要具备一定的堆栈知识,可参考《编程底层概念回顾:虚拟内存、栈、栈帧、堆》一文,另外,还可阅读一下《C++ 智能指针:示例、原理、适用场景全方位解读》。

回想一下 C/C++,在堆上创建对象是一件再稀松平常不过的事,只要使用new操作符,我们就可轻而易举地得到了一个存放在堆上的对象,然后用它的“地址”(指针)去访问它。相对之下,在 Rust 里,没有new操作符,这本身就传达出了 Rust 的一种态度:Rust 并不鼓励你直接操纵堆内存(使用裸指针)。所以,Rust 中几乎所有创建在堆上的对象都是“包装”过的。而这其中,最常用的有能力在堆上分配空间并存放数据的工具就是Box<T>(官方文档),它也是 Rust 中最有代码表性的一种智能指针。

fnmain(){letb=Box::new(5);println!("b = {}",b);}

上述代码在堆上开辟了一个 4 字节的存储空间,存放了一个 i32 类型的值 5,然后返回了它。这里的 b 的类型是Box<i32>,我们说 b 是Box<i32>的一个实例(也会常说成“值”,这是和引用向对应的称呼)。还记得我们在《深入理解 Rust 所有权》一文开篇时提到过的吗?——“一个变量在离开作用域时是否会自动释放,以及所有权的 move 与变量是在栈上还是堆上没有必然的关系”,这里就是一个示例。因为:这里的 b 是分配在堆上的,它是一个值,它有所有权,离开作用域后会被自动释放,而如果 b 是这样赋值的:let b = 5;,那它就是分配在栈上的,是 Copy 类型,没有 move 语义,它不会在离开作用域时走自动析构的流程,而是伴随着栈帧的弹出,和函数的参数及其其他局部变量一起“集体消失”了(栈顶指针移动)。

像示例那样,在对堆上创建一个基本类型的情况很少见,因为像 i32 这种基本类型它们的大小是确定的,通常都是直接拿来分配给一个变量,这时候它们都是分配在栈上的,因为栈的运作机制决定了只在它上面分配大小确定的值,只有这样,程序才能提前确定一个栈帧的总大小,进而计算出栈顶指针的位移量,如果一个类型的大小是不确定的,那么它就只能分配到堆上去了。我们来看一个非常典型的“递归类型”。

cons list是一个函数式编程语言中的常见类型,可以用来展示“递归类型”这个概念。cons list 最初(可能)来源于 Lisp 编程语言(List 里有一个叫 cons 的函数(construct function),它是专门用来构建这种数据结构的,所以这是 cons 一词的由来),如果你熟悉 Scala 的话,其实 Scala 的 List 就是这种类型。这种类型的特点是:它的每一项都包含两个元素:当前项的值和下一项,其中最后一项的值叫Nil且不再有下一项。我们来看一下怎么在 Rust 里定义一个 cons list:

// 暂时无法编译通过,编译器报错:enumList{Cons(i32,List),Nil,}

注意:Cons 是一个元组,它的第二个元素又是一个 List 枚举,所以它是一个非常确定的“递归类型”!下面是它的一个实例化操作:

fnmain(){letlist=Cons(1,Cons(2,Cons(3,Nil)));}

通过这种层层嵌套的形式可以创建出一个包含了 1,2,3 三个元素的 List。上述代码在定义 List 的地方就会报一个这样的错误recursive typeListhas infinite size,意思是说:List 是一个递归类型,拥有无限大小。这里要特别注意一点:你可能会认为因为这里的 list 是分配在栈上,而递归类型不能确定大小,无法存在于栈上。这是完全错误解读。这里其实还没牵涉到堆和栈的问题,List 类型编译不过的根本问题是:它是一个递归类型,但 Rust 不允许递归类型嵌套自己(也就是 Cons 里面的那个 List),因为这将导致 List 类型可能是无限大小(意味:无法判定大小),这与 list 是在堆上还是栈上无关

你看:我们说一个类型之所以能称之为“递归类型”肯定是因为在它的定义中,它的某一部分(字段)又包含了自己,这样才能形成“递归”,而 Rust 又不允许递归类型嵌套自己,因为要防止类型无限大,那怎么办呢?应对方法就是:在嵌套定义时,引入一个间接层,也就是Box<T>,通过它给递归类型一个已知的大小!看一下例子就知道了。现在我们用 Box 来实现一个可以编译通过的 List:

enumList{Cons(i32,Box<List>),Nil,}usecrate::List::{Cons,Nil};fnmain(){letlist=Cons(1,Box::new(Cons(2,Box::new(Cons(3,Box::new(Nil))))));}

使用 Box 实现的递归 List 就可以编译通过,因为:编译器知道它的“大小”,因为使用了Box<List>去替换ListBox<List>是一个智能指针,它的大小是固定可预知的,大小不确定的 List 本身被放到了栈上,然后Box<List>去指向它。由于 Box 的出现,打断了递归类型的无限递归,把原来无法预知的无限嵌套变成了可预知的线性增长,也就是每增加一层嵌套只是增加一个确定大小的 Box 指针,而实际的 List 放在堆上,可以(理论上)无限增长。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询