ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Rust所有权及引用

2022-02-25 15:31:39  阅读:161  来源: 互联网

标签:String s1 let 内存 所有权 Rust 引用


Rust 所有权和借用

Rust之所以可以成为万众瞩目的语言, 就是因为其内存安全性. 在以往内存安全几乎全都是通过GC的方式实现, 但是GC会引来性能、CPU以及Stop The World等问题, 在需要高性能的场景是不可以接受的,因此Rust使用一种与众不同的方式 解决内存安全问题: 所有权机制

Rust所有权

所有程序都必须和计算机的内存打交道, 如何从RAM中申请空间存放程序运行所需要的数据, 在不需要是回收内存空间, 成为了关键, 在计算机编程语言不断进化的过程中出现了三种解决方案:

  • 垃圾回收机制(GC) , 程序运行时RunTime 通过三色标记 引用计数 分代回收等算法 回收空闲内存 : Go Python Java

  • 手动管理内存的分配和释放, 编写通过函数调用的方式申请释放内存 : C malloc() free(), C++ new() delete()

  • 通过所有权机制管理内存, 在程序编译期间 确定内存申请 释放的时间, 将相关的数据硬编码到二进制程序中, 在程序运行期间不会有任何性能上的损耗

一段内存不安全的代码

int* foo() {
    int a;          // 变量a的作用域开始
    a = 100;
    char *c = "xyz";   // 变量c的作用域开始
    return &a;
}                   // 变量a和c的作用域结束

​ 这段C代码是可以顺利编译通过的 foo函数返回一个int指针类型, 但是变量a和c是foo函数内的局部变量, 我们都知道 函数和函数内的局部变量 都是存储在栈当中的, 当foo函数执行完成后 局部变量a,c及函数foo 在栈内申请的内存 就已经被回收了, 此时返回变量a的指针, 从而形成了悬空指针 (悬垂指针, 野指针) 因为a申请的内存数据在foo函数结束是已经被回收, 此时返回a的指针 指向的内存地址已经被回收或者被其他程序使用, 如果这块地址再次被其他程序申请到并放入数据, 那就跟我们程序预期的效果产生差异,容易导致程序崩溃.

例如: a程序中a的数据是100 , 回收后被其他程序申请存入数据为 "malloc"。

​ 我们再来看一下变量c, 变量c的问题在于内存的浪费, 也是对栈的空间的浪费, c变量申请的内存在他声明完成后没有任何操作, 但是他回收的时间需要在foo函数结束是才进行回收 产生了资源的浪费

​ 内存安全的问题一直都是令开发者头疼的问题, 所以如何保证内存安全成为我们对技术深度评判标准之一, Rust的所有权机制将解决大部分内存安全问题, 想要保证内存安全我们就需要对 堆 栈有足够的认知

堆 和 栈

堆和栈是编程语言最核心的两个数据结构, 在许多编程语言我们不需要深入了解, 因为GC会偷偷的无感知的帮我们进行内存的回收, 这也意味着性能的瓶颈, 但是对于Rust这种系统编程语言, 数据值位于栈 或 堆 上是很重要的, 因为他大大的影响程序运行时的性能

堆栈实际上都是我们RAM

栈 是按照顺序且连续存储值 并以相反的数据取值, 先进后出, 存储数据为进栈 , 取出数据为出栈。 栈中的数据值所申请的内存大小必须是已知的固定的内存空间, 如果数据值大小是未知的, 那么取出数据时, 你无法取出你想要的数据。

栈 通常存储的数据是 编程语言的内置的基本类型的数据 i32 i64 f32 f64 &str bool 、 函数、 函数内的局部变量 、堆指针地址、元祖

ulimit -s 用于查看操作系统的栈空间 间接的说明栈空间是有限的 如果申请栈内存空间超出栈 就会发生栈溢出 程序崩溃、Go内存逃逸分析 等场景

每一个程序运行时操作系统都会为其分配栈的内存空间 1-8M , 通常情况下不会出现栈溢出 如果出现死循环、深递归的时候就极有可能出现程序崩溃。

对比着栈来理解堆 更容易理解一些

栈是由cpu寄存器来访问控制回收, 堆是由开发者来控制堆内存的回收

栈中存储的数据值都是已知大小的数据, 堆内可以存储未知大小的动态数据 相对灵活 .

栈申请的内存用完立即释放, 堆内存需要根据生命周期和GC算法释放内存

栈是连续的内存空间, 堆是不连续的 很有可能会产生内存碎片 无法回收造成浪费

栈的空间是有限的, 堆的空间可以认为是无限的

栈为什么会比堆快

1.cpu高速缓存会缓存栈内的数据 不会缓存堆内的数据 跟他们的存储规则有关

2.栈是直接寻址 申请只存只需要移动一个指针即可, 堆是间接寻址的 首先要去栈内取得变量的堆指针, 才可以获取数据。

3.栈是由cpu的寄存器直接访问控制的

4.栈在程序开始运行就已经开辟好了内存空间, 而堆需要在程序运行时 运行到对应到指定位置才开辟内存空间

5.入栈比堆分配内存快, 因为入栈操作系统无需分配新的内存空间,只需将新数据放入栈顶

所有权原则

在理解堆栈的前提下, 更有利理解Rust的所有权

1.Rust中的每一个值 有且只有一个所有者(变量)

let s = String::from("teststr")  // 变量s就是字符串teststr的所有者

2.当所有者(变量)离开作用域范围时,这个值将被丢弃(free) 也就是释放内存空间

fn test() {
  let s = String::from("teststr")  // s为test函数中的局部变量
} // 函数执行完成  变量s 离开作用域 字符串teststr的内存将被释放 生命周期结束

简单介绍String类型

上边提到了String::from 方法 , 创建变量的类型是String

let s = String::from("teststr")  // 变量s就是字符串teststr的所有者

还有一种声明字符串的例子 这种声明的字符串类型是 字符串字面值 a 是被硬编码到程序的类型是&str 他不可修改

let a = "test"

所有权背后的数据交互

下面看这样一段代码

let x = 5;  // x 变量就是 整数5的所有者
let y = x;  // 拷贝 x 赋值给 y  最终x和y都等于5  且都可以调用 因为上述操作都是在栈中运作的 整数类型是rust的基本类型 基本类型赋值调用都会自动拷贝 不会在堆中进行分配使用  也不会引发所有权机制

// 可能有好奇宝宝 会想 这种栈中的的copy赋值 是不是太慢了些, 但是实际上在rust的基本类型足够简单 ,拷贝会非常快, 只需要赋值一个i32,4字节的内存即可

随即看这样一段代码:


let s1 = String::from("hello");
let s2 = s1;

println!("{}{}", s1, s2)
// 跟上边的整型拷贝很像吧 但是 String类型 并不是rust的基本类型  所以他是存放在堆上的 不会自动拷贝 此时打印s1,s2就会触发rust的所有权机制

// 我们可以先看一下上边这段代码具体发生了什么
//String类型是一个复杂的类型, 他的堆指针、字符串长度、字符串容量共同存放在栈中, 真实数据存放在堆中,下面我们分析 let s2 = s1 可能出现的两种情况
	1.拷贝栈上String堆指针 容量 长度 和存储在堆上的字节数组, 这就是深拷贝了
	2.只拷贝String的堆指针 容量 长度 8+8+8字节 理解为浅拷贝, 但是这样就跟Rust所有者机制产生了冲突  因为我们的数据的所有者有且只能有一个, 如果按照这种浅拷贝的情况 那么这个数据就出现了两个所有者, 那么当s1和s2离开作用域的时候都会释放同一块内存, 也称为二次释放, 导致内存污染 违背了Rust的所有权机制, 那么Rust是如何处理这种问题呢? 解决方法: 
	当s1将值赋值给s2的时候, Rust认为s1不再有效, 因此也无需在s1离开作用域后drop释放s1的内容, s1的数据的所有权已经转移给了s2, s1同时也就失效了, 不会产生二次释放的问题, 效率大大增加,

image

上图中就是第二中浅拷贝的情况rust解决的方案, s1赋值给s2后 s1自动失效, s2接管这块内存地址

深拷贝

Rust永远不会自动创建数据的"深拷贝", 因此, 任何的自动复制都不是深拷贝. 浅拷贝被认为运行时性能影响较小

let s1 = String::from("hehahi");
let s2 = s1.clone();  // 深拷贝
println!("{}{}", s1, s2)

此段代码编译运行畅通无阻, 因为s2 完成的clone了s1 包括栈内的堆指针 容量 长度 堆内的数据, 但是如果频繁使用clone深拷贝 将会带来性能上的降低。

函数参数传递及返回 所有权的转移

在变量作为参数传递给函数是, 同样会发生移动或者复制, 所有权就会对应的产生变化


fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s);             // s 的值移动到函数里 ...
                                    // ... 所以到这里不再有效

    let x = 5;                      // x 进入作用域

    makes_copy(x);                  // x 应该移动函数里,
                                    // 但 i32 是 Copy 的,所以在后面可继续使用 x

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

我们如果尝试在takes_ownership(s); 语句执行之后 打印s值 就会产生报错 因为s作为参数传递给takes_ownership函数 String类型 不是基本类型 不会自动拷贝, 所以String的所有权转移到函数内, 又转移给了println宏当中 但函数执行完成, String开辟的这块内存已经被释放了 所以在函数之后打印s 就会报错 ,但是如果makes_copy(x) 函数之后执行打印x 就不会报错的, 因为i32类型是基本类型, 存储在栈内会进行自动拷贝, 不会触发所有权机制 , 但如果不是存储在栈的数据 就需要将数据返回出来, 这样数据传来传去 很是麻烦, Rust就帮我们解决了这个问题 引入了借用机制。

借用

在Rust中借用 在变量前加& 就变成了借用 不会产生所有权的转移, 在其他语言我们称这样的变量是引用, 但是Rust解释器中明确表明 就称其为借用, Rust通过借用Borrow概念达成减少所有权传递程序复杂的目的: **获取变量的引用, 称之为借用 **, 可以很好的理解, 我们上学忘记带铅笔, 可以跟朋友同学去借, 但是在使用完成后, 要物归原主.这里排除老赖等极端情况...

引用与解引用

常规的引用是一个指针类型, 指向了对象存储的内存地址。 在下面我们创建一个x i32值的引用 y, 然后使用解引用得到内存中真实的数据

let x: i32 = 5;
let y = &x

assert_eq!(5, x)
assert_eq!(5, *y) // y 是 5这个i32类型的数据内存地址  *y就是反引用得到的就是内存中的真实的数据5

当然这个时候 x 和 y也都可以正常打印出来因为引用不会涉及到所有权转移的问题 x 的不会出现失效的情况

不可变引用


fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1); // 将s1的引用传递给函数

    println!("The length of '{}' is {}.", s1, len);
}

// 函数接受 String的引用 返回一个 usize类型  usize就是无符号的根据操作系统位数生成的整数类型 例如我们操作系统是64位 那就是u64 
fn calculate_length(s: &String) -> usize { 
    s.len()
}// 因为传入的是引用类型  所以函数执行完成后不会释放drop掉s 什么也不会发生, 通过下面看一下类型引用的整体结构

s            s1         

ptr    ->    ptr     ->   0  h
             len          1  e 
             cap          2  l
       										3  l
													4  o

上述场景我们函数传参的简易性有了, 我们不觉的想到如果想修改 数据的值可以吗, 接下来我们看下面的代码:


fn main() {
    let s1 = String::from("hello");

    calculate_length(&s1); // 将s1的引用传递给函数

}
 
fn calculate_length(s: &String) { 
    s.push_str(" world!"); // 再此处修改数据
}

push_str处就会报错。因为在rust中定义的引用 都是不可以更改原来的数据的 就好像我们去图书馆借书 看可以 但是如果在毁坏书籍 乱涂乱画是不被允许的, 那如何我想画就画呢? Rust 也帮我们解决了, 那就是定义引用的时候声明他是一个可变引用

可变引用

fn main() {
    let mut s1 = String::from("hello"); // 声明s1为可变参数

    calculate_length(&mut s1); // 将s1的引用传递给函数

}
 
fn calculate_length(s: &mut String) {  // 声明传递的参数必须是一个可变的String类型参数
    s.push_str(" world!"); // 再此处修改数据
}

这段代码就可以完美的运行了

但是可变引用必须遵从Rust的一个原则:可变引用同时只能存在一个, 也就是在同一个作用域中, 一个数据只能有一个可变的引用, 同时不可变可以拥有多个

也就是说 一本书我借给多个人 , 你们一堆人可以一起看, 其中只能有一个人可以对这本书 修改 , 这样的好处就是 Rust在编译时就避免了数据的竞争, 下面这段代码就出现了多引用:

fn main() {
  let mut s = String::from("hello");

	let r1 = &mut s;
	let r2 = &mut s;

	println!("{}, {}", r1, r2);
} 
// 这段代码就会报错  因为声明了两个可变引用 且他们在同一个作用域main函数中,第一个可变引用r1声明周期必须持续到print完成后 在r1的声明周期内又尝试创建了一个可变引用r2 引起了数据的竞争 




fn main() {
  let mut s = String::from("hello");

	let r1 = &s;
  println!("{}", r1); 
	let r2 = &mut s;   // 如果想要 一段代码中同时引用可变引用和不可变引用  他们的生命周期必须没有交集
	println!("{},", r2);  
} 

// 可变引用和不可变引用在新版本的编译器中是可以同时存在的, 1.31之前不可以
// 对于这种编译器的优化Rust专门去了一个名字NLL - Non-Lexical Lifetimes(NLL),, 就是专门找出某一个引用在作用域 } 结束之前就不在被使用的引用的位置


悬垂引用 (出现悬空指针、 也可称迷途指针 、 野指针)

悬空指针 就是 指针指向实际的数据, 但是这个值在使用之前之前就已经被释放掉了, 但是 指针 也就是引用存在, 释放掉的内存可能不存在任何值, 或者被其他程序变新使用了, 造成了数据污染 , 而Rust编译器可以永远保证 引用不悬垂。

发生悬垂的场景:

fn main() {
    let mut testStr = String::from("testing"); 
    let result = overhang(testStr); // 将String数据传给overhang函数 此时String的所有权转移到overhang函数当中
    println!("{}",result); // 悬空指针产生了因为引用真正数据已经被释放了 找不到原本你的数据了
}

fn overhang(mut s: String) -> &String {  // 
    s.push_str("123");  // 修改String
    &s  // 返回String 的引用
} // 在此处 s 离开当前作用域 s 被drop掉 内存释放 , 返回&s 危险

error : error[E0106]: missing lifetime specifier

这里出现了关于生命周期的概念: 程序中每一个变量都有对应的作用域, 当超出作用域之后变量就会被自动销毁 一句话说就是一个变量在创建 到 被释放的过程, 称之为生命周期.

不过即使不了解生命周期仅仅了解引用 就可以理解悬垂指针。

解决上述代码的方法:将String返回 而不是&String

fn overhang(mut s: String) -> &String {  // 
    s.push_str("123");  // 修改String
    s  // 返回String 的引用
} 

这样就没有任何问题了

本文部分参照: Rust圣经

标签:String,s1,let,内存,所有权,Rust,引用
来源: https://www.cnblogs.com/zjaiccn/p/15936255.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有