rust ABC (一)内存管理

rust ABC (一)内存管理

rust 是一个功能强大的语言,包括了基本现代编程语言的基础数据类型,同时保证了类型安全。同时具备声明式语言和函数式语言的能力。其中为了解决语言安全问题,提出了生命周期和所有权的概念,保证了指针的安全性。

基本数据类型和常用数据类型

基本类型:

这些数据类型保存在栈上,在引用时会和 引用类型 的有所区别。方法引用时,栈内存拷贝到堆内存,然后再从堆内存拷贝到栈内存。会在生命周期上有所不同,后面会讲什么是生命周期。

字符串

Rust有两种字符串类型:str和String。其中str是String的切片类型,也就是说,str类型的字符串值是String类型的字符串值的一部分或全部。

    let s = "&str_type";
    let string1 = String::from(s); 
    let string2 = s.to_string();
数组和元组

rust中,Rust数组长度固定、元素类型相同。元组中不保存任何数据的tuple表示为(),被称作 unit类型

内存管理

这里就要说明基本类型和引用类型的区别了。首先要理解一下几点

如果想让栈类型放在堆(allocate buffer)中,需要使用 Box::new()将它们存放在堆上。

那么为了保证内存安全,就需要讲到所有权的概念。

所有权

在语言中,rust是基于内存安全实现了GC,为了实现这一功能,首先需要理解在rust语言的前身,c系语言中常见的问题。没错,c 系语言中经常的 bug就是 内存泄漏的问题,因为 c系语言是无GC,因此要自己声明 析构函数 来回收已经不再使用的内存,但是由于人不是100% 的机器,人为实现过程始终会存在漏掉部分的内存回收,所以会就会写出有内存泄漏的程序。同时也有 访问越界悬垂指针 的问题。为了解决以上问题,**rust语言牺牲了一部分语言的灵活性,通过强制规定程序处理内存问题,从根因上增强了编程语言处理内存问题的能力,这也就是所有权(ownership)的概念提出的初衷 **。

首先需要理解在那些地方会出现这些问题,也就是内存的堆栈问题

变量范围

一般来说,可以理解成隐式地实现析构函数回收内存,通过所有权判断变量在什么时候回收内存,保证内存的安全。

    let s1 = String::from("分配一个字符的内存");
    let s2 = s1;
    {
        println!("{}",s2);
        //这里s2 就被隐式析构函数回收了,又因为s2就是s1,那么s1也是被回收了
    }
    // 这里就会报错,因为s1,或者说s2也被回收了
    // println!("{}",s1);

那么解决这个问题有两种方式,

函数和闭包

这里进一步理解2个新的概念。

    let text = String::from("text");
    let num = 32;
    do_something(text);
    //print!("{}",text); // 报错
    do_something_num(num);
    print!("{}",num);


fn do_something(some_string:String)  {
    println!("{}",some_string);
}
fn do_something_num(some_string:i32)  {
    println!("{}",some_string);
}

等价于使用了move关键字的闭包

    let text = String::from("text");
    let num = 32;
    let s = move || print!("{}",text);
    //print!("{}",text); // 报错
    let t = move || print!("{}",num);
    print!("{}",num);
引用和租借

那么还有什么办法可以不通过隐式析构函数回收内存,就是使用 & 引用,同样是上面的例子

    let s1 = String::from("分配一个字符的内存");
    let s2 = &s1;
    {
        println!("{}",s2);
        //这里&s1,不会触发析构函数,但是这里引人了一个新的概念, 借用(borrow)
    }
	//这里可以正常运行
     println!("{}",s1);
     

但是这里带来了一个新的问题,我们这里的引用,也就是一般来说的指针,可以理解成一个调用。那么有调用,必定存在调用关系,也就是调用链。最后如何在调用链中回收内存,那么就要做一些限制

    let s1 = String::from("分配一个字符的内存");
    let s2 = &s1;
	let s3 = &s1;
    let s4_s2 = &s2;
	//可以理解成
    //         s1
	//      ↗    ↖
    //     s2     s3
    //   ↗
    //  s4_s2

那么这里就引入了借用规则,同时对借用规则做了一些限制。

作为类比就是租房子,一个房子只能住一个人,房子同时分房东(所有权)和租户(使用权)。非引用(赋值语句)就是房子的所有权, & 引用就是租用房子,只有使用权,没有所有权,并且同一时间只能有一个借用的人有使用权。

    let s1 = String::from("分配一个字符的内存");
    let s2 = &s1;
    let s3 = s1; //这里不正确,第四行s2被借用了,这里获取不了所有权(cannot move out of `s1` because it is borrowed)
    println!("{}", s2);

以上都是不可变引用 & ,现在再增加可变引用 &mut 的规则

变量的引用分为可变引用&mut var和不可变引用&var,站在所有权借用的角度来看,可变引用表示的是可变借用,不可变引用表示的是不可变借用。

简单理解可以类比读写锁,不可变引用是读锁,可变引用是写锁。写写,读写,写读会竞争,读读不会竞争。rust这样设计的目的是为了保证引用的线程安全。(可以理解为什么rust设计语言的时候设计成变量默认是不可变的, 这个是引入了函数式编程的思想,不可变的变量天然适用于线程安全的场景)

读读安全

  let x = String::from("分配一个字符的内存");
  let _x1 = &x;
  let _x2 = &x;

读写不安全

    let mut x = String::from("分配一个字符的内存");
    let x_mut1 = &mut x;    
    let x2 = &x;    
    println!("{}", x_mut1); 
    println!("{}", x_mut2); 

写写不安全

    let mut x = String::from("分配一个字符的内存");
    let x_mut1 = &mut x;    
    let x_mut2 = &mut x;    
    println!("{}", x_mut1); 
    println!("{}", x_mut2); 

智能指针

上面在闭包中使用了move关键字,现在就需要理解什么是 Move 语义,就是解引用操作也需要转移所有权。

    #[derive(Debug,Copy,Clone)]
    struct Car {}

    let car = &Car {};
    let p1 = *car; //成功

    let s = &String::from("分配一个字符的内存");
    let p2 = *s; //这里不正确,解引用操作需要Copy语义(cannot move out of `*s` which is behind a shared reference)

解引用操作也就是指针操作,那么有什么办法能够操纵指针。那么就要引入新的概念

智能指针 Box(解引用)

    let boxed = Box::new(String::from("hello"));
    let p3 = *boxed;

扩展:同时一下是包装类型和常用场景

包装类型 使用场景
Box 堆分配或递归
Rc 线程不安全的引用计数,单线程多处共享只读数据
Arc 线程安全的引用计数,多线程共享只读数据
Arc<Mutex> / Arc<RwLock> 多线程共享可变数据
RefCell 单线程内部可变性
Cow<’a, T> 避免不必要的克隆
Pin<Box> async 自引用结构