rust的所有权与引用

所有权

  • 所有权是rust最独特的特性,它让rust无需GC就可以保证内存安全。

什么事所有权

  • rust的核心特性就是所有权
  • 所有程序在运行时都必须管理他们使用计算机内存的方式
  1. 有些语言有垃圾收集机制,在程序运行时它们会不同地寻找不再使用的内存
  2. 在其他语言中,程序员必须显式地分配和释放内存
  • rust采用了第三种方式
  1. 内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则
  2. 当程序运行时,所有权特性不会减慢程序的运行速度。

stack(栈) vs heap(堆)

  • 在像rust这样的系统级编程语言里,一个值是在stack上还是在heap上对语言的行为和你为什么要做某些决定是有很大的影响的
  • 在你的代码运行的时候,stack和heap都是你可用的内存,但他们的结构很不相同。

stack vs heap 存储数据

  • stack按值的接受顺序存储,按相反的顺序将他们移除(后进先出,LIFO)
  1. 添加数据叫做压入栈
  2. 移除数据叫做弹出栈
  • 所有存储在stack上的数据必须拥有已知的固定的大小
  1. 编译时大小未知的数据或运行时大小可能变化的数据必须存放在heap上
  • heap内存组织性差一些
  1. 当你把数据放入heap时,你会请求一定数量的空间
  2. 操作系统在heap里找到一块足够大的空间,把它标记在用,并放回一个指针,也就是这个空间地址
  3. 这个过程叫做在heap上进行分配,有事仅仅成为“分配”
  • 把值压到stack上不叫分配
  • 因为指针是已知固定大小的,可以把指针存放在stack上。
  1. 但如果想要实际数据,你必须使用指针来定位。
  • 把数据压到stack上要比在heap上分配快的多
  1. 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在stack的顶端
  • 在heap上分配空间需要做更多的工作
  1. 操作系统首先需要找到一个足够大的空间来存放数据,然后要做好记录方便下次分配

stack vs heap 访问数据

  • 访问heap中的数据要比访问stack中的数据慢,因为需要通过指针才能找到heap中的数据
  1. 对于现代的处理器来说,由于缓存的缘故,如果指令在内存中跳转的次数越少,那么速度就越快
  • 如果数据存放的距离比较近,那么处理器的处理速度集合更快一下(stack上)
  • 如果数据直接的距离比较远,那么处理速度就会慢一下(heap上)
  1. 在heap上分配大量空间也是需要时间的

stack vs heap 函数调用

  • 当你的代码调用函数是,值被传入到函数(也包括指向heap的指针)。函数本地的变量被压到stack上。当函数结束后,这些值会从stack上弹出

stack vs heap 所有权存在的原因

  • 所有权解决的问题
  1. 跟踪代码的那些部分正在使用heap的哪些数据
  2. 最小化heap上的重复数据量
  3. 清理heap上为使用的数据以避免空间不足。
  • 一旦你懂的了所有权,那么就不需要经常去想stack或heap了。
  • 但是知道管理heap数据是所有权存在的原因,这个有助于解释它为什么会这样工作。

所有权规则

  • 每个值都有一个变量,这个变量是该值的所有者
  • 每个值同时只能有一个所有者
  • 当所有者超出作用域(scope)时,该值将被删除

变量作用域

  • scope就是程序中一个项目的有效范围
fn main(){
    // s 不可用
    let s = "hello";    // s 可用
    // 可以对 是进行相关操作
} // s 作用域到此结束,s 不再可用

string 类型

  • string 比那些基础标量数据类型更复杂
  • 字符串字面值:程序里手写的那些字符串值。他们是不可变的
  • rust还有第二种字符串类型:String
  1. 在heap上分配。能够存储在编译时未知数量的文本。

创建string类型的值

  • 可以使用from函数从字符串字面值创建string类型
//"::"表示from是string类型下的函数
let s = String::from("hello");
  • 这类字符串是可以被修改的
fn main(){
    let mut str = String::from("hello");
    str.push_str(", world");
    println!("{}", str);
}
  • 为什么String类型的值可以修改,而字符串字面值不能被修改?

内存和分配

  • 字符串字面值,在编译时就知道它的内容了,其文本内容直接被硬编码到最终的可执行文件里
  1. 速度快、高效。是因为其不可变性。
  • String类型,为了支持可变性,需要在heap上分配内存来报错编译时位置的文本内容:
  1. 操作系统必须在运行时来请求内存
  2. 这步通过调用String::from来实现
  • 当用完String之后,需要使用某种方式将内存返回给操作系统
  1. 这步,在拥有GC的语言中,GC会跟踪并清理不再使用的内存
  2. 没有GC,就需要我们去识别内存何时不再使用,并调用代码将它返回。
  3. 如果忘记了,那就浪费内存
  4. 如果提前做了,变量就会非法
  5. 如果做了两次,也是bug,必须一次分配对应一次释放
  • rust采用不同的方式:对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动的交给操作
  • drop函数

变量和数据交互的方式:移动(move)

  • 多个变量可以与同一个数据使用一种独特的方式来交互
let x = 8;
let y = x;
  • 整数是已知且固定大小的简单的值,这个两个8被压到了stack中

变量和数据交互的方式:移动(move) String版本

let s1 = String::from("hello");
let s2 = s1;
  • 一个String由3部分组成:
  1. 一个执行存放字符串内容的内存的指针
  2. 一个长度
  3. 一个容量
namevalue
prt
len5
capacity5
indexvalue
0h
1e
2l
3l
4o
  • 上面这些东西都是反正stack上。
  • 存放字符串内容的部分在heap上
  • 长度len,就是存放字符串内容所需要的字节数
  • 容量capacity是指String从操作系统总共获取的内存总字节数
  • 当把s1赋给s1,String的数据被复制了一份
  1. 在stack上复制了一份指针、长度、容量
  2. 并没有复制指针所指向的heap上的数据
let s1 = String::from("hello");
let s2 = s1;
  • 当变量离开作用域时,rust会自动调用drop函数,并将变量使用的heap内存释放。
  • 当s1、s2离开作用域时,它们都会尝试释放相同的内存
  1. 二次释放(double free)bug
  • 为了保证内存安全
  1. rust没有尝试复制被分配的内存
  2. rust让s1失效
  3. 当s1离开作用域的时候,rust不需要释放任何东西
  • 试试看当s2创建以后,再使用s1是什么效果
fn main(){
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{}, {}", s1, s2);
}
  • 浅拷贝
  • 深拷贝
  • 你也许会将复制指针、长度、容量视为浅拷贝,但由于rust让s1失效了,所以我们用一个新的术语:移动(move)
  • 隐含的一个设计原则:rust不会自动创建数据的深拷贝
  1. 就运行时性能而言,任何自动赋值的操作都是廉价的

变量和数据交互的方式:克隆(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的类型
  1. 所有的整数类型,例如u32
  2. bool
  3. char
  4. 所有的浮点类型,例如f64
  5. tuple(元组),如果其所有的字段都是copy的
  6. (i32, i32) 是
  7. (i32, String) 不是

所有权与函数

  • 在语义上,将值传递给函数和把值赋给变量是类似的
  1. 将值传递给函数将发送移动或复制
fn main(){
    let s = String::from("hello world");
    take_ownership(s);  // s作为实参,s移动后,失效
    let x = 4;
    makes_copy(x);      // x 作为实参,x拷贝,不失效
    println!("x:{}", x);
    // println!("s:{}", s);
}

fn take_ownership(some_string: String){
    println!("{}", some_string);    // 方法结束后  some_string 失效
}

fn makes_copy(some_number: i32){
    println!("{}", some_number);  // 方法结束后  some_number 失效
}

返回值与作用域

  • 函数在返回值的过程中同样也会发生所有权的转移
  • 一个变量的所有权总是遵循同样的模式
  1. 把一个值赋给其他变量时就会发生转移
  2. 当一个包含heap数据的变量离开作用域时,他的值会被drop函数清理,除非数据的所有权转移到另外一个变量上了
fn main(){
    let s1 = gives_ownership();
    let s2 = String::from("hello_2");
    let s3 = takes_and_gives_back(s2);      //a_string实参数 传入函数,移动,失效
    println!("s1:{}", s1);
    // println!("s2:{}", s2);
    println!("s4:{}", s3);
}

fn gives_ownership()-> String{
    let some_string = String::from("hello_1");
    some_string         // some_string 作为函数返回值,移动,失效
}

fn takes_and_gives_back(a_string: String)-> String{
    a_string       // a_string参数 作为函数返回值,移动,失效
}

如何让函数使用某个值,但不获得某所有权?

  • rust有一个特性叫做“引用(reference)”
fn main(){
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    // print!("s1:{}", 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");
    // s1作为引用,传递到方法里面
    // 方法参数没有s1所有权,所以,s1不会失效
    let len = calculate_length(&s1);  
    print!("s1:{}, len:{}", s1, len);
}

fn calculate_length(s: &String)-> usize{
    // 不能修改借用的东西
    // s.push_str(", world");
    s.len()
}

借用

  • 我们吧引用作为函数参数这个行为叫做借用
  • 是否就可以修改借用的东西(不行)
  • 和变量一样,引用默认也是不可变得

可变引用

fn main(){
    let mut s1 = String::from("hello");
    // s1作为引用,传递到方法里面
    // 方法参数没有s1所有权,所以,s1不会失效
    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;
    // let s2 = &mut s;
    println!("s1:{}", s1);
    // println!("s2:{}", s2);
}
  1. 这样做的好处是可在编译时防止数据竞猜
  • 一下三种行为下回发生数据竞争
  1. 两个或多个指针同时访问多个数据
  2. 至少有一个指针用于写入数据
  3. 没有使用任何机制来同步对数据的访问
  • 可以通过创建新的作用域,来允许非同时的创建多个可变引用
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;
    // let s3 = &mut s;
    println!("s1:{}", s1);
    println!("s2:{}", s2);
    // println!("s3:{}", s3);
}

悬空引用dangling references

  • 悬空指针(dangling pointer):一个指针引用了内存中的某个地址,而这块内存可能已经释放并分配给其它人使用了。
  • 在rust里,编译器可保证引用永远都不是悬空引用;
fn main(){
    let r = dangle();
}

fn dangle()-> &String{
    let s = String::from("hello");
    &s      // s 在方法调用结束后失效,返回一个失效的引用
}

引用的规则

  • 在任何给定的时刻,只能满足下列条件之一
  1. 一个可变的引用
  2. 任意数量不可变的引用
  • 引用必须一直有效
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值