经过这一段时间的学习,基础知识里,我们还剩数据结构没有学习,而数据结构里最难的就是智能指针。
我们先回顾一下指针:它的值是一个内存地址,要想访问它指向的这个内存地址,需要解引用。理论上可以解引用到任意数据类型;引用是一个特殊的指针,它只能解引用到它引用的数据类型。
智能指针
智能指针除了指向数据的指针外,还有源数据以提供额外的处理能力。
智能指针与胖指针的区别:智能指针一定是一个胖指针,但胖指针不一定是一个智能指针。
我们来看一个例子:
String(智能指针) 对堆上的值有所有权,而 &str(胖指针) 是没有所有权的,这是 Rust 中智能指针和普通胖指针的区别。
代码语言:javascript复制pub struct String {
vec: Vec<u8>,
}
从String的定义来看明明就是个结构体,怎么就高攀上了智能指针了呢?这是因为String 实现了 我们昨天刚学的 Deref 和 DerefMut 这2个trait,这使得它在解引用的时候,会得到 &str。
代码语言:javascript复制impl ops::Deref for String {
type Target = str;
fn deref(&self) -> &str {
unsafe { str::from_utf8_unchecked(&self.vec) }
}
}
impl ops::DerefMut for String {
fn deref_mut(&mut self) -> &mut str {
unsafe { str::from_utf8_unchecked_mut(&mut *self.vec) }
}
}
由于在堆上分配了数据,String 还需要为其分配的资源做相应的回收。而 String 内部使用了 Vec,所以它可以依赖 Vec的能力来释放堆内存。
现在我们发现,在Rust中,但凡是要回收资源,并实现了Deref/DerefMut/Drop这3个trait的数据结构,都是智能指针。
这么说的话,我们发现以下这些都是智能指针
- 用于在堆上分配内存的Box<T> 和 Vec <T>、
- 用于引用计数的 Rc<T> 和 Arc <T>
- 还有很多其他的数据结构 PathBuf、Cow <'a B>、MutexGuard <T> 等。
Box<T>
它是 Rust 中最基本的在堆上分配内存的方式,绝大多数其它包含堆内存分配的数据类型,内部都是通过 Box完成的,比如 Vec<T>。
我们先来复习一下C是怎么分配堆内存的。C提供了malloc/calloc/realloc/free 来处理内存分配,但是最后为了安全释放内存,给我们码农造成比较大的心智负担。
C 发现这个问题,于是搞了一个智能指针unique,可以在指针退出作用域的时候释放堆内存,这样保证了堆内存的单一所有权。这就是Box<T>的前身。
我们看到在Box<T>的定义里就有Unique表示借鉴的C
堆上分配内存的 Box其实有一个缺省的泛型参数 A,就需要满足 Allocator trait,这其实是指定一种内存分配器,并且默认是 Global,当然也可以替换成自己的内存分配器。
Allocator trait 提供很多方法:
- allocate 是主要方法,用于分配内存,对应 C 的 malloc/calloc;
- deallocate,用于释放内存,对应 C 的 free;
- 还有 grow / shrink,用来扩大或缩小堆上已分配的内存,对应 C 的 realloc。
再来看下Box的Drop实现:
代码语言:javascript复制#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<#[may_dangle] T: ?Sized, A: Allocator> Drop for Box<T, A> {
fn drop(&mut self) {
// FIXME: Do nothing, drop is currently performed by compiler.
}
}
可以看到这是个空的实现。像我们平时开发一样,先确定接口,然后各自去自己的服务里实现自己的逻辑。这是不是就是我们一直说的面向接口编程的思想?