笔记-Rust中的资源管理

228 阅读5分钟

Rust 中的资源管理

Rust 和其他编程语言一样,采用虚拟内存空间上分配内存。

变量和函数

变量分为 全局变量局部变量,全局变量分为 常量静态变量

常量使用 const 关键字定义,常量没有固定的内存地址,会被编译器有效地内联到每个使用到它的地方,随着程序的消亡而消亡。

静态变量使用 static 关键字定义,静态变量的声明周期也是全局的,但它并不会被内联,每个静态变量都有一个固定的内存地址。

静态变量既不在栈中,也不在堆中,而是和程序代码一起被存储于静态存储区中。

字符串字面量也存储于静态内存中。

检测是否声明未初始化变量

在函数中定义的局部变量都会被默认存储到栈中。

Rust 编译器可以检查未初始化的变量,以保证内存安全。

fn main() {
    let x: i32;
    if true {
        x = 1;
    } else {
        // 如果去掉 else 分支,则编译器会报错
        x = 2;
    }
    println!("x = {}", x);
}

检测循环中是否产生未初始化变量

当在循环中使用 break 关键字的时候,break 的返回并不影响在循环中初始化的变量。

fn main() {
    let x: i32;
    let mut y = 0;
    loop {
        y += 1;
        if y > 3 {
            x = 6;
            break;
        }
    }
    println!("x = {}", x);
}

空数组或向量可以初始化变量

当变量绑定 空的 数组或向量时,需要显式指定类型,否则编译器无法推断其类型。

fn main() {
    let a: Vec<i32> = vec![];  // 必须指定类型
    let b: [i32; 0] = [];      // 必须指定类型
    println!("a = {:?}, b = {:?}", a, b);
}

转移所有权产生了未初始化变量

当将一个已经初始化的变量 a 绑定给另一个变量 b 时,Rust 会将变量 a 看作逻辑上的 未初始化变量

fn main() {
    let a1 = 1;
    let b1 = Box::new(1);

    let a2 = a1;
    let b2 = b1;

    println!("a1 = {:?}", a1);
    // println!("b1 = {:?}", b1);  // compile error: value borrowed here after move
}

智能指针与RAII

Rust 中的指针大致可以分为三种: 引用原生指针(裸指针)智能指针

  • 引用:是Rust 提供的普通指针,用 & 和 &mut 来创建,形如 &T 和 &mut T。

  • 原生指针:是指形如 *const T 和 *mut T 这样的类型。

  • 引用和原生指针,可以相互转换:例如 &T as *const T,&mut T as *mut T。

  • 原生指针可以在 unsafe 块下任意使用,不受 Rust 的安全检查规则的限制,而引用则必须收到编译器安全检查规则的限制。

智能指针

智能指针(Samrt Pointer) 是一种结构体,是对指针的封装。

智能指针区别于常规结构体的特征在于,它实现了 Deref 和 Drop。

Deref 提供了解引用能力(重载了解引用符号 *),Drop 提供了自动析构能力,这两个 trait 让智能指针拥有了类似指针的行为。

String 和 Vec 类型 也是一种智能指针,它们的值都是被分配到堆内存并返回指针,通过将返回的指针封装来实现 Deref 和 Drop。

Drop 对于智能指针来说非常重要,可以帮助智能指针在被丢弃时自动执行一些重要的清理工作,比释放堆内存

确定性析构

这种资源管理的方式有一个术语,叫 RAII(Resource Acquisition Is Initialization),意思是资源获取即初始化。

RAII的机制是使用构造函数来初始化资源,使用析构函数来回收资源。

RAII 和 GC 最大的不同在于,RAII将资源托管给创建内存的指针对象本身来管理,并保证资源在其生命周期内使用有效,一旦生命周期终止,资源马上被回收。

fn main() {
    let a = S(1);
    println!("create a = {:?}", a);
    {
        let b = S(2);
        println!("create b = {:?}", b);
        println!("exit inner scope");
    }
    println!("exit main scope");
}

输出如下:
create a = S(1)
create b = S(2)
exit inner scope
drop S --> 2
exit main scope
drop S --> 1

这正是 Drop 的特性,它允许在对象即将消亡之时,自行调用指定代码(drop方法)。

drop-flag

fn create_box() {
    let box3 = Box::new(3);
}

fn main() {
    let box1 = Box::new(1);
    {
        let box2 = Box::new(2);
    }
    for _ in 0..=5 {
        create_box();
    }
}

在上述代码中:

- 变量 box1 和 box3 的析构函数分别在离开 main 函数 和 create_box 函数之后调用的。
- 变量 box2 的析构函数,是在离开由花括号构造的显式内部作用域时调用的。

- 它们的析构函数调用顺序是在编译期(而非运行时)就确定好的。
- Rust 编译期使用了名为 drop-flag 的方式,在函数调用栈中为离开作用域的变量自动插入布尔标记,标注是否调用析构函数。这样,在运行时就可以根据编译期做的标记来调用析构函数。

- 对于结构体或枚举体这类复合类型,并不存在隐式的 drop-flag。只有在函数调用时,这些复合结构实例被初始化之后,编译器才会加上 drop-flag。

- 如果复合结构本身实现了 Drop,则会先调用它自己的析构函数,然后再调用其成员的析构函数;否则,会直接调用其成员的析构函数。

- 当变量被绑定给另外一个变量,值发生移动时,也会被加上 drop-flag,在运行时会调用析构函数。加上 drop-flag 的变量意味着其生命周期的结束,之后再也不能被访问。(所有权机制)

- 对于实现 Copy 的类型,是没有析构函数的。因为实现了 Copy 的类型会复制,其生命周期不受析构函数的影响,所以也没必要存在析构函数。

- 变量遮蔽(shadowing) 并不会导致变量的生命周期提前结束。

内存泄露与内存安全

// todo