# `Box`堆对象分配 关于作者帅不帅,估计争议还挺多的,但是如果说`Box`是不是Rust中最常见的智能指针,那估计没有任何争议。因为`Box`允许你将一个值分配到堆上,然后在栈上保留一个智能指针指向堆上的数据。 之前我们在[所有权章节](https://course.rs/basic/ownership/ownership.html#栈stack与堆heap)简单讲过堆栈的概念,这里再补充一些。 ## Rust中的堆栈 高级语言Python/Java等往往会弱化堆栈的概念,但是要用好C/C++/Rust,就必须对堆栈有深入的了解,原因是两者的内存管理方式不同: 前者有GC垃圾回收机制, 因此无需你去关心内存的细节。 栈内存从高位地址向下增长,且栈内存是连续分配的,一般来说**操作系统对栈内存的大小都有限制**,因此C语言中无法创建任意长度的数组。在Rust中, `main`线程的[栈大小是`8MB`](https://zhuanlan.zhihu.com/p/446039229),普通线程是`2MB`,然后在函数调用时会在其中创建一个临时栈空间,调用结束后Rust会让这个栈空间里的对象自动进入`Drop`流程,最后栈顶指针自动移动到上一个调用栈顶,无需程序员手动干预,因而栈内存申请和释放是非常高效的。 与栈相反,堆上内存则是从低位地址向上增长,**堆内存通常只受物理内存限制**,而且通常是不连续的, 因此从性能的角度看,栈往往比对堆更高。 相比其它语言,Rust堆上对象还有一个特殊之处,它们都拥有一个所有者,因此受所有权规则的限制:当赋值时,发生的是所有权的转移(只需浅拷贝栈上的引用或智能指针即可), 例如以下代码: ```rust fn main() { let b = foo("world"); println!("{}", b); } fn foo(x: &str) -> String { let a = "Hello, ".to_string() + x; a } ``` 在`foo`函数中,`a`是`String`类型,它其实是一个智能指针结构体,该智能指针存储在函数栈中,指向堆上的字符串数据。当被从`foo`函数转移给`main`中的`b`变量时,栈上的智能指针被复制一份赋予给`b`,而底层数据无需发生改变,这样就完成了所有权从`foo`函数内部到`b`的转移. #### 堆栈的性能 很多人可能会觉得栈的性能肯定比堆高,其实未必。 由于我们在后面的性能专题会专门讲解堆栈的性能问题,因此这里就大概给出结论: - 小型数据,在栈上的分配性能和读取性能都要比堆上高 - 中型数据,栈上分配性能高,但是读取性能和堆上并无区别,因为无法利用寄存器或CPU高速缓存,最终还是要经过一次内存寻址 - 大型数据,只建议在堆上分配和使用 总之栈的分配速度肯定比堆上快,但是读取速度往往取决于你的数据能不能放入寄存器或CPU高速缓存。 因此不要仅仅因为堆上性能不如栈这个印象,就总是优先选择栈,导致代码更复杂的实现。 ## Box的使用场景 由于`Box`是简单的封装,除了将值存储在堆上外,并没有其它性能上的损耗。而性能和功能往往是鱼和熊掌,因此`Box`相比其它智能指针,功能较为单一,可以在以下场景中使用它: - 特意的将数据分配在堆上 - 数据较大时,又不想在转移所有权时进行数据拷贝 - 类型的大小在编译期无法确定,但是我们又需要固定大小的类型时 - 特征对象,用于说明对象实现了一个特征,而不是某个特定的类型 以上场景,我们在本章将一一讲解,后面车速较快,请系好安全带。 ## 使用`Box`将数据存储在堆上 如果一个变量拥有一个数值`let a = 3`, 那变量`a`必然是存储在栈上的,那如果我们想要`a`的值存储在堆上就需要使用`Boxt`: ```rust fn main() { let a = Box::new(3); println!("a = {}", a); // a = 3 // 下面一行代码将报错 // let b = a + 1; // cannot add `{integer}` to `Box<{integer}>` } ``` 这样就可以创建一个智能指针指向了存储在堆上的`5`,并且`a`持有了该指针。在本章的引言中,我们提到了智能指针往往都实现了`Deref`和`Drop`特征,因此: - `println!`可以正常打印出`a`的值,是因为它隐式的调用了`Deref`对智能指针`a`进行了解引用 - 最后一行代码`let b = a + 1`报错,是因为在表达式中,我们无法自动隐式的执行`Deref`解引用操作, 你需要使用`*`操作符`let b = *a + 1`,来显式的进行解引用 - `a`持有的智能指针将在作用结束(`main`函数结束)时,被释放掉,这是因为`Box`实现了`Drop`特征 以上的例子在实际代码中其实很少会存在,因为将一个简单的值分配到堆上并没有太大的意义。将其分配在栈上,由于寄存器、CPU缓存的原因,它的性能将更好,而且代码可读性也更好。 ## Box::leak