Day 09 - 指针 智能指针

本贴最后更新于 454 天前,其中的信息可能已经斗转星移

image

指针的概念和 C 语言一致 也就是指向内存中包含地址的变量 在 Rust 中最常见的就是引用

img

📚 智能指针

创建时间:2023-04-05 14:22 星期三


他们的行为和指针类似但是有额外的元数据与功能。

与普通指针的对比

📅 引用

  • 只能借用数据

⛅️ 智能指针

  • 可以直接拥有他所指向的数据

例子: String Vec 他们的特点是都拥有一片内存区域且允许用户对其操作还拥有元数据

  • 元数据 - 容量
  • 功能 - String 提供 UTF-8 编码的保障

实现

智能指针通常由 struct 实现 并且实现了, Deref​ 和 Drop​ 两个 trait

  • 【Deref】允许智能指针 Struct 的实例像引用一样使用
  • 【Drop】允许自定义当智能指针走出作用域的代码

常见的智能指针

Box 在堆内存上分配值

Rc 启用多重所有权的引用计数类型

Ref 和 RefMut 通过 RefCell<T>访问在运行时而不是编译时强制借用规则的类型

这里我们介绍几个概念:

  • 内部可变模式

    • 不可变类型暴露出可以修改内部值的 API
    • 引用循环 防止内存泄漏

Box 以及堆内存

Box 作为一个指针他可以让你在 heap 上存储数据,他也会同时在 stack 中存储一个指针 指针指向了存储的数据

除此之外没有其他的功能和开销

使用场景

  • 在编译的时候某类型的大小无法确定但使用类型的时候 上下文却需要只知道他的确切的大小
  • 当你有大量的数据想要移交所有权 但需要确保在操作的时候数据不会被复制
  • 使用某个值的时候你只关心他是否实现了某个 trait 而不是具体的类型
fn main(){
    let b = Box::new(4);
    print!("{}", b);
}

当 b 走出他的作用域的时候 他的栈内存堆内存都会被 drop

当我们想要实现链表的时候:

struct link_array{
    value: i32,
    next: link_array
}

程序会报错

因为这个结构的大小暂时不知道他的大小,因为他的结构可能像下面的结构一样 导致无法计算出指定的大小。

image

但是我们知道指针的大小不会因为他指向的数据改变而变化(指针的大小是确定的)

和引用的区别:智能指针可以在申请变量创建在堆内存空间中。并且实现了 Dereftrait​ 和 DropTrait​.

Deref Trait

它可以使我们的变量可以自定义解引用运算符: *​ 。通过 Deref,智能指针可向常规引用一样处理。

解引用: 就是将引用的值拿到获得到具体的值

fn main(){
    let a = 1;
    let b = &a; // 普通引用
    assert_eq!(1, *b);
  
    let b = Box::new(a);
    assert_eq!(1, *b);
  
     print!("{}", b);
}

程序执行成功.

但是我们使用结构的时候默认是不能使用结构的

struct my_box<T>(T);

impl <T> my_box<T> {
    fn new(x: T) -> my_box<T>{
        my_box(x)
    }
}

fn main(){
    let a = my_box::new(3);
    let b = *a; 
}

因为啊 我们没有个这个 my_box​ 实现我们的解引用的功能

use std::ops::Deref;

struct my_box<T>(T);

impl <T> my_box<T> {
    fn new(x: T) -> my_box<T>{
        my_box(x)
    }
}

impl <T> Deref for  my_box<T> {
    type Target = T; // 关联变量 将泛型交给这个关联变量

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

fn main(){
    let a = my_box::new(3);
    let b = *a; 
    // 类似于: *(a.deref())
}

隐式解引用

Deref Coercion​ 是一种为函数、方法提供的一种便携特性。

假设类型 T 实现了 Deref Trait​ 自动解引用可以把 T 的引用转化为 T 经过 Deref​ 方法操作后生成的引用。

假设 T 的 deref 返回的是一个: &String​ 则:

`&(T) == &(T.deref) == &(&String) == &str`​

String 默认实现了一个 Deref 返回自己的字符串切片 -> String[..](&str)

代码如下:

use std::ops::Deref;

fn get_value(username: &str){
    println!("我叫做{username}");
}
struct my_box<T>(T);

impl <T> my_box<T> {
    fn new(x: T) -> my_box<T>{
        my_box(x)
    }
}

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

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

fn main(){
    let name = my_box::new(String::from("你好"));
    // name : &my_box<String>

    get_value(&name);
}

解引用与可变性

可以使用 DerefMut trait 重载可变引用的*运算符

image

Drop Trait

DT 可以让我们自定义当值离开自己作用域时候发生的动作,他的虚函数(抽象方法)是:Drop 方法. 参数是 self 的可变引用。

// 其余代码同上
impl <T> Drop for  my_box<T> {
    fn drop(&mut self) {
        println!("这个变量被销毁了");
    }
}

fn main(){
    let name = my_box::new(String::from("你好"));
    // name : &my_box<String>

    get_value(&name);
}

我们不能显式调用 DropTrait 对象的 Drop 方法但是可以使用 drop 进行调用。

RC

Rust 的多重所有权(Multiple Ownership)是指一个值可以有多个所有者,并且这些所有者之间是平等的,即没有“主人”。Rust 使用引用计数(Reference Counting,简称 RC)来实现多重所有权,它可以让一个值有多个所有者,直到所有的所有者都不再需要该值时才会被销毁。

具体来说,==当一个值被创建时,它会被分配在堆上,并返回一个指向该值的智能指针==,==这个指针记录了该值的引用计数==。==当有一个新的所有者出现时,引用计数会加 1;当某个所有者不再需要该值时,引用计数会减 1。当引用计数为 0 时,该值就会被销毁。==

所有者就是这个值的引用。

使用引用计数可以避免一些内存管理上的问题,比如使用裸指针时容易出现的野指针和内存泄漏等问题。同时,Rust 的引用计数也是类型安全的,它可以避免跨线程访问时的数据竞争问题。

需要注意的是,使用引用计数可能会带来一些性能上的损失,因为每次增加或减少引用计数都需要进行原子操作。另外,引用计数也不能解决所有的内存管理问题,比如循环引用问题,需要通过其他方式来解决。

RC 只能用于单线程的场景。

enum List{
    Cons(i32, Box<List>),
    Nil
}

use crate::List::{Cons,Nil}; // 简化枚举的使用

fn main(){
    let a = Cons(5, Box::new(
        Cons(10, Box::new(
            Cons(20, Box::new(
                Nil
            ))
        ))
    ));

    let b: List= Cons(10, Box::new(a)); // 在首节点上加入一个10
    let e: List =Cons(100, Box::new(a)); // 报错 因为a移动了
}

我们需要这么写:

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use std::rc::Rc;

use crate::List::{Cons, Nil}; // 简化枚举的使用

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Cons(20, Rc::new(Nil)))))));

    let b: List = Cons(10, Rc::clone(&a)); // 在首节点上加入一个10
    // 在不取得所有权的时候拿到a的数据 并在前面添加一个10首节点

    let e: List = Cons(100, Rc::clone(&a));
}

strong_count 是获取 RC 强引用的值

image

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use std::rc::Rc;

use crate::List::{Cons, Nil}; // 简化枚举的使用

fn main() {
    let mut a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Cons(20, Rc::new(Nil)))))));

    let mut b: List = Cons(10, Rc::clone(&a)); // 在首节点上加入一个10
    *b = 2;
}
// 报错

RefCell 和 内部可变性

RefCell 是 Rust 标准库中的一种数据类型,它允许在拥有不可变和可变借用的情况下,以安全的方式共享数据。

RefCell 实现了内部可变性模式,它允许你通过一个不可变引用来修改值,而不用拥有一个可变引用。这使得代码更加灵活,但同时也增加了运行时的开销和潜在的错误。

下面是一个简单的例子,演示了如何使用 RefCell 来共享一个可变值:

不可变引用 代码执行错误

use std::cell::RefCell;

fn main() {
    let x = RefCell::new(42); // 创建一个包含值 42 的 RefCell

    
    let mut z = x.borrow(); // 获取不可变引用
    println!("The value of x is: {}", *z);
    *z += 3;

    println!("The value of x is now: {}", *z);
}

可变引用 代码执行正确

use std::cell::RefCell;

fn main() {
    let x = RefCell::new(42); // 创建一个包含值 42 的 RefCell
    let mut z = x.borrow_mut(); // 获取可变引用
    *z += 1; // 修改值 
    println!("The value of x is now: {}", *z);
}

在这个例子中,我们首先创建了一个 RefCell,它包含了一个整数值 42。然后我们调用了 borrow()​ 方法来获取一个不可变引用,打印了这个值。接着我们再次调用了 borrow_mut()​ 方法来获取一个可变引用,通过这个引用我们将值加一并打印。

需要注意的是,当我们调用 borrow_mut()​ 方法时,如果已经存在一个不可变引用,则会导致运行时的 panic。这是因为 RefCell 会在运行时检查引用的有效性,以确保没有违反 Rust 的借用规则。

总之,RefCell 提供了一种安全的方式来在可变和不可变借用之间共享数据,并且可以避免在运行时出现数据竞争和内存安全问题。

附录:循环引用导致内存泄漏

当 Rc 的计数不可能成为 0 的时候导致内存泄漏

  • Rust

    Rust 是一门赋予每个人构建可靠且高效软件能力的语言。Rust 由 Mozilla 开发,最早发布于 2014 年 9 月。

    58 引用 • 22 回帖 • 3 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...