所有权是Rust最独特的特性,他让Rust无需GC就可以保证内存安全
1.什么是所有权
- rust的核心特性就是所有权
- 所有程序在运行时都必须管理他们使用计算机内存的方式
- 有些语言有垃圾收集机制,在程序运行时,他们会不断的寻找不再使用的内存
- 程序员必须要显示的分配和释放内存
- rust采用了第三种方式
- 内存是通过一个所有权系统来管理,其中包含一组编译器在编译时检查的规则。
- 当程序运行时,所有权特性不会减慢程序的运行速度。
2.Stack vs Heap
栈内存和堆内存
-
在代码运行时,stack和heap都是可用的内存,但是他们的结构不相同。
-
stack 按值的接收顺序来存储,按相反的顺序将他们移除(后进先出,LIFO)
- 添加数据是压入栈
- 移除数据是弹出栈
-
所有存储在stack上的数据必须拥有已知的固定的大小
- 编译时大小未知的数据或运行时大小可能发生变化的数据必须存放在heap上
-
heap 内存的组织性差一点
- 当数据放入heap,你会请求一定数量的空间。
- 操作系统在heap中找到一块足够大的空间,把他标记为在用,并返回一个指针,也就是这个空间的地址。
- 这个过程就是在heap上进行分配,又是仅仅称为"分配",把值压到stack上不叫分配。
-
因为指针是已知固定大小的,可以把指针存放在stack上
- 但如果想要实际数据,必须使用指针来定位。
-
把数据压到stack上要比在heap上分配快的多
- 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在stack的顶端
-
在heap上分配空间需要做更多的工作
- 操作系统首先需要找到一个足够大的空间来存放数据,然后要做好记录方便下次分配
2.Stack vs Heap 访问数据
-
访问heap中的数据要比访问stack中的数据慢,因为需要通过指针才能找到heap中的数据
- 对于现代的处理器,由于缓存的缘故,如果指令在内存中跳转的次数越少,那么速度就越快。
-
如果数据存放的距离比较近,那么处理器的处理速度就会更快一些(stack上)
-
如果数据之间的距离比较远,那么处理速度就会慢一些(heap上)
3.Stack vs Heap 函数调用
- 当代码调用函数时,值被传入到函数(也包括指向heap的指针)。函数本地的变量被压到stack上,当函数结束后,这些值会从stack上弹出
4.所有权存在的原因
-
所有权解决的问题
- 跟踪代码的那些部分正在使用heap的那个数据
- 最小化heap上的重复数据量
- 清理heap上未使用的数据以避免空间不足
-
一旦明白所有权,就不需要经常去想stack或heap了
-
管理heap数据是所有权存在的原因
5.所有权规则
- 每个值都有一个变量,这个变量是该值的所有者
- 每个值同时只能有一个所有者
- 当所有者超出作用域时,该值将被删除
6.变量作用域
- scope就是程序中一个项目的有效范围
7.String类型
-
String比那些基础标量数据类型更加复杂
-
字符串字面值:程序里手写的那些字符串值,他们是不可变的
-
rust还有第二种字符串类型:String
- 在heap上分配,能够存储-在编译时未知数量的文本
8. 创建String类型的值
- 可以使用from函数从字符串字面值创建出String类型
rust
let mut s = String::from("hello");
s.push_str(",world");
println!("{}",s);
- 为什么String类型的值可以更改,而字符串字面值却不能修改
- 因为他们处理内存的方式不同
9. 内存和分配
-
字符串字面值,在编译时就知道他的内容了,其文本内容直接被硬编码到最终的可执行文件里
- 速度快,高效,是因为其不可变性。
-
String类型,为了支持可变性,需要在heap上分配内存来保存编译时未知的文本内容
- 操作系统必须在运行时来请求内存,这步通过 String::from 来实现
-
当用完String之后,需要使用某种方式将内存返回给操作系统
- 这步,在拥有GC的语言中,GC会跟踪并处理不再使用的内存
- 没有GC的语言中,就需要我们去识别内存何时不再使用,并调用代码将它返回
- 如果没有做,就会内存浪费,如果提前做了,变量就会非法,必须一次分配,一次释放
-
rust采用了不同的方式,对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动的交还给操作系统。
-
当变量走出作用域的时候,rust会自动调用drop这个函数。
10. 变量和数据交互的方式:移动(move)
- 多个变量 可以与同一个数据使用一种独特的方式来交互
- let x = 5;
- let y = x;
- 整数是已知且固定大小的简单的值,这两个5被压到了stack中
-
string类型的
-
let s1 = String::from("hello");
-
let s2 = s1;
-
一个String由3个部分组成:
- 一个指向 存放字符串内容的内存 的指针
- 一个长度
- 一个容量
-
上面这些东西都在stack中
-
存放字符串内容的部分在heap中
-
长度len,就是存放字符串内容所需 的字节数
-
容量capacity是指String从操作系统总共获取 内存的总字节数
-
当s1赋值给s2的时候,String的数据被复制了一份
- 就是在stack中复制了一份指针、长度、容量
- 并没有复制指针所指向的heap上的数据
-
当变量离开作用域的时候,rust会自动调用drop函数,并将变量使用的heap内存释放
-
当s1、s2离开作用域时,它们都会尝试释放相同的内存
- 二次释放bug
-
为了保证内存安全
- rust让s1失效
- 当s1离开作用域的时候,rust不需要释放任何东西
- 深拷贝和浅拷贝
- 通常复制指针、长度、容量这种操作视为浅拷贝,但是rust让s1失效了,所以这种操作在rust中叫移动(Move)
- rust设计原则:不会自动创建数据的深拷贝
- 就运行时的性能而言,任何自动赋值的操作都是廉价的
11. 变量和数据交互的方式:克隆(clone)
- 如果真想对heap上面的String数据进行深拷贝,而不仅仅是复制stack上的数据,可以使用clone方法
rust
let mut s1 = String::from("hello");
s1.push_str(",world");
let s2 = s1.clone();
println!("{}+{}",s1,s2);
- 克隆这种操作,是比较消耗资源的
12. stack上的数据:复制
- copy trait ,可以用于像整数这样完全存放在stack上面的类型
- 如果一个类型实现了copy这个trait,那么旧的变量在赋值后仍然可以使用
- 如果一个类型或者该类型的一部分实现了 drop trait ,那么rust不允许它再去实现 copy trait
一些拥有copy trait的类型
- 任何简单标量的组合类型都可以是copy的
- 任何需要分配内存或某种资源的都不是copy的
- 一些拥有copy trait 的类型
- 所有的整数类型,例如u32
- bool
- char
- 所有的浮点类型,例如f64
- tuple元组,如果其所有的字段都是copy的,那么他就是copy的,(i32,i23)是,(i32,String)不是
13. 所有权与函数
- 在语义上,将值传递给函数和把赋值给变量是类似的
- 将值传递给函数,将会发生移动或者复制
rust
fn main() {
let a_string = String::from("hello");
take_ownershi(a_string);
let a_number: i32 = 2;
makes_copy(a_number);
}
fn take_ownershi(some_string: String) {
println!("{}",some_string);
}
// a_string 在这个位置释放
fn makes_copy(some_number: i32) {
println!("{}",some_number);
}
// a_number 在这个位置并没有释放
- 函数在返回值的过程中同样也会发生所有权的转移
- 一个变量的所有权总是遵循同样的模式
- 把一个值赋给其他变量时就会发生移动
- 当一个包含heap数据的变量离开作用域时,他的值就会被drop函数清理,除非数据的所有权移动到另一个变量上了