【Rust】004-Rust 所有权
文章目录
rust 语言很严谨!符合我严谨认真的性格!
一、预备知识
1、堆和栈
堆和栈 都可以在程序 运行时给程序 提供内存 空间,但是他们是有区别的。
想象一下,栈就像一叠盘子 。先来的盘子 在底部,新盘子 则放在顶部 。取用时,总是先取最上面的盘子 ,就像在餐馆 里洗完的盘子 先用先拿。这就是先进后出的 原则。但是,盘子 的大小必须是确定的,否则无法存放在这个"栈"里。
而堆则像一个大杂货仓库 。你可以随时放入各种大小 和形状 的物品 。为了方便找到它们,你需要贴上标签 ,这些标签 就像指针 ,告诉你物品 的位置 。虽然这样存放物品 灵活多变,但要找到合适的空间 并读取它们就不如"栈"中的盘子方便了。
在性能 的赛跑 中,栈就像速度 迅猛的跑车 ,堆则更像一辆载重卡车 。栈的读写速度 快,因为一切都井井有条 ,就像把盘子 放在最上面那样简单。而堆要先找到合适的存放空间 ,读取时还要通过标签 (指针 )找到物品 ,就像在杂货仓库 里寻找一个小小的零件 一样,可能需要多花点时间。
2、String 类型
在 Rust 编程 的世界 中,字符串 有点像我们现实生活 中的名片 (印好之后,不再改变)和便签 (可以继续往上面写东西)。有两种类型 让我们来认识它们:String和&str。
&str
存储逻辑
在 Rust 中,字符串字面量(如 "hello"
)的实际值通常存储在程序的只读数据段中。这个数据段是编译时确定的,并且在程序运行时是不可变的。这意味着字符串字面量在程序的生命周期内是常驻内存的,不会被修改或释放。
当你在代码中使用字符串字面量时,例如:
rust
let s: &str = "hello";
这个 &str
类型的变量 s
是一个胖指针,包含了指向 "hello"
的内存地址和字符串的长度 。这些信息本身存储在栈上,而 "hello"
的实际字符串数据则在程序的只读数据段中。
想象&str 是一种名片 ,它被称为字符 串字面值 ,就像是工厂 里生产出来的名片 ,印好了,就再也不能改了。你可以这样给自己制造一张名片:
rust
fn main() {
// 使用字符串字面量创建静态字符串
let hello = "hello world";
}
这张名片是固定的,不可变的 ,因为它是硬编码到程序中的,就像是直接印在墙上的字。
String
存储逻辑
- 堆上存储数据
String
的实际字符串数据存储在堆上。这意味着String
可以在程序运行时动态调整其大小,而不受栈大小的限制。
- 栈上存储元数据 :
String
类型在栈上存储了一些元数据,包括一个指向堆上数据的指针、字符串的长度以及当前的容量 (即分配的堆内存大小)。这些元数据使得String
可以管理堆上的内存。
- 容量与长度 :
- 长度 :表示当前存储在
String
中的字符数。 - 容量 :表示已经分配的内存空间,允许在不重新分配的情况下扩展字符串。
String
可能会预先分配比实际需要更多的内存,以优化性能,减少频繁的内存分配和复制操作。
- 长度 :表示当前存储在
- 增长策略 :
- 当
String
的内容增加到超出其当前容量时,String
会自动分配更大的内存块,并将现有数据复制到新分配的内存中。这通常是通过倍增策略来实现的,以平衡内存使用和性能。
- 当
- 所有权与内存管理 :
String
拥有其堆上数据的所有权,这意味着当String
被丢弃时,它会自动释放其占用的堆内存。这是 Rust 所有权模型和借用检查器的一个重要特性,确保了内存的安全管理。
- 可变性 :
String
是可变的,可以通过方法如push
或push_str
来追加数据,或者通过truncate
来减少长度。
但生活中不是所有的信息都是确定的,有时候我们需要一些便签 。这就是String
的角色了,比如你想要记录下用户的一些临时想法或命令。String
的数据存储在堆上,就像前面提到的杂货仓库,可以随时变动。你还记得堆和栈的故事吗?
创建一个String
就像是抓一张空白的便签纸,你可以从字符串字面值开始书写:
rust
fn main() {
// 创建一个可变字符串
let mut hi = String::new();
// 写入文字
hi.push_str("hello");
hi.push_str(" world");
}
二、所有权规则
1、所有权系统的三条规则
- Rust 中每个值都有一个所有者;
- 一个值同时只能有一个所有者;
- 当所有者离开作用域范围,这个值将被丢弃。
2、代码示例
rust
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)。
rust
let x = 5;
let y = x;
这段代码中,首先将 5 绑定到 x,接着再将 x 的值拷贝给 y。这两行执行完, x 和 y 都是 5,且都可以正常使用。稍稍改变一下这个例子。
而对于 String 这样的分配到堆上的复杂类型,发生的却是所有权的转移,而不是拷贝。
rust
let s1 = String::from("hello");
let s2 = s1;
简单类型:自动拷贝(简单类型的实际数据内容存储在栈上的);
复杂类型:所有权转移(拷贝的是内存地址,实际数据内容在堆上,但会导致一个数据被两个变量同时拥有,离开作用域会出现双重释放的情况,进而导致安全问题,因此Rust设计了所欲全转移机制!)。
复杂类型的拷贝
这种拷贝不存在所有权的转移,他们是相互独立的!
rust
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2); // s1 = hello, s2 = hello
4、函数的传值与返回
将值传给函数跟上面讲的赋值 类似,都会进行转移或者拷贝的过程。**函数返回一个值的时候,也会经历所有权转移的过程。**我们用下面的例子来说明:
rust
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、不可变引用(只读)
rust
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、可变引用(可读可写)
rust
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、重要规则
对于一个变量,同时只能存在一个可变引用或者多个不可变引用。
rust
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
,r2
和r3
是会报错的。但是这样其实会带来很多麻烦,导致代码很难写。于是 Rust 编译器做了一项优化:引用的作用域结束的位置不再是花括号的位置,而是最后一次使用的位置。因此,在现在 Rust 的版本中,上面的例子并不会报错。
6、悬垂引用
悬垂引用 指的是指针指向的是 内存中一个已经被释放的 地址**,这在其他的一些有指针 的语言 中是很常见的错误 。而 Rust 则可以在编译阶段 就保证不会产生悬垂引用。也就是说,如果有一个引用指 向某个数据 ,编译器 能保证在引用离开作用域 之前,被指向的数据不会被释放。
错误代码
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、字符串切片
基本写法
rust
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("{}", s);
println!("{}", hello);
println!("{}", world);
}
简化写法
rust
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 本身就是数组
rust#[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,数组类型也有切片。例如:
rust
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
五、总结
本节内容较多,主要包含了三部分的知识:所有权,借用和切片。所有权这套系统是 Rust 内存安全的重要保障。有了这套系统,我们既可以享受不需要手动释放内存的便利,又可以对内存使用有足够的控制,保证内存安全。