rust的所有权和生命周期问题,借用问题

我们知道所有权系统是rust语言内存安全的重要原因之一。
举个例子,比如main函数里面声明一个动态数组arr,并把这个数组作为参数传到另一个函数中。
(要知道vector或者int *p=new int[5].这种都是存储在堆上的,因为编译时期不知道大小。然后在栈上会有一个指针指向堆的地址。)
当我们进行把这种传参,或者赋值,或者作为返回参数,都会产生复制的语义。也就是变量的拥有者不唯一了。同时,这就会产生很多问题。这样堆上的同一块内存就会有多个引用或者指针指向它,可能会产生垂悬指针多次释放内存的问题,不知道到底什么时候该释放内存。
比如局部对象引用返回,这个会有一个临时引用,再赋值给接受的变量,但是函数结束后局部对象就销毁了,因此这个返回的引用是一个垂悬指针。
再比如含有指针对象的赋值,赋值默认都是浅拷贝,所以也会有多个指针指向一块内存,必须自己实现赋值构造函数。

对于这种现象,C++是没办法禁止的,只有靠一些经验,避免产生这种操作,但是有意无意的,总会出错的。Java 等语言使用追踪式 GC,通过定期扫描堆上数据还有没有人引用,来替开发者管理堆内存,不失为一种解决之道,但 GC 带来的 STW 问题让语言的使用场景受限,性能损耗也不小。ObjC/Swift 使用自动引用计数(ARC),在编译时自动添加维护引用计数的代码,减轻开发者维护堆内存的负担。但同样地,它也会有不小的运行时性能损耗。
有人会说了,C++的RAII+智能指针可以避免,确实,智能指针确实可以缓解,通过引用计数的方式,当引用为0类自动使用析构函数释放内存。但是智能指针并不严格,比如转移了move指针p所有权之后,还可以定义*p语句,编译器不会报错,运行就会出错。

而rust就引用了所有权的概念。
原则之一就是,一个值只能被一个变量所拥有。
然后,当出现函数传参,赋值,函数返回值时,都会有所有权的转移,move语义。比如第一个例子,当动态数组作为参数传到另一个函数中,这个堆上的内存所有权就转移到另一个函数实参代表的变量上了。主函数不能再对原来的动态数组做操作了。这样,就确保了堆上的内存依旧只有唯一的引用

可以看到,所有权规则,解决了谁真正拥有数据的生杀大权问题,让堆上数据的多重引用不复存在,这是它最大的优势。

但是,这也会让代码变复杂,尤其是一些只存储在栈上的简单数据,如果要避免所有权转移之后不能访问的情况,我们就需要手动复制,会非常麻烦,效率也不高。比如int a=2; int b, b=a;如果这样a就不能用了,必须手动复制才可以,这也太麻烦了。所以,rust还有一个copy语义,就是说,当发生赋值,参数传参,返回值时,不是移动语义了,而是浅复制。比如b=a,就不是把2这个内存的所有权转给b,而是按位拷贝一份。
那么,一些原生类型比如整数,浮点数等简单类型,组合类型,以及函数,裸指针都是copy语义,这些都比较小,浅拷贝栈上可以放的下去,不会转移所有权。
而对于一些无法确定大小的比如动态数组,无法存放在栈中,只能存放在堆内存,通过栈上的指针来引用,这些就没有copy语义。

借用
还有一些数据结构既没有实现Copy trait,也不想转移所有权。这时候就需要用到今天的主角–Borrow语义。Borrow 语义通过引用语法(& 或者 &mut)来实现。所有权还是原来的人的,你知识借用一下。
比如传参的时候,用&arr,就是借用了,只读借用实现了copy语义,也就是堆上数据的指针会复制一份给函数,但是函数并不拥有数据本身,数据只是临时借给它使用,所有权还在原来的拥有者那里。
这样,因为引用是move语义,但是我们不想转移所有权,只要借用&一下就可以了。
这样一来,只有一个所有者data可以释放堆上数据arr,而借用浅复制的指针虽然也指向这个引用data,但是并不能释放堆内存的。

生命周期(花括号是一般变量的生命周期)
前面借用,虽然借用的不能释放堆内存,但是如果data把堆内存释放了,借用就变成了垂悬指针了,不能再访问内存了。所以,对于借用的生命周期有个限制。生命周期长的不能借用生命周期短的,比如main函数的变量借用其他函数返回的局部对象的借用,这是不行的。

可变借用
允许借用的变量修改内存的东西。在同一作用域下,可变借用只能有一个,且不可变借用和可变借用不能同时存在。这个和读写锁的思想差不多。

特殊情况:一个值多个拥有者
有2个指针指向同一个节点。
多个线程要访问同一块共享内存。
编译期是无法检查到这些情况的,所以rust除了静态检查,还提供了运行时动态检查来满足这些特殊需求。Rust的处理思路,大部分场景能在编译器处理,这样能保证安全性和性能要求;运行时检查,会牺牲部分性能,来提高灵活性
那Rust在运行时如何做检查呢?答案可能会令你比较失望:还是用到了引用计数rc和智能指针box

先看Rc,对一个数据结构T,我们可以创建引用计数Rc,让它有多个所有者。Rc会把对应的数据结构创建堆上。如果想对数据创建更多的所有者,我们可以通过clone()来完成。
use std::rc::Rc;
fn main() {
let a = Rc::new(1);
let b = a.clone();
let c = a.clone();
}
对一个Rc结构进行clone(),不会将其内部的数据赋值,只会增加引用计数。当一个Rc结构离开作用域被drop()的时候,只会减少其引用计数,直到引用计数为0,才会真正清除对应的内存。
实际上a才是真正的所有者,b,c在clone()后,得到了一个新的Rc,从编译器的角度,a,b,c都各自拥有一个Rc。所以Rc的clone()并不复制实际的数据,只是把引用计数+1了

那么Rc是怎么在堆上产生的?且这段内存不受栈内存的生命周期所控制呢?这里就要提到 Box::leak机制了.
这种机制可以让Rust像C/C++那样,创建一块堆内存,且不受栈内存的控制,这样才能绕过编译器的所有权规则。有点类似C/C++里的malloc()分配的内存。

总结:所有权机制,就是,对堆上内存的多重引用不复存在了。
比如,C++中,赋值,传参,返回值都是复制语义。如果是一个在栈上指向堆内存的指针, 那么很有可能会造成垂询指针的问题。
举例子:比如函数中vector,或者int p=new int(5)作为参数,那么它会复制一份,也就是堆上内存有两份引用了;比如类如果有指针对象,复制的时候默认是浅复制,也是有两份引用;比如局部对象的指针返回,也是两份,出了函数对象销毁,而另一个就变成垂悬指针了。
这种情况在C++不可避免,都是靠经验的。有人可能会说智能指针,确实它弥补了一部分,但是。但是智能指针并不严格,比如转移了move指针p所有权之后,还可以定义
p语句,编译器不会报错,运行就会出错。

所有权就是对于一些move语义的(堆内存),所有权会转移;对简单类型,还是复制(必须实现copy trait);
并且还有借用&语义,就是没有copy,但不想转移,就借用,借用的无法释放资源,但可以读写(读写锁类似)

并且,对于一些个值多个拥有者,还有类似于共享指针的box机制。

猜你喜欢

转载自blog.csdn.net/weixin_53344209/article/details/129994182