15|数据结构:这些浓眉大眼的结构竟然都是智能指针?

230 阅读4分钟

正式开始

指针和引用

  1. 指针是一个持有内存地址的值,可以通过解引用来访问它指向的内存地址,理论上可以解引用到任意数据类型;
  2. 引用是一个特殊的指针,它的解引用访问是受限的,只能解引用到它引用数据的类型,不能用作它用

智能指针

  1. 智能指针是一个表现行为很像指针的数据结构
  2. 除了指向数据的指针外,它还有元数据以提供额外的处理能力
  3. 智能指针一定是一个胖指针,但胖指针不一定是一个智能指针

定义

在 Rust 中,凡是需要做资源回收且实现了 Deref/DerefMut/Drop的数据结构都是智能指针

智能指针 String 和 胖指针&str 的区别

image.png

  1. String 多一个 capacity 字段
  2. String 对堆上的值有所有权,而 &str 是没有所有权的

智能指针与结构体的区别

  1. String 是用结构体定义的
pub struct String {
    vec: Vec<u8>,
}
  1. String 实现了 Deref 和 DerefMut,这使得它在解引用的时候,会得到 &str。普通的结构体并不一定实现
  2. String 需要为其分配的资源做相应的回收。它内部使用了 Vec,所以可以依赖 Vec 的能力来释放堆内存

在堆上创建内存的 Box

  1. Rust 中最基本的在堆上分配内存的方式,绝大多数其它包含堆内存分配的数据类型,内部都是通过 Box 完成的
  2. Box<T> 的定义里,内部是一个 Unique<T>,它是一个私有的数据结构,我们不能直接使用,它包裹了一个 *const T 指针,并唯一拥有这个指针
  3. 堆上分配内存的 Box<T> 其实有一个缺省的泛型参数 A,就需要满足 Allocator trait,并且默认是 Global
  4. 可以使用 #[global_allocator] 标记宏,定义你自己的全局分配器
  5. 可以实现 GlobalAlloc trait 来撰写自己的全局分配器,它和 Allocator trait 的区别,主要在于是否允许分配长度为零的内存

Cow<'a, B>

  1. Cow 是 Rust 下用于提供 写时克隆(Clone-on-Write) 的一个智能指针,它跟 虚拟内存管理的写时复制(Copy-on-write) 有异曲同工之妙
  2. 包裹一个只读借用,但如果调用者需要所有权或者需要修改内容,那么它会 clone 借用的数据

pub enum Cow<'a, B> where B: 'a + ToOwned + ?Sized {
  Borrowed(&'a B),
  Owned(<B as ToOwned>::Owned),
}

它是一个 enum,可以包含一个对类型 B 的只读引用,或者包含对类型 B 的拥有所有权的数据


pub trait ToOwned {
    type Owned: Borrow<Self>;
    #[must_use = "cloning is often expensive and is not expected to have side effects"]
    fn to_owned(&self) -> Self::Owned;

    fn clone_into(&self, target: &mut Self::Owned) { ... }
}

pub trait Borrow<Borrowed> where Borrowed: ?Sized {
    fn borrow(&self) -> &Borrowed;
}
  1. type Owned: Borrow<Self> 是一个带有关联类型的 trait

image.png

MutexGuard<T>

  1. 通过 Deref 提供良好的用户体验

  2. 通过 Drop trait 来确保,使用到的内存以外的资源在退出时进行释放

  3. MutexGuard这个结构是在调用 Mutex::lock 时生成的

    pub fn lock(&self) -> LockResult<MutexGuard<'_, T>> {
        unsafe {
            // 取得锁资源,如果拿不到,会在这里等待
            self.inner.raw_lock();
            // 如果拿到了,会把 Mutex 结构的引用传递给 MutexGuard
            MutexGuard::new(self)
        }
    }
    

新鲜知识点

  1. 自己写内存分配时不能使用 println!()

    a. 因为 stdout 会打印到一个由 Mutex 互斥锁保护的共享全局 buffer 中,这个过程中会涉及内存的分配,分配的内存又会触发 println!(),最终造成程序崩溃。

    b. 而 eprintln! 直接打印到 stderr,不会 buffer

  2. 在使用 Box 分配堆内存的时候要注意

    a. Box::new() 是一个函数,传入它的数据会出现在栈上,再移动到堆上

    b. 如果我们的 Matrix 结构不是 505 个字节,是一个非常大的结构,就有可能出问题

    fn main() {
        // 在堆上分配 16M 内存,但它会先在栈上出现,再移动到堆上
        let boxed = Box::new([0u8; 1 << 24]);
        println!("len: {}", boxed.len());
    }
    
  3. 一个类型还可以被借用成不同的引用

    use std::borrow::Borrow;
    fn main() {
        let s = "hello world!".to_owned();
        // 这里必须声明类型,因为 String 有多个 Borrow<T> 实现
        // 借用为 &String
        let r1: &String = s.borrow();
        // 借用为 &str
        let r2: &str = s.borrow();
        println!("r1: {:p}, r2: {:p}", r1, r2);
    }
    
  4. 三种分发手段

    a. 使用泛型参数做静态分发

    b. 使用 trait object 做动态分发

    c. 根据 enum 的不同状态来进行统一分发

好用链接

  1. String Deref/DerefMut标准库实现
  2. Vec 的drop实现
  3. C++中智能指针unique_ptr
  4. Box<T> 中的Unique<T>
  5. buddy system 内存分配
  6. Allocator trait
  7. jemalloc
  8. 全局分配器 trait
  9. std::borrow
  10. str 对 ToOnwed trait的实现
  11. String 实现 Borrow
  12. Cow实现Deref trait
  13. 序列化库 serde
  14. Mutex::lock
  15. Mutex Guard定义
  16. smart string
  17. 内存分配器 glibc pthread malloc
  18. 内存分配器 Google开发的tcmalloc
  19. 内存分配器 FreeBSD上默认使用的jemalloc