学习Rust基础最重要的就是理解它的内存管理,前面我们学习了Rust赋值相关的Copy语义, Move语义, Borrow语义(引用和借用), 学习了Rust生命周期等知识。 这些概念都要围绕Rust如何管理内存来理解。本节开始将学习Rust中的智能指针

什么是智能指针

先看一下指针的概念,在支持指针的编程语言中,指针是一个包含内存地址的变量,从而可以通过这个地址引用(指向)一些其他的数据。在Rust中最常见的指针是在使用Borrow语义时使用语法(&&mut)创建的引用. 引用只是借用了数据,并没有获取到数据的所有权。可见引用这类普通的指针还不够智能。

那么什么是智能指针呢?在Rust中智能指针要比普通的引用的功能多一些,而且在大多数情况下智能指针具有它所指向数据的所有权

为了引出智能指针,我们还是回到程序对栈内存、堆内存管理的问题上。

  • 栈的分配上是连续的,后进先出,入栈增加数据,出栈移出数据。
  • 堆是动态分配的内存空间,程序在运行时动态分配和释放,因此堆内存的分配是不连续的。
  • 访问栈上的数据要比访问堆上的数据速度快,但在栈上保存的数据必须在编译时就能确定其占用内存的大小,如果在编译时不能确定大小或者在运行时会动态改变的数据只能保存在堆上。
  • 栈是由系统自动管理的,不需要我们担心。堆由于要保存动态数据,其空间可能会很大的,由于其内存分配的不连续性,随着时间推移将变得碎片化,如果管理不好不仅会使程序变得很慢,还可能将内存耗尽。

为了管理堆内存,不同编程语言采用了不同的方式,C/C++让开发人员手动管理,Java, Go提供了垃圾回收机制。而Rust则希望通过所有权模型来自动管理堆内存

先复习一下Rust的所有权规则:

  1. Rust中的每一个值都有一个所有者变量
  2. 一个值在同一时刻只能有一个所有者
  3. 当所有者离开作用域时,该值将被丢弃并释放内存

这3条规则对于分配在栈上保存的数据,很好理解,因为栈是自动管理,变量离开作用域,占用内也就自动释放了。Rust的所有权机制怎么能够自动管理堆内存呢? 在回答这个问题之前,先要明白一点: 对于编译时能够确认占用内存大小的变量,Rust默认是将其分配到栈上的

在Rust中即使变量是复杂的类型(例如复杂的结构体类型),只要编译时能够确认其占用内存大小,就默认将其分配到栈上。分配到栈上的优点就是效率高和自动回收。

例1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::mem::size_of;

#[derive(Debug)]
struct Foo {
    a: i32,
    b: i32,
}

fn main() {
    println!("{}",size_of::<Foo>()); // 8
    let f1 = Foo{a: 5, b: 6};
    println!("{:?}", f1);
}

例1代码的内存分配示意图如下,因为结构体Foo编译时就能确定其占用内存的大小为8字节,所以会被默认分配在栈上。

rust-smart-pointer-1.png

如果在编译时不能确认占用内存大小的变量,数据将会被分配到堆上保存。数据被分配到堆上后,Rust的所有权机制是如何确保堆上数据的内存自动管理的呢? 答案就是,对于不能确认占用内存大小的变量,Rust将其数据分配到堆上上,同时在栈上创建指向堆上数据的智能指针。即变量是栈上的智能指针,指向堆上的数据,智能指针具有堆上数据的所有权,当其离开作用域时,堆上的数据就会自动释放。

也就是说Rust通过在栈上放一个智能指针,指向堆上的数据,通过所有权机制保证了堆上数据的内存受栈上内存(智能指针)生命周期的控制。 所以就很容易理解,智能指针就是一种数据结构,一般是使用结构体实现的,智能指针中除了有指向堆内存数据的地址外,还可以有其他元数据字段,因为智能指针被保存在栈上,所以这些元数据占用内存大小必须也是固定的。

前面在学习所有权时举例中的StringVec<T>其实就是智能指针。

例2:

1
2
3
4
fn main() {
    let word = String::from("hello");
    println!("{}", word);
}

rust-smart-pointer-2.png

栈上的值的生命周期是自动管理的,栈上值的生命周期和栈帧一样,Rust通过所有权机制为堆上的值也引入了生命周期,智能指针在栈上指向堆上的值,这样堆内存的生命周期就默认和对应栈内存的生命周期一样了。

标准库中的常用智能指针

前面学习了智能指针只是一种数据结构,它们的表现类似指针,同时有额外的元数据和功能。大多数智能指针拥有其所指向数据的所有权。

智能指针通常使用结构体实现,其区别于常规结构体的显著特征是DerefDrop trait。 Deref trait允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。Drop trait 允许我们自定义当智能指针离开作用域时运行的代码。例如Vec<T>实现了Drop当其离开作用域时会自动释放其指向的堆内存上的数据。

智能指针是一个在Rust经常被使用的通用设计模式,很多库都有自己的智能指针,我们也可以编写属于我们自己的智能指针。标准库中有一些常用的智能指针:

  • Box<T>,用于在堆上分配值
  • Rc<T>,一个引用计数类型,其数据可以有多个所有者
  • RefCell<T>,一个在运行时而不是在编译时执行借用规则的类型

参考