所有权
栈(Stack)与堆(Heap)
栈何和堆的核心目标就是为程序在运行时提供可供使用的内存空间。
栈
栈按照顺序存储值并以相反顺序取出值,后进先出。
增加数据叫进栈,取出数据叫出栈。
栈中的所有数据必须占用 已知且固定大小的空间。假设数据大小是未知的,那么在取出数据时,将不能取出想要的数据。
堆
对于大小未知或可能变化的数据,我们需要将其存储在堆上。
当向堆上存储数据时,需要请求一定大小的空间,然后操作系统在堆的某处找到符合大小的空间,将其标注为已使用,并返回一个表示该位置地址的指针。该过程被称为在堆上分配内存。有时简称为"分配(allocation)"。
返回的指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用中,可以通过指针,来获取数据在堆上的实际内存位置,进而访问数据。
堆上的数据是无组织的。
性能区别
写入方面:入栈比在堆上分配内存要快。
因为入栈只需将新数据放在栈顶,而在堆上分配内存需要找到足够存放数据空间,还要做一些记录为下次分配做准备。
读取方面:出栈比从堆上读取数据更快。
首先,栈上的数据都是存储在CPU的高速缓存上,而堆上的数据只能存储在内存中。而高速缓存和内存的访问速度差异在10倍以上。
其次,访问堆上数据,必须先访问栈再通过栈上的指针来访问内存。
所有权和堆栈
当调用一个函数,传递给函数的参数(包括指向堆的指针和函数的局部变量)一次压入栈中,当函数调用结束时,这些值将中栈中按照相反的顺序依次移除。
所有权帮助避免内存泄漏。
所有权原则
规则:
- Rust 中每一个值都被一个变量所拥有,该变量称为值的拥有者。
- 一个值同时只能被一个变量拥有,一个值只能有一个拥有者。
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(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
,最终 x
和 y
都等于 5
,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存。
只有存储在栈上的基本类型,rust 会自动拷贝。
rust
let s1 = String::from("hello");
let s2 = s1;
String
不是基本类型,而且是存储在堆上的,因此不能自动拷贝。
实际上, String
类型是一个复杂类型,由存储在栈中的堆指针 、字符串长度 、字符串容量 共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存,至于长度和容量,容量是堆内存分配空间的大小,长度是目前已经使用的大小。
这里有两种情况:
- 深拷贝:拷贝了所有数据,包含堆上的数据和栈上的数据。对性能有很大的影响。
- 只拷贝了
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)