1、所有权是rust里面最特殊的一个特性,它可以让rust无需gc就能够保证它的内存安全
那么什么是所有权?简单来说就是定义的一系列规则来管理内存的释放问题。当程序运行时,不会影响程序本身的运行速度
2、stack和heap
在很多的语言中,我们不需要考虑数据是在stack中还是在heap中,但是rust需要有这方面的考虑
在像Rust这样的系统级编程语言里,一个值是在stack 上还是在 heap 上对语言的行为和你为什么要做某些决定是有更大的影响的
所有存储在Stack 上的数据必须拥有已知的固定的大小
编译时大小未知的数据或运行时大小可能发生变化的数据必须存放在 heap 上
Heap内存组织性差一些:
当你把数据放入heap 时,你会请求一定数量的空间
操作系统在 heap 里找到一块足够大的空间,把它标记为在用,并返回一个指针,也就是这个空间的地址
这个过程叫做在heap 上进行分配,有时仅仅称为“分配”
把值压到stack上不叫分配
因为指针是已知固定大小的,可以把指针存放在stack 上
但如果想要实际数据,你必须使用指针来定位。
把数据压到stack上要比在heap 上分配快得多
因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在stack的顶端
在heap 上分配空间需要做更多的工作
操作系统首先需要找到一个足够大的空间来存放数据,然后要做好记录方便下次分配
访问heap 中的数据要比访问stack中的数据慢,因为需要通过指针才能找到heap 中的数据
对于现代的处理器来说,由于缓存的缘故,如果指令在内存中跳转的次数越少,那么速度就越快
如果数据存放的距离比较近,那么处理器的处理速度就会更快一些(stack 上)
如果数据之间的距离比较远,那么处理速度就会慢一些(heap 上)
在heap 上分配大量的空间也是需要时间的
函数调用
当你的代码调用函数时,值被传入到函数(也包括指向heap 的指针)。函数本地的变量被压到stack 上。当函数结束后,这些值会从stack上弹出
所有权存在的原因
所有权解决的问题:
-跟踪代码的哪些部分正在使用heap的哪些数据
-最小化 heap 上的重复数据量
–清理heap 上未使用的数据以避免空间不足。
一旦你懂的了所有权,那么就不需要经常去想stack或heap 了。
但是知道管理heap 数据是所有权存在的原因,这有助于解释它为什么会这样工作。
3、所有权规则
每个值都有一个变量,这个变量是该值的所有者
每个值同时只能有一个所有者
当所有者超出作用域(scope)时,该值将被删除
变量的作用域
Scope就是程序中一个项目的有效范围
fn main() {
// s不可用
let mut s = "123"; // s可用
// 可以对s进行相关的操作
} // s的作用域结束,s不再可用
内存和分配
字符串字面值,在编译时就知道它的内容了,其文本内容直接被硬编码到最终的可执行文件里
―速度快、高效。是因为其不可变性。
String 类型,为了支持可变性,需要在heap 上分配内存来保存编译时未知的文本内容:
―操作系统必须在运行时来请求内存。
这步通过调用String:from来实现
-当用完String 之后,需要使用某种方式将内存返回给操作系统
这步,在拥有GC的语言中,GC会跟踪并清理不再使用的内存
没有GC,就需要我们去识别内存何时不再使用,并调用代码将它返回。
―如果忘了,那就浪费内存。
一如果提前做了,变量就会非法
―如果做了两次,也是Bug。必须一次分配对应一次释放
Rust采用了不同的方式:对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动的交还给操作系统。
fn main() {
let mut s = String::from("Hello");
s.push_str(", World");
println!("{}",s)
}
rust中的变量在离开作用域的时候它会调用一个特殊的函数drop来释放内存
变量和数据交互的方式:移动(Move)
多个变量可以与同一个数据使用一种独特的方式来交互
let x = 5;
let y = x;
整数是已知且固定大小的简单的值,这两个5被压到了stack 中
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离开作用域时,它们都会尝试释放相同的内存:
二次释放(double free) bug
为了保证内存安全:
Rust没有尝试复制被分配的内存
Rust 让s1失效。
试试看当s2创建以后再使用s1是什么效果?
我先说结果,会报错
fn main() {
let mut s1 = String::from("Hello");
let s2 = s1;
println!("{}",s1)
}
变量和数据交互的方式:移动(Move)
浅拷贝(shallow copy)
深拷贝( deep copy)
你也许会将复制指针、长度、容量视为浅拷贝,但由于Rust 让s1失效了,
所以我们用一个新的术语:移动(Move)
灰色的部分就表示s1已经失效了
隐含的一个设计原则:Rust不会自动创建数据的深拷贝
就运行时性能而言,任何自动赋值的操作都是廉价的
变量和数据交互的方式:克隆(Clone)
如果真想对heap 上面的String数据进行深度拷贝,而不仅仅是stack 上的数据,可以使用clone 方法(以后再细说,先看个例子)
fn main() {
let s1 = String::from("Hello");
let _s2 = s1.clone();
println!("{}",s1)
}
Stack 上的数据:复制
fn main() {
let x = 5;
let y = x;
println!("{},{}",x,y);
}
这里就不会报错,为啥呢?因为x,y都是整数类型,他们都存放在stack上,能够自动复制它的值。
Copy trait,可以用于像整数这样完全存放在 stack上面的类型
如果一个类型实现了Copy这个trait,那么旧的变量在赋值后仍然可用
如果一个类型或者该类型的一部分实现了Drop trait,那么Rust不允许让它再去实现Copy trait 了
一些拥有Copy trait 的类型
任何简单标量的组合类型都可以是Copy 的
任何需要分配内存或某种资源的都不是Copy的
一些拥有Copy trait的类型:
所有的整数类型,例如u32
bool
char
所有的浮点类型,例如f64
Tuple (元组),如果其所有的字段都是Copy的。
(1i32, i32)是
(i32,String)不是
4、所有权与函数
在语义上,将值传递给函数和把值赋给变量是类似的:
将值传递给函数将发生移动或复制
fn main() {
let s = String::from("Hello World");
take_ownership(s);
let x = 5;
make_copy(x);
println!("x:{}",x)
}
fn take_ownership(sonme_string:String) {
println!("{}",sonme_string);
}
fn make_copy(x:i32) {
println!("{}",x)
}
返回值与作用域
函数在返回值的过程中同样也会发生所有权的转移
fn main() {
let _s1 = give_ownership();
let s2 = String::from("Hellow World");
let _s3 = take_and_gives_back(s2);
}
fn give_ownership() -> String {
let sonme_string = String::from("Hello World");
sonme_string
}
fn take_and_gives_back(a_string:String) -> String {
a_string
}
一个变量的所有权总是遵循同样的模式:
把一个值赋给其它变量时就会发生移动
当一个包含heap 数据的变量离开作用域时,它的值就会被drop函数清理,除非数据的所有权移动到另一个变量上了
如何让函数使用某个值,但不获得其所有权?
fn main() {
let _s1 = String::from("Hello World");
let s = calculate_length(_s1);
println!("{},{}",s.0,s.1)
}
fn calculate_length(s:String) -> (String,usize) {
let len = s.len();
(s,len)
}
这种做法确实可以,但是太麻烦了,也太笨了
下面我们来看一下怎么优雅的实现:Rust有一个特性叫做“引用(Reference)”