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