rust ABC (一)内存管理
rust ABC (一)内存管理
rust 是一个功能强大的语言,包括了基本现代编程语言的基础数据类型,同时保证了类型安全。同时具备声明式语言和函数式语言的能力。其中为了解决语言安全问题,提出了生命周期和所有权的概念,保证了指针的安全性。
基本数据类型和常用数据类型
基本类型:
- 裸指针(一个机器字长)、普通引用(一个机器字长)、胖指针(除了指针外还包含其他元数据信息,智能指针也是一种带有额外功能的胖指针,而胖指针实际上又是Struct结构)
- 布尔值
- char
- 各种整数、浮点数
- 数组(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);
那么解决这个问题有两种方式,
-
使用 clone(需要实现 Clone trait,这样能够使用clone方法) ,s1 和 s2 是两个值
let s1 = String::from("分配一个字符的内存"); let s2 = s1.clone(); { println!("{}",s2); //这里s2 就被隐式析构函数回收了 } println!("{}",s1); //这里s1 就被隐式析构函数回收了 -
让 s1 具备 COPY 语义 (需要实现 Copy trait)
//通过宏实现 Copy语义,同时Copy语义必须要Clone语义 #[derive(Debug,Copy,Clone)] struct Car {} let s1 = Car {}; let s2 = s1; { println!("{:?}",s2); //这里s2 就被隐式析构函数回收了 } println!("{:?}",s1); //这里s1 就被隐式析构函数回收了
函数和闭包
这里进一步理解2个新的概念。
- 函数和闭包同样可以理解是一个作用域,在作用域之后,内存需要回收,因此也会隐式存在析构函数。这样设计是为了在方法中释放内存后,防止指向该数据的指针变成悬垂指针,这样Rust就永远不会产生悬垂指针。
- 基本类型可以理解成实现了 Copy trait的类型,被借用的时候不会报错,因为是复制的值类型。而比如字符串、vec 等因为放在堆中,没有Copy语义。
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 |
多线程共享可变数据 |
| RefCell |
单线程内部可变性 |
| Cow<’a, T> | 避免不必要的克隆 |
| Pin<Box |
async 自引用结构 |