【Rust】004-Rust 所有权


文章目录

  • 【Rust】004-Rust 所有权
  • 一、预备知识
  • 1、堆和栈
  • 2、String 类型
  • &str
  • String
  • 二、所有权规则
  • 1、所有权系统的三条规则
  • 2、代码示例
  • 3、所有权转移
  • 简单示例
  • 复杂类型的拷贝
  • 4、函数的传值与返回
  • 三、引用与借用
  • 1、借用
  • 2、不可变引用(只读)
  • 3、可变引用(可读可写)
  • 4、重要规则
  • 5、NLL
  • 6、悬垂引用
  • 错误代码
  • 四、切片
  • 1、概念
  • 2、字符串切片
  • 基本写法
  • 简化写法
  • 3、其他切片
  • 五、总结


rust 语言很严谨!符合我严谨认真的性格!

一、预备知识

1、堆和栈

堆和栈都可以在程序运行时给程序提供内存空间,但是他们是有区别的。



想象一下,栈就像一叠盘子。先来的盘子在底部,新盘子则放在顶部。取用时,总是先取最上面的盘子,就像在餐馆里洗完的盘子先用先拿。这就是先进后出的原则。但是,盘子大小必须是确定的,否则无法存放在这个“栈”里。



而堆则像一个大杂货仓库。你可以随时放入各种大小形状物品。为了方便找到它们,你需要贴上标签,这些标签就像指针,告诉你物品位置。虽然这样存放物品灵活多变,但要找到合适的空间并读取它们就不如“栈”中的盘子方便了。



性能赛跑中,栈就像速度迅猛的跑车,堆则更像一辆载重卡车。栈的读写速度快,因为一切都井井有条,就像把盘子放在最上面那样简单。而堆要先找到合适的存放空间,读取时还要通过标签指针)找到物品,就像在杂货仓库里寻找一个小小的零件一样,可能需要多花点时间



2、String 类型

Rust 编程世界中,字符串有点像我们现实生活中的名片(印好之后,不再改变)和便签(可以继续往上面写东西)。有两种类型让我们来认识它们:String和&str

&str

存储逻辑

在 Rust 中,字符串字面量(如 "hello")的实际值通常存储在程序的只读数据段中。这个数据段是编译时确定的,并且在程序运行时是不可变的。这意味着字符串字面量在程序的生命周期内是常驻内存的,不会被修改或释放。

当你在代码中使用字符串字面量时,例如:

let s: &str = "hello";

这个 &str 类型的变量 s 是一个胖指针,包含了指向 "hello"内存地址和字符串的长度。这些信息本身存储在栈上,而 "hello" 的实际字符串数据则在程序的只读数据段中。



想象&str是一种名片,它被称为字符串字面值,就像是工厂里生产出来的名片,印好了,就再也不能改了。你可以这样给自己制造一张名片

fn main() {

    // 使用字符串字面量创建静态字符串
    let hello = "hello world";

}

这张名片是固定的,不可变的,因为它是硬编码到程序中的,就像是直接印在墙上的字。



String

存储逻辑

  1. 堆上存储数据
  • String 的实际字符串数据存储在堆上。这意味着 String 可以在程序运行时动态调整其大小,而不受栈大小的限制
  1. 栈上存储元数据
  • String 类型在栈上存储了一些元数据,包括一个指向堆上数据的指针、字符串的长度以及当前的容量(即分配的堆内存大小)。这些元数据使得 String 可以管理堆上的内存。
  1. 容量与长度
  • 长度:表示当前存储在 String 中的字符数。
  • 容量:表示已经分配的内存空间,允许在不重新分配的情况下扩展字符串。String 可能会预先分配比实际需要更多的内存,以优化性能,减少频繁的内存分配和复制操作。
  1. 增长策略
  • String 的内容增加到超出其当前容量时,String 会自动分配更大的内存块,并将现有数据复制到新分配的内存中。这通常是通过倍增策略来实现的,以平衡内存使用和性能。
  1. 所有权与内存管理
  • String 拥有其堆上数据的所有权,这意味着当 String 被丢弃时,它会自动释放其占用的堆内存。这是 Rust 所有权模型和借用检查器的一个重要特性,确保了内存的安全管理。
  1. 可变性
  • String 是可变的,可以通过方法如 pushpush_str 来追加数据,或者通过 truncate 来减少长度。

但生活中不是所有的信息都是确定的,有时候我们需要一些便签。这就是String的角色了,比如你想要记录下用户的一些临时想法或命令。String的数据存储在上,就像前面提到的杂货仓库,可以随时变动。你还记得堆和栈的故事吗?

创建一个String就像是抓一张空白的便签纸,你可以从字符串字面值开始书写:

fn main() {
    
    // 创建一个可变字符串
    let mut hi = String::new();
    // 写入文字
    hi.push_str("hello");
    hi.push_str(" world");

}



二、所有权规则

1、所有权系统的三条规则

  1. Rust 中每个值都有一个所有者;
  2. 一个值同时只能有一个所有者;
  3. 当所有者离开作用域范围,这个值将被丢弃。


2、代码示例

fn main() {
    // 规则 1:Rust中每个值都有一个所有者
    let s1 = String::from("hello"); // s1 是值 "hello" 的所有者

    {
        // 规则 2:一个值同时只能有一个所有者
        let s2 = s1; // 所有权从 s1 转移到 s2(s1 不再有效)
        // println!("{}", s1); // 这会导致编译时错误,因为 s1 不再有效
        println!("{}", s2); // 这是允许的,因为 s2 现在拥有该值
    } // s2在此处超出范围

    // 规则 3:当所有者离开作用域范围,这个值将被丢弃
    // 由于 s2 超出范围,因此为值 "hello" 分配的内存在此处自动释放。
}`



3、所有权转移

简单示例

i32这样的简单类型,赋值的时候 Rust 会自动进行拷贝(Copy)。

let x = 5;
let y = x;

这段代码中,首先将 5 绑定到 x,接着再将 x 的值拷贝给 y。这两行执行完, x 和 y 都是 5,且都可以正常使用。稍稍改变一下这个例子。

而对于 String 这样的分配到堆上的复杂类型,发生的却是所有权的转移,而不是拷贝。

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

简单类型:自动拷贝(简单类型的实际数据内容存储在栈上的);

复杂类型:所有权转移(拷贝的是内存地址,实际数据内容在堆上,但会导致一个数据被两个变量同时拥有,离开作用域会出现双重释放的情况,进而导致安全问题,因此Rust设计了所欲全转移机制!)。



复杂类型的拷贝

这种拷贝不存在所有权的转移,他们是相互独立的!

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

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



4、函数的传值与返回

将值传给函数跟上面讲的赋值类似,都会进行转移或者拷贝的过程。**函数返回一个值的时候,也会经历所有权转移的过程。**我们用下面的例子来说明:

fn takes_ownership(s: String) {
    println!("Received string: {}", s);
} // s 离开作用域,被丢弃

fn gives_ownership() -> String {
    String::from("hello")
} // 返回了String的所有权

fn main() {
    // s 拿到了"hello"的所有权
    let s = String::from("hello");
    // 所有权转移给了 takes_ownership 函数的参数:s
    takes_ownership(s); // s转移到了函数内,不再可用

    // s 不再可用

    // 此处还可以声明一个 s ,是因为上面的 s 已经被回收了!
    let s = gives_ownership(); // s 获得了返回值的所有权
}



三、引用与借用

1、借用

只使用变量,而不拿走所有权,叫“借用”!



2、不可变引用(只读)

fn main() {
    // s1 拿到"hello"的所有权
    let s1 = String::from("hello");

    // 使用 &s1 而不是 s1,借出去,只是借出去,并不允许值被改变
    // 使用 len 接收返回值
    let len = calculate_length(&s1);

    // s1 仍然具有"hello"的所有权
    // len 是借用出去后所得到 len() 的返回值
    println!("The length of '{}' is {}.", s1, len);
}

// 使用 &String 表示借用,是 String 类型的引用
fn calculate_length(s: &String) -> usize {
    // 从借来的 s 取得 len() 的值,并返回
    s.len()
}



3、可变引用(可读可写)

fn main() {
    // s 拿到 "hello" 的所有权,s 本身是可修改的
    let mut s = String::from("hello");
    // 将 s 借出去,并允许被修改
    change(&mut s);
    // s 的值被修改了
    println!("The updated string is: {}", s);
}

// 这里使用 &mut String 来接收,表示要求可被修改
fn change(s: &mut String) {
    // 修改借来的数据
    s.push_str(", world!");
}



4、重要规则

对于一个变量,同时只能存在一个可变引用或者多个不可变引用。

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

    // 多个不可变引用是允许的
    let r1 = &s;
    let r2 = &s;
    println!("r1: {}, r2: {}", r1, r2);
    // 在这里,多个不可变引用是允许的,因此打印不会引发错误。

    // 一个可变引用
    let r3 = &mut s;
    println!("r3: {}", r3);
    // 在这里,只有一个可变引用,因此打印不会引发错误。

    // 不允许同时存在可变引用和不可变引用
    // let r4 = &s; // 这会导致编译时错误
    // 如果这里只打印 r4 是不会报错的,因为 r3 在上面已经释放,但此处打印了 r3 ,r3 就不会在此前被释放了!
    // println!("r3: {}, r4: {}", r3, r4);
    // 如果取消注释上面两行,同时存在可变引用和不可变引用将导致编译时错误。
}



5、NLL

在老版本的 Rust 编译器中(1.31之前),确实上述的r1,r2r3是会报错的。但是这样其实会带来很多麻烦,导致代码很难写。于是 Rust 编译器做了一项优化:引用的作用域结束的位置不再是花括号的位置,而是最后一次使用的位置。因此,在现在 Rust 的版本中,上面的例子并不会报错。



6、悬垂引用

悬垂引用指的是指针指向的是内存中一个已经被释放的地址**,这在其他的一些有指针语言中是很常见的错误。而 Rust 则可以在编译阶段就保证不会产生悬垂引用。也就是说,如果有一个引用指向某个数据编译器能保证在引用离开作用域之前,被指向的数据不会被释放。

错误代码

fn main() {
    let reference_to_nothing = dangle();
}
fn dangle() -> &String {
    let s = String::from("hello");

    // 这里会报错,因为 s 已经被释放了!返回的地址是一个无效的地址!
    // this function's return type contains a borrowed value, but there is no value for it to be borrowed from
	// 该函数返回了一个借用的值,但是没有可以借用的来源
    // 引用必须是有效的
    &s
}



四、切片

1、概念

切片可以让我们引用集合中的一段连续空间。切片也是一种引用,因此没有所有权



2、字符串切片

基本写法

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

    let hello = &s[0..5];
    let world = &s[6..11];

    println!("{}", s);
    println!("{}", hello);
    println!("{}", world);


}



简化写法

let s = String::from("hello");
let len = s.len();

// 以0开始时,0可以省略
let slice = &s[0..2];
let slice = &s[..2];

// 以最后一位结束时,len可以省略
let slice = &s[3..len];
let slice = &s[3..];

// 同时满足上述两条,那么两头都可以省略
let slice = &s[0..len];
let slice = &s[..];



3、其他切片

String 本身就是数组

#[derive(PartialEq, PartialOrd, Eq, Ord)] #[stable(feature = "rust1", since = "1.0.0")] #[cfg_attr(not(test), lang = "String")] pub struct String { vec: Vec<u8>, }

除了 String,数组类型也有切片。例如:

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);



五、总结

本节内容较多,主要包含了三部分的知识:所有权,借用和切片。所有权这套系统是 Rust 内存安全的重要保障。有了这套系统,我们既可以享受不需要手动释放内存的便利,又可以对内存使用有足够的控制,保证内存安全。