Rust | Ownership
一段不安全的代码
int* // 变量a和c的作用域结束
- 变量
a
和c
都是局部变量,函数结束后将局部变量a
的地址返回,但局部变量a
存在栈中,在离开作用域后,a
所申请的栈上内存都会被系统回收,从而造成了悬空指针(Dangling Pointer)
的问题。
虽然编译可以通过,但是运行的时候会出现错误。
- 变量
c
常量字符串,存储于常量区,"xyz"
只有当整个程序结束后系统才能回收这片内存。
栈(Stack)与堆(Heap)
- 栈中的所有数据都必须占用已知且固定大小的内存空间
- 对于大小未知或者可能变化的数据,需要存储在堆上
- 当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存,有时简称为 “分配”(
allocating
) - 接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据
- 当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存,有时简称为 “分配”(
- 写入
- 入栈比在堆上分配内存要快
- 入栈时操作系统无需分配新的空间,只需要将新数据放入栈顶即可
- 在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,接着做一些记录为下一次分配做准备
- 读取
- 得益于 CPU 高速缓存,使得处理器可以减少对内存的访问,高速缓存和内存的访问速度差异在 10 倍以上
- 栈数据往往可以直接存储在 CPU 高速缓存中,而堆数据只能存储在内存中
- 访问堆上的数据比访问栈上的数据慢,因为必须先访问栈再通过栈上的指针来访问内存
所有权原则
- Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
变量绑定背后的数据交互
转移所有权
let x = 5;
let y = x;
两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存
let s1 = String from;
let s2 = s1;
对于基本类型(存储在栈上),Rust 会自动拷贝,但是 String
不是基本类型,而且是存储在堆上的,因此不能自动拷贝。
String
类型是一个复杂类型,由存储在栈中的堆指针、字符串长度、字符串容量共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存,至于长度和容量,如果你有 Go 语言的经验,这里就很好理解:容量是堆内存分配空间的大小,长度是目前已经使用的大小。
String
类型指向了一个堆上的空间,这里存储着它的真实数据, 下面对上面代码中的 let s2 = s1
分成两种情况讨论:
- 拷贝
String
和存储在堆上的字节数组 如果该语句是拷贝所有数据(深拷贝),那么无论是String
本身还是底层的堆上数据,都会被全部拷贝,这对于性能而言会造成非常大的影响 - 只拷贝
String
本身 这样的拷贝非常快,因为在 64 位机器上就拷贝了8字节的指针
、8字节的长度
、8字节的容量
,总计 24 字节,但是带来了新的问题:一个值只允许有一个所有者,而现在这个值(堆上的真实字符串数据)有了两个所有者:s1
和s2
。
当变量离开作用域后,Rust 会自动调用 drop
函数并清理变量的堆内存。
当 s1
和 s2
离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free) 的错误。
所以,Rust 这样解决问题:当 s1
赋予 s2
后,Rust 认为 s1
不再有效,因此也无需在 s1
离开作用域后 drop
任何东西,这就是把所有权从 s1
转移给了 s2
,s1
在被赋予 s2
后就马上失效了。
看看在所有权转移后再来使用旧的所有者
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
Rust 会禁止你使用无效的引用
move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
x
只是引用了存储在二进制中的字符串 "hello, world"
,并没有持有所有权。
因此 let y = x
中,仅仅是对该引用进行了拷贝,此时 y
和 x
都引用了同一个字符串。
克隆(深拷贝)
Rust 永远也不会自动创建数据的 “深拷贝”。任何自动的复制都不是深拷贝。
如果确实需要深度复制 String
中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone
的方法。
let s1 = String from;
let s2 = s1.clone;
println!;
s2
完整的复制了 s1
的数据
拷贝(浅拷贝)
浅拷贝只发生在栈上
Rust 有一个叫做 Copy
的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy
特征,一个旧的变量在被赋值给其他变量后仍然可用。
任何基本类型的组合可以 Copy
,不需要分配内存或某种形式资源的类型是可以 Copy
的。如下是一些 Copy
的类型:
- 所有整数类型
- 布尔类型
- 所有浮点数类型
- 字符类型
- 元组,当且仅当其包含的类型也都是
Copy
的时候 - 不可变引用
&T
引用与解引用
常规引用是一个指针类型,指向了对象存储的内存地址。
不可变引用
通过 &s1
语法,我们创建了一个指向 s1
的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。
可变引用
s
是可变类型,其次创建一个可变的引用 &mut s
和接受可变引用参数 some_string: &mut String
的函数。
可变引用同时只能存在一个
// 老编译器中,r1、r2、r3作用域在这里结束
// 新编译器中,r3作用域在这里结束
引用作用域的结束位置从花括号变成最后一次使用的位置,因此 r1
借用和 r2
借用在 println!
后,就结束了,此时 r3
可以顺利借用到可变引用。
悬垂引用(Dangling References)
指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。
// 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!
解决方法是直接返回 String
:
String
的 所有权被转移给外面的调用者
- https://course.rs/basic/ownership/ownership.html
- https://course.rs/basic/ownership/borrowing.html