Rust 圣经 阅读 所有权和借用

所有权

栈(Stack)与堆(Heap)

栈何和堆的核心目标就是为程序在运行时提供可供使用的内存空间。

栈按照顺序存储值并以相反顺序取出值,后进先出。

增加数据叫进栈,取出数据叫出栈。

栈中的所有数据必须占用 已知且固定大小的空间。假设数据大小是未知的,那么在取出数据时,将不能取出想要的数据。

对于大小未知或可能变化的数据,我们需要将其存储在堆上。

当向堆上存储数据时,需要请求一定大小的空间,然后操作系统在堆的某处找到符合大小的空间,将其标注为已使用,并返回一个表示该位置地址的指针。该过程被称为在堆上分配内存。有时简称为"分配(allocation)"。

返回的指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用中,可以通过指针,来获取数据在堆上的实际内存位置,进而访问数据。

堆上的数据是无组织的。

性能区别

写入方面:入栈比在堆上分配内存要快。

因为入栈只需将新数据放在栈顶,而在堆上分配内存需要找到足够存放数据空间,还要做一些记录为下次分配做准备。

读取方面:出栈比从堆上读取数据更快。

首先,栈上的数据都是存储在CPU的高速缓存上,而堆上的数据只能存储在内存中。而高速缓存和内存的访问速度差异在10倍以上。

其次,访问堆上数据,必须先访问栈再通过栈上的指针来访问内存。

所有权和堆栈

当调用一个函数,传递给函数的参数(包括指向堆的指针和函数的局部变量)一次压入栈中,当函数调用结束时,这些值将中栈中按照相反的顺序依次移除。

所有权帮助避免内存泄漏。

所有权原则

规则:

  1. Rust 中每一个值都被一个变量所拥有,该变量称为值的拥有者。
  2. 一个值同时只能被一个变量拥有,一个值只能有一个拥有者。
  3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)。
String 类型

动态字符串类型: String,该类型被分配到堆上,因此可以动态伸缩,可以存储在编译时大小未知的文本。

基于字符串字面量来创建 String 类型:

rust 复制代码
let s = String::from("hello");

:: 是一种调用操作符,表示调用 String 中的 from 方法,因为 String 存储在堆上的动态的,所有它可以修改。

rust 复制代码
let mut s = String::from("hello");

s.push_str(", world!"); // push_str() 在字符串后追加字面值

println!("{}", s); // 将打印 `hello, world!`

变量绑定背后的数据交互

转移所有权
rust 复制代码
let x= 5;
let y = x;

5 绑定到变量 x;接着拷贝 x 的值赋给 y,最终 xy 都等于 5,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存。

只有存储在栈上的基本类型,rust 会自动拷贝。

rust 复制代码
let s1 = String::from("hello");
let s2 = s1;

String 不是基本类型,而且是存储在堆上的,因此不能自动拷贝。

实际上, String 类型是一个复杂类型,由存储在栈中的堆指针字符串长度字符串容量 共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存,至于长度和容量,容量是堆内存分配空间的大小,长度是目前已经使用的大小。

这里有两种情况:

  1. 深拷贝:拷贝了所有数据,包含堆上的数据和栈上的数据。对性能有很大的影响。
  2. 只拷贝了 String,即仅拷贝指针、长度、容量。但是这里就涉及到了所有权的问题,因为一个值只允许有一个所有者。这样的拷贝会诞生两个所有者。多个所有权可能会导致多次释放同一个内存,会导致内存污染

Rust 在当 s1 赋予 s2 后,Rust 认为 s1 不再有效,所有在 s1 离开作用域后不会 drop 任何东西。

当赋予后,原来的所有者会成为无效的引用。

这样的浅拷贝在Rust称为移动,因为原有的拥有者失效了。

rust 复制代码
fn main() {
    let x: &str = "hello, world";
    let y = x;
    println!("{},{}",x,y);
}

这段代码不会出问题,是因为x并不是字符串的拥有者,它只是引用了这个字面量。仅仅是对该引用进行了拷贝,二者都引用了同一个字符串。

克隆(深拷贝)

Rust 不会自动创建数据的深拷贝。 任何自动的复制都不是深拷贝。

可以使用 clone 方法来深度复制 string 中堆上的数据,而不仅仅是栈上的数据。

rust 复制代码
let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

克隆完整地复制了 s1 的数据,他们是两个不同的数据的所有者,所以不会出现错误。

频繁地使用克隆会降低程序的性能。

拷贝(浅拷贝)

浅拷贝只发生在栈上,因此性能很高。

基本类型在编译时是已知大小的,会被存储在栈上,这里的深浅拷贝没有区别。

copy

Rust 有一个叫做 copy 的特征,可以用在类型整型这样在栈上存储的变量。

当一个类型拥有 copy 特征,一个旧的变量被赋给其他变量后依然可用。

具体一个类型是否可以 copy 需要查看给定的文档来确认。

通用规则是 任何基本类型的组合可以copy,不需要分配内存或某种形式资源的类型可以 copy

下面是一些可以 copy 的类型:

  • 所有整数类型,比如 u32
  • 布尔类型, bool
  • 所有浮点数类型,比如 f64
  • 字符类型, char
  • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如, (i32, i32)Copy 的,但 (i32, String) 就不是。
  • 不可变引用 &T,但是 可变引用 &mut T 是不可以 Copy 的

函数传值与返回

将值传递给函数,一样会发生 移动复制

rust 复制代码
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 移出作用域。不会有特殊操作

函数返回值也有所有权。

rust 复制代码
fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值
                                        // 移给 s1

    let s2 = String::from("hello");     // s2 进入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移动到
                                        // takes_and_gives_back 中,
                                        // 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
  // 所以什么也不会发生。s1 移出作用域并被丢弃

fn gives_ownership() -> String {             // gives_ownership 将返回值移动给
                                             // 调用它的函数

    let some_string = String::from("hello"); // some_string 进入作用域.

    some_string                              // 返回 some_string 并移出给调用的函数
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域

    a_string  // 返回 a_string 并移出给调用的函数
}

所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦: 总是把一个值传来传去来使用它。 传入一个函数,很可能还要从该函数传出去,结果就是语言表达变得非常啰嗦,幸运的是,Rust 提供了新功能解决这个问题。

原来的可变变量在转移所有权时,是否可变取决于新的变量。

rust 复制代码
fn main() {
    let mut s = String::from("hello, ");
    s.push_str("world");
    println!("{:?}",s);
    
    // 只修改下面这行代码 !
    let  s1 = s; // 需要添加 mut

    s1.push_str("world");
    println!("{:?}",s1);
}

可以使用 ref 来引用一个变量,引用的不是值,而是变量,而不是转移所有权。

rust 复制代码
fn main() {
    #[derive(Debug)]
    struct Person {
        name: String,
        age: Box<u8>,
    }

    let person = Person {
        name: String::from("Alice"),
        age: Box::new(20),
    };

    // 通过这种解构式模式匹配,person.name 的所有权被转移给新的变量 `name`
    // 但是,这里 `age` 变量却是对 person.age 的引用, 这里 ref 的使用相当于: let age = &person.age 
    let Person { name, ref age } = person;

    println!("The person's age is {}", age);

    println!("The person's name is {}", name);

    // Error! 原因是 person 的一部分已经被转移了所有权,因此我们无法再使用它
    //println!("The person struct is {:?}", person);

    // 虽然 `person` 作为一个整体无法再被使用,但是 `person.age` 依然可以使用
    println!("The person's age from person struct is {}", person.age);
}
复制代码
![请添加图片描述](https://img-blog.csdnimg.cn/direct/c7f3d6a453674aae992f125bf7af6c2d.png)
相关推荐
吞掉星星的鲸鱼1 小时前
使用高德api实现天气查询
前端·javascript·css
lilye661 小时前
程序化广告行业(55/89):DMP与DSP对接及数据统计原理剖析
java·服务器·前端
zhougl9963 小时前
html处理Base文件流
linux·前端·html
花花鱼3 小时前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_3 小时前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
叠叠乐4 小时前
rust Send Sync 以及对象安全和对象不安全
开发语言·安全·rust
·薯条大王4 小时前
MySQL联合查询
数据库·mysql
careybobo4 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
niandb5 小时前
The Rust Programming Language 学习 (九)
windows·rust
杉之6 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue