Rust Diary DAY2 —— Ownership and reference
所有权和借用
Rust的内存安全性是其最为显著的特征。在以往,内存安全几乎都是通过GC的方式实现,但是GC会引来性能、内存占用的问题。
Rust因此采用了所有权系统。
所有权
所有的程序都得和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成了重中之重。
在语言的演变中,产生了三种流派
- GC: 程序运行的时候不断寻找不再使用的内存,eg: java go
- 手动管理: 程序员自己去申请和释放内存,eg: C C++
- 所有权系统: 编译器在编译期就能知道内存的使用情况,eg: Rust
例子
1 | int* foo(){ |
a和c都是局部变量,但是局部变量a存在栈中,离开作用域之后,a申请的栈上内存都会被回收,从而造成了悬空指针的问题。
变量c是常量字符串,存储在常量区域, "xyz" 只有在整个程序结束后才能回收这片内存。
所以内存安全问题,一直都是程序员很头痛的问题。
Stack和Heap
栈
栈按照顺序存储值,并以相反顺序取出值,也叫做后进先出。
增加顺序叫做进栈,移出数据叫做出栈。
因为上述的实现方式,栈中的所有数据都必须占用已知且固定大小的内存空间。假设数据大小未知,那么在取出数据的时候无法得到想要的。
堆
对于大小未知或者可能变化的数据,我们需要将它存储在堆上。
当向堆上放入数据的时候,需要请求一定大小的内存空间,操作系统在堆上某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该地址的指针,该过程被称为在堆上分配内存。
接着,指针会被放到栈中,因为指针的大小是固定的。后续,我们通过栈中的指针来找到堆上的实际位置进而访问该数据
由上可知,堆是一种缺乏组织的数据结构
性能区别
栈上分配内存更快。因为在堆上放的时候需要先找到一块足够存放数据的内存空间。接着做一些记录为下一次分配做准备。
所有权和堆栈
当你的代码调用一个函数,传递给函数的参数依次被压入栈,调用结束时,这些值将按照相反的顺序被移除。
因为堆上数据缺乏组织,因此跟踪这些数据何时分配和释放时非常重要的。否则堆上的数据将内存泄漏
所有权原则
- Rust中每一个值都被一个变量所拥有,该变量被称为值的所有者。
- 一个值只能同时被一个变量拥有,一个值只能拥有一个所有者。
- 当所有者离开作用域时,该值将被丢弃。
变量作用域
作用域时一个变量在程序中有效的范围
1 | let s = "hello"; // s 进入作用域 |
变量s绑定到了一个字符串字面值,该字符串字面值是硬编码到程序代码中的。
s 变量从声明的点开始直到当前作用域的结束都是有效的。
简单介绍一下string
我们已经见过字符串字面值 let s : &str = "hello" , s是被硬编码进程序的字符串值。字符串字面值是很方便的,但是它并不适用于所有场景
- 字符串值是不可变的,因为被硬编码到了程序中
- 并非所有字符串都能在编写代码的时候得知
例如,字符串是需要程序运行的时候由用户动态输入然后存储在内存中的,这种情况下,字符串字面值就完全无用武之地。
Rust提供了动态字符串类型 String, 该类型被分配在堆上,因此可以动态收缩,也就能存储在编译时大小未知的文本。
可以使用下面的方法基于字符串字面量来创建String类型
1 | let s = String::from("hello"); |
:: 是一种调用运算符,表示调用String类型的from关联函数。因为string存储在堆上,因此它是动态的。可以这样修改:
1 |
|
变量绑定背后的数据交互
转移所有权
1 | let x = 5; |
这段代码并没有发生所有权的转移: 代码先将5绑定到变量x,接着拷贝x值给y,最终x和y都等于5. 因为整数是rust基本数据类型,是固定大小的简单值。因此这两个值都是通过自动拷贝的方式来赋值的。
1 |
|
String是一个复杂类型,由存储在栈中的堆指针,字符串长度,容量共同组成,其中堆指针是最重要的。它指向了真实存储字符串的堆内存。至于长度和容量,就是: 容量指堆内存分配空间的大小,长度是目前已经使用的大小。
例如vector. 我们一般都会存一个capacity,一个size.用来实际存储数据的大小和分配的空间大小。
总之,String类型指向了一个堆上的空间,这里存储着它的真实数据,下面对上面的代码分成两种情况讨论
-
拷贝String 和 存储在堆上的字节数组。 如果该语句是拷贝所有数据(深拷贝) 那么无论是 String 本身 还是底层的堆上数据,都会被全部拷贝。
这对于性能而言会造成非常大的影响。 -
只拷贝String本身。这样很快但是如果这样的话,s1和s2都会指向同一块堆内存,这样就会出现问题。因为当s1和s2都离开作用域时,Rust会尝试释放这块堆内存两次,从而造成了悬空指针的问题。
因此,rust这样解决问题。
当 s1被赋给了s2之后,rust会认为s1已经不再有效了,s1的所有权被转移给了s2。也就是说,s1不再是一个有效的变量,不能再使用它。
1 |
|
想必读者都听说过: 浅拷贝和深拷贝。那么拷贝指针,长度和容量而不拷贝数据,这听起来就像是浅拷贝。但是又因为Rust同时使用第一个变量s1无效了,因此这个操作被称为移动(move),而不是浅拷贝。
上面的例子可以解读为s1被移动到了s2中。
再看一个
1 |
|
这段代码实际上不会报错。因为和String有一个本质的区别,string中 s1持有了通过 String::from() 分配的堆内存,而在这里,x只是引用了存储在二进制可执行文件中的字符串。并没有持有所有权
因此 let y = x 只是对这个值进行了拷贝
克隆(深拷贝)
Rust不会自动创建数据的深拷贝,因此,任何自动的复制都不是深拷贝。可以被认为对运行时的性能影响比较小
如果我们确实需要深度复制String中堆上的数据,而不仅仅是栈上的数据,可以使用clone方法
1 | let s1 = String::from("hello"); |
如果性能无关紧要,可以使用clone简化编程体验。
但是对于执行较为频繁的代码,最好别用。
拷贝(浅拷贝)
1 | let x = 5; |
原因是,整形这样的基本类型在编译的时候是已知大小的,会被存储在栈上,所以拷贝实际值是极快的。
Rust有一个叫做Copy的特征,可以用在类似整形这样在栈中存储的类型。
如果一个类型拥有了Copy特征,一个旧的变量在被赋值给其他变量之后仍然可用。
那么什么类型是可以copy的呢? 一般认为基本类型的组合是可以的。不需要分配内存或者形式资源的类型是可以copy的。
- 整数类型
- 布尔类型
- 浮点类型
- 字符类型
- 元组类型,只要元组中的类型都是Copy的,那么这个元组类型也是Copy的
- 不可变引用 &T
函数传值和返回
将值传给函数,一样会发生移动 or 复制
1 |
|
同样的,函数的返回值也具有所有权,例如
1 |
|








