Skip to content

什么是指针(pointer)

指针是一个保存内存地址的变量的通用的概念, 学过C的都知道是怎么回事, 就不画图了, 在C的笔记中画过了

什么是智能指针(smtart pointers)

智能指针是Rust中特殊的指针, 用法类似指针, 但是拥有额外的元数据和功能

Rust 标准库中不同的智能指针提供了多于引用的额外功能, 比如自动解引用, 引用计数等

在 Rust 中, 普通引用和智能指针是有区别的

  1. 引用是一类只借用数据的指针
  2. 智能指针拥有它们指向的数据

标准库中最常用的一些智能指针:

  • Box<T>: 用于在堆上分配数据
  • Rc<T>/Arc<T>: 一个引用计数类型, 他的数据可以有多个所有者
  • RefCell<T>/Mutex<T> : 修改 Rust 默认的内部可变性规则

另外可能会涉及 内部可变性(interior mutability)模式, 这是不可变类型暴露出改变其内部值的 API, 我们也会讨论 引用循环(reference cycles)会如何泄漏内存,以及如何避免

指针指针的特点:

  1. 指针指向堆上分配内存空间(如: Box), 普通指针(如:&str) 是引用栈上的数据

  2. 实现 Deref 和 Drop 特性

    1. Deref 自动解引用
    2. Drop 超出生命周期自动清理(类似垃圾回收机制)

指针与内存

Rust 程序有3个存放数据的内存区域: 数据内存 栈内存 堆内存 用C语言来举例类比, 方便理解

数据内存

对于固定大小和静态(生命周期为程序的整个运行期间)的数据, 这个msg的字节是只读的, 因此它位于数据内存区域中, 编译器对这类数据做了许多优化, 由于内存位置是已知且固定的, 所以编译器使用起来非常快

rust
// 静态变量和常量
const SLOGAN: &str = "Rust is best language";
static APP_SLOGAN: &str = "Hello Rust";

fn main() {
  println!("slogan:{}", SLOGAN);
  println!("app_slogan:{}", APP_SLOGAN);
}
c
#include <stdio.h>
static const SLOGAN: char* msg = "C is best language";
int main() {
  printf("%s\n", SLOGAN);
  return 0;
}

栈内存

对于在函数中声明为变量的数据, 在函数调用期间, 内存的位置不会改变, 以为编译器可以优化代码, 所以栈内存中的数据使用起来也比较快

rust
fn main() {
  let num = 10; // 局部变量存在栈中
  println!("num={}", num);
}
c
#include <stdio.h>
int main() {
  int num = 10; // 局部变量存在栈中
  printf("num=%d\n", num);
  return 0;
}

堆内存

对于在程序运行时创建的数据, 此区域中的数据可以添加、移动、删除、调整大小等, 由于它的动态特性, 通常认为它使用起来比较慢, 但是它允许更多创造性的内存使用, 当数据添加到该区域时, 我们称其为分配, 从本区域中删除 数据后,我们将其称为释放

rust
fn main() {
  let ptr = Box::new(5); // 这个数据是存在堆上的
  println!("ptr={}, num={}", ptr, *ptr); // 智能指针会自动解引用
}
c
#include <stdio.h>
#include <stdlib.h>

int main() {
  // 手动分配内存的数据存在堆上
  int *ptr = malloc(sizeof(int));
  *ptr = 10;
  printf("ptr=%p,num=%d \n", ptr, *ptr);
  free(ptr);
  ptr = NULL;
}

Box<T>

  • Box<T> 是最简单的智能指针, 他允许你在堆内存中存储数据
  • 没有额外的性能开销, 但是也没有额外的功能
  • 实现了 DerefDrop 特性(trait)

适用场景:

  • 在编译时, 某个类型的大小无法确定, 但是使用该类型时, 上下文却需要知道他确切的大小
  • 当有大量的数据, 想要移交所有权, 但需要确保在操作时数据不会被复制
  • 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候

使用 Box 在堆上存储数据

rust
fn main () {
    let n = Box::new(5); // 类似C语言的 malloc 函数
    println!("n = {}", n);
}

可以使用 Box 创建递归类型

比如创建一个单向链表 LinkedList

rust
struct Node<T> {
  value: T,
  next: Option<Box<Node<T>>>
}
struct LinkedList<T> {
  head: Option<Box<Node<T>>>
}

Deref 特性

通过实现 Deref 这个特性, 就能够重载* 解引用运算 (de-reference operator), 注意, 这个 * 不是乘法的意思, 和 C 语言的解引用是一样的: 获取指针对应内存地址空间的值

解引用获取指针(引用)的值

rust
fn main () {
    let num = 5;
    let num_ref = &num; // 指针指向栈上的内存空间
                        // 用C表示: int* num_ref = &num;

    assert_eq!(num, 5); // 直接取值对比
    assert_eq!(*num_ref, 5); // 需要解引用, 获取到引用(指针)对应空间的值然后对比
}

Box 也可以直接解引用

rust
fn main() {
    let x = 15;
    let xbox_ref = Box::new(x);
    // Box::new 指针和上面的指针是不一样的, 堆内存上空间需要
    // 手动 malloc 分配, 手动 free 释放
    // 用 C 表示: int * xbox_ref = (int *)malloc(sizeof(int));

    assert_eq!(x, 15);
    assert_eq!(*xbox_ref, 15); // Box 智能指针也可以直接解引用获取值
}

自定义智能指针

手动实现一个 Box, 了解智能指针的原理

初步实现

rust
#[derive(Debug)]
struct MockBox<T>(T);

impl<T> MockBox<T> {
    fn new(value: T)-> Self {
        Self(value)
    }
}

fn main () {
    let y = 25;
    let mbox_ref = MockBox::new(y);
    assert_eq!(y, 25);
    assert_eq!(*mbox_ref, 25);// 编译器错误: type `MockBox<{integer}>` cannot be dereferenced
    // 默认情况下, 如果一个 struct 实例不是一个智能指针, 它就无法被直接解引用
    // 获取到内存中对应的值, 如果需要让这个 struct 变成一个智能指针(也就是可以直接解引用)
    // 那么就需要实现 std::ops::Deref 这个特性(也就是: trait)
}

发现不能直接解引用, 需要实现 Deref 特性

rust
use std::ops::Deref;

#[derive(Debug)]
struct MockBox<T>(T);

impl<T> MockBox<T> {
    fn new(value: T)-> Self {
        Self(value)
    }
}

impl<T> Deref for MockBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

fn main () {
    let y = 25;
    let mbox_ref = MockBox::new(y);
    assert_eq!(y, 25);
    assert_eq!(*mbox_ref, 25);
    // 实现 std::ops::Deref 这个特性以后, 就可以像Box那样, 直接解引用
    // 那么换句话说, 我们就实现了一个自己的智能指针 MockBox, 功能和 Box 一样
}

函数/方法的隐式解引用转换

什么是隐式解引用转换

英文: deref coercions

当将一个与函数定义的参数类型不匹配的指针(引用), 作为 参数传函数去调用, Rust 编译器会尝试: 自动将这个指针转换为一个与函数定义 的参数类型匹配的指针(引用), 这个过程就叫做: 隐式解引用转换(Deref Coercion)

  1. 解引用转换是将一种类型(A)隐式的(也就是不用手动操作的)转换为另外一种类型(B)的引用
  2. 因为 A 类型实现了 Deref trait, 并且其关联类型是 B 类型(意思是: A 解引用之后, 得到的数据类型是 B 类型)

比如: 解引用转换 可以将 &String 转换为 &str, 因为类型 String 实现了 Deref trait 并且其关联类型是 &str 也就是说: impl std::ops::Deref for String 中实现的 deref 方法 返回的值是一个 &str 数据类型, 看文档验证下

隐式解引用转换现象

rust
fn say_hi(name: &str) {
    println!("Hi, {}!", name);
}

fn main () {
    let tom = "tom";
    let jack = String::from("jack");
    let jerry = Box::new(String::from("jerry"));

    say_hi(tom);    // 类型: &str
    say_hi(&jack);  // 类型: &String
    say_hi(&jerry); // 类型: &Box<String>
    // 为什么可以这样使用呢? 按照道理来说, 这应该类型不匹配才对啊
    // 这是因为: 传入的参数都被隐式地解引用转换类型了, 并不是传入
    // 什么类型, 函数就使用什么类型
}

能够进行隐式解引用转换的条件

只有实现了 std::ops::Deref 或 std::ops::DerefMut 这两个特性的类型, 才能进行隐式解引用转换

隐式解引用转换的规则

  • 一直调用 deref 方法(所以要求必须实现 std::ops::Deref 特性)直到类型正确
  • 如果到最后无法再调用 deref 方法, 那么就证明不能隐式转换为函数想要的类型
rust
say_hi(tom); // 类型: &str, 不用转换类型

say_hi(&jack);
// 传入类型: &String, 参数类型: &str
// 1. 调用 String deref => &str

say_hi(&jerry);
// 传入类型: &Box<String>, 参数类型: &str
// 1. 先调用 Box deref => String
// 2. 再调用 String deref => &str

解引用转换如何与可变性交互

上述代码传入的引用都是只读的, 如果有可变的引用, 如何进行 deref 解引用操作呢?

std::ops::Deref 类似的特性还有一个 std::ops::DerefMut 这个特性就是用于可变引用的

Rust编译器在发现类型和 trait 的实现满足以下任意一种情况时会进行解引用强制转换:

  1. T: Deref<Target=U> => 从 &T 转为 &U
  2. T: DerefMut<Target=U> => 从 &mut T 转为 &mut U
  3. T: Deref<Target=U> => 从 &mut T 转为 &U

Drop 特性

这个特性可以实现类似其他编程语言 垃圾回收 的效果, 但是原理不一样, 查看文档

编程语言的内存管理方式:

  1. 垃圾回收: 需要一直运行一个程序, 来检查, 所使用的内存是否还需要使用, 不使用了就回收
  2. Drop: 是 Rust 编译器, 在编译时就分析出代码运行结束的时候, 自动调用 Drop 特性
  3. Rust 语言的 Drop 特性, 是 Rust 编译器在编译时, 自动调用的, 而不是 运行时

Rust 既不同于 C/C++ 手动管理释放内存, 也不同于 Go/Java/Python 自动垃圾回收内存, 而是通过编译器在编译时分析并决定, 变量需要何时释放内存, 并且将释放内存的代码自动注入到源代码里, 然后再编译, 那么通过编译后的代码, 就能实现一种 类似垃圾回收的效果

由于, 手动释放内存可能会导致 重复释放 的问题(在C语言中很明显), 所以在 Rust 中 并不允许你手动调用这个特性的 drop 方法, 而是编译器会自动调用这个特性的 drop 方法

rust
#[derive(Debug)]
struct TestDropTrait;

impl TestDropTrait {
    fn new() -> Self {
        TestDropTrait
    }
}

impl Drop for TestDropTrait {
    fn drop(&mut self) {
        println!("TestDropTrait::drop executed");
    }
}

fn main () {
    let tdt = TestDropTrait::new();
    println!("tdt created, {:?}", tdt);

    // tdt.drop();
    // 不能手动调用 drop, 会报错  explicit destructor calls not allowed
}

// 当 tdt 离开这个作用域后, 会自动的执行 drop 方法
// 也就是说, 程序执行完 main 方法后会输出: `TestDropTrait::drop executed`

Rc 引用计数指针

什么是引用计数智能指针?

引用计数智能指针英文: reference counting smart pointer

引用计数是指: 当一个对象被多个智能指针指向时, 这个对象被引用的次数

为什么有引用计数智能指针?

大多数情况下数据的所有权事非常明确的, 可以准确的知道那个变量拥有哪个数据的所有权, 但是有些情况, 数据的所有权并不明确, 需要多个变量共享数据的所有权, 这时就需要使用引用计数智能指针

比如: 一栋居民楼, 可以被多个业主共享, 但是这栋居民楼只有一个, 所以这栋居民楼的所有权是不明确的, 因为他不是某一个人的, 需要使用引用计数智能指针来管理

rust
struct Building {
    name: String,
}

struct Owner {
    name: String,
    owned_house_id: u32,
    owned_house_in: Building,
}

impl Owner {
    pub fn new(name: &str, owned_house_id: u32, owned_house_in: Building) -> Self {
        Self {
            name: name.to_string(),
            owned_house_id,
            owned_house_in,
        }
    }
}


fn main() {
    let xfxq = Building {
        name: "幸福小区1栋".to_string(),
    };

    // 默认情况下: 这样做是不行的,
    // 创建 zs 的时候, xfxq 的所有权给了 zs 这个对象
    // 之后再创建 ls, 此时 ls 是没有 xfxq 的所有权的
    let zs = Owner::new("zhang sang", 1101, xfxq);
    let ls = Owner::new("li si", 1201, xfxq);

    // 难道 一栋楼有一个人买了一套房子之后, 其他的人就不能买其他房子了?
    // 这显然是不合理的
}

使用引用计数智能指针共享数据

通过不可变引用, 可以在程序的不同部分之间共享只读的数据

rust
use std::rc::Rc;

struct Building {
    name: String,
}

struct Owner {
    name: String,
    owned_house_id: u32,
    owned_house_in: Rc<Building>,
}

impl Owner {
    pub fn new(name: &str, owned_house_id: u32, owned_house_in: Rc<Building>) -> Self {
         Self {
             name: name.to_string(),
             owned_house_id,
             owned_house_in,
         }
    }
}


fn show_house_info(o: Owner) {
    println!("{}{}室的业主是:{}",o.owned_house_in.name, o.owned_house_id, o.name);
}

fn main() {
    let xfxq = Building {
        name: String::from("幸福小区1栋")
    };
    let rc_xfxq = Rc::new(xfxq); // 使用引用计数智能指针

    // 此时可以通过引用计数指针来获取现在有多少个引用
    println!("{}现在有{}个业主", rc_xfxq.name, Rc::strong_count(&rc_xfxq));

    // 此时并没有将 xfxq 的所有权给到 zs 对象
    // 而是传入了 rc_xfxq 克隆结果的所有权给了 zs ls ww 对象
    // xfxq 是真正存储数据的变量而 rc_xfxq 只是一个指针,
    // 因此克隆一个指针速度是非常快的, 而且克隆指针的时候会让计数递增
    let zs = Owner::new("zhang sang", 1101, Rc::clone(&rc_xfxq));
    let ls = Owner::new("li si", 1201, Rc::clone(&rc_xfxq));
    let ww = Owner::new("wang wu", 1501, Rc::clone(&rc_xfxq));

    // 此时会有4个引用: 因为 Rc::new 就算1个, 后续每次 clone 会让数字递增1
    println!("{}现在有{}个业主", rc_xfxq.name, Rc::strong_count(&rc_xfxq));

    show_house_info(zs);
    show_house_info(ls);
    show_house_info(ww);
}

RefCell 与 内部可变性

内部可变性英文: internal mutability

内部可变性允许你用只读引用去修改数据, 这违背了借用规则(内部使用了 unsafe 来绕过借用检查器)

RefCell 可以在运行时进行借用检查规则, 所谓在运行时进行检查就是告诉编译器, 在编译代码的时候你不要管借用检查了, 让我自己在程序运行的时候来判断引用是否可变

rust
struct MessageManager {
    history: Vec<String>,
}

impl MessageManager {
    fn new() -> Self {
        Self {
            history: Vec::new()
        }
    }

    fn send(&self, msg: &str) {
        // 这样是不行的, 因为 history 属性是只读的
        // 但是这个操作逻辑是没有问题的, 向一个空的 Vec 中添加元素
        // error[E0596]: cannot borrow `self.history` as mutable,
        // as it is behind a `&` reference
        self.history.push(msg.to_string());
    }

    fn show_history(&self) {
        let mut i = 0;
        self.history.iter().for_each(|msg| {
            i += 1;
            println!("{}: {}", i, msg);
        });
    }
}


fn main() {
    let mgr = MessageManager::new();
    mgr.send("hello");
    mgr.show_history();
}
rust
use std::cell::RefCell;

struct MessageManager {
    history: RefCell<Vec<String>>,
}

impl MessageManager {
    fn new() -> Self {
        Self {
            history: RefCell::new(Vec::new()),
        }
    }

    fn send(&self, msg: &str) {
        // 在编译时不会检查借用是否可变, 让我自己手动控制
        // borrow_mut 表示对 Vec::new 的可变借用
        self.history.borrow_mut().push(msg.to_string());
    }

    fn show_history(&self) {
        // borrow 表示对 Vec::new 的只读借用
        let mut i = 0;
        self.history.borrow().iter().for_each(|msg| {
            i += 1;
            println!("{}: {}", i, msg);
        });
    }
}


fn main() {
    let mgr = MessageManager::new();
    mgr.send("hello");
    mgr.send("world");
    mgr.show_history();
}

循环引用与内存泄漏

循环引用会导致内存一直无法释放(也就是内存泄漏)

rust
use std::rc::Rc;
use std::cell::RefCell;

#[allow(unused)]
struct Node {
    value: i32,
    next: Option<Rc<RefCell<Node>>>,
}

impl Node {
    fn new(value: i32) -> Self {
        Self { value, next: None }
    }
}

fn main() {
    let a = Rc::new(RefCell::new(Node::new(11)));
    let b = Rc::new(RefCell::new(Node::new(22)));

    a.borrow_mut().next = Some(Rc::clone(&b));
    b.borrow_mut().next = Some(Rc::clone(&a));

/*  此时的 a 和 b 就是循环引用, 这会导致引用计数(Rc)永远不会为0,
    也就是说这两个引用所对应的内存一直不会被释放, 也就是内存泄漏

    a: Node {
        value: 11,
        next : b
    }
    b: Node {
        value: 22,
        next: a
    }
*/
}
rust
use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
#[allow(unused)]
struct Node {
    value: i32,
    next: Option<Weak<RefCell<Node>>>,
}

impl Node {
    fn new(value: i32) -> Self {
        Self { value, next: None }
    }
}

fn main() {
    let a = Rc::new(RefCell::new(Node::new(11)));
    let b = Rc::new(RefCell::new(Node::new(22)));

    // convert Rc to Weak: Weak 不会计数
    let weak_a = Rc::downgrade(&a);
    let weak_b = Rc::downgrade(&b);

    a.borrow_mut().next = Some(weak_b);
    b.borrow_mut().next = Some(weak_a);

    // 如果是 RC, 是无法用 println 输出的,
    // 因为循环引用, 会让导致栈溢出问题(stack over flow)
    println!("a: {:?}", a);
    println!("b: {:?}", b);
}

Released under the MIT License.