07|所有权:值的生杀大权到底在谁手上?
该思维导图由 AI 生成,仅供参考
变量在函数调用时发生了什么
- 深入了解
- 翻译
- 解释
- 总结
Rust语言的内存管理机制是初学者学习过程中的难点,尤其是所有权和生命周期的概念。本文从传统编程语言中堆内存多次引用所带来的内存管理问题出发,比较了C/C++、Java、ObjC/Swift等语言的解决方案,并引入了Rust语言的独特思路。Rust通过限制开发者随意引用的行为来解决内存管理问题,采用单一所有权模式、Move语义和Copy语义来解决堆内存管理的问题。文章深入讨论了Rust的所有权和内存管理机制,介绍了其独特的所有权模式和解决方案,为读者提供了对Rust语言核心特点和解决内存管理问题的深入思考。同时,文章还提出了一个关键问题:谁真正拥有数据或值的生杀大权,这种权利可以共享还是需要独占?这一问题为读者提供了对Rust语言的核心特点和解决内存管理问题的思路的深入思考。通过本文的阐述,读者可以更好地理解Rust语言的设计理念和特点。
《陈天 · Rust 编程第一课》,新⼈⾸单¥68
全部留言(52)
- 最新
- 精选
- 赵岩松第一题:可以 只要栈上的数据生命周期大于堆上数据的生命周期就可以 简单来说就是在堆上数据被回收之前栈上的数据一定会存在的情况下,是可以的 ``` let x = 1; let y = 2; let v = vec![&x, &y]; println!("{:?}", v); ``` 第二题:因为其为基本数据类型,实现了Copy trit 在find_pos()的栈帧中,第二个参数栈上保存的是0x0000_002A并不是main()中v所在的栈地址,没有所有权的转移(Copy trit) 也就是说find_pos()函数中无论怎么改变入参v的值,在find_pos()函数结束的时候也不会导致main()中v值的回收,故main()中v的值是不会改变的,是安全的 在这里说一下我对所有权的理解 首先,接触Rust之后我发现Rust里的新名词虽然很多,但是如果抛开名词本身转而去思考程序运行过程中的堆栈分析就可以比较快速的理解 首先来关注所有权规则的最后一点:当所有者离开作用域,其拥有的值被丢弃,"内存得到释放" 最后一点表明了提出所有权这个概念要做的事情,就是为了内存回收 那么在单一所有权的限制下,如何理解内存分配呢? 在这里我暂且用 x -> a 表示指针x指向数据a 在其他语言中,内存中可以出现如下的情况 x -> a; y -> a; z -> a; ... 但是在Rust中,假设最初为 x -> a; 当我们接下来需要 y -> a 时,我们可以认为x不会被使用了,也就是 x -> a 这个引用在"我的理解上"就已经断了(所有权转移) 在执行过程中被引用的数据只会有一个"有效的"指针指向它(所有权唯一) 那么来看第一题,问的是堆上数据是否可以引用栈上的数据,我选择抛开堆栈不谈,因为不管分配到堆栈上都是分配到了内存上 在所有权机制的限制之下,可不可以引用这个问题其实就变成了如何避免悬垂引用,那么如何避免呢?使用生命周期(老师在抛砖引玉xdm)
作者回复: 理解地非常透彻! 的确如此,Rust 就是这样从最根本的问题出发来解决它。所有权是第一步,借用和生命周期是第二步。:)
2021-09-06140 - dotfiles常见的内存安全问题: 内存泄漏(非内存安全问题) , 堆栈溢出(迭代器/运行时检查), 重复释放, 悬垂指针; 所有权先解决重复释放的问题. rust中,为了处理内存管理问题,要求每个内存对象(无论堆上或者栈上)仅有一个所有者,也就是所有权. 当所有者超出其作用域时,其管理的内存对象将会被释放, 这里分两种: 栈上内存由编译器自动管理,无需额外释放. 堆上内存会调用内存对象的Drop Trait. 这里就保证了不会发生重复释放. rust中为了保证一块内存仅有一个所有者, 在所有权转移时(赋值,函数调用,函数返回)默认使用move语义, 也就是转移对象所有权. 除非对象实现了copy语义,那么会优先使用copy语义. copy语义的作用类似于浅拷贝,仅拷贝栈上的内存.如基础类型, 裸指针,组合类型(其成员全部实现copy语义), 引用等.此时还是一块内存仅有一个所有者,只是内存被复制了一份. 因为栈上通常内存不大,那么此时发生了消耗较少的拷贝. 在rust语言机制上,不允许copy trait和drop trait同时实现,因为允许copy的,都在栈上. 栈上的内存管理是不需要开发者操心的,只有堆上的内存需要, 类似于C++的析构函数. 在rust语言机制上,clone trait是copy trait的supertait,也就是基类. copy trait的调用是由编译器默认调用的, 而clone trait则是开发者通过clone方法调用的.在了解了copy语义的作用后,clone语义也比较好理解,基本就是深拷贝了.那么深拷贝后的堆内存,通常也需要实现Drop Trait以保证内存不泄漏. clone相较栈消耗要大得多,因此为了避免拷贝,就引入了*borrow*的概念,类似C++的引用. 但引用又会带来悬垂指针的问题,这就需要通过*生命周期*来解决. 以上就是目前对所有权的理解.
作者回复: 非常好!
2021-10-01350 - pedro另外对按位复制补充一点: 按位复制,等同于 C 语言里的 memcpy。 C 语言中的 memcpy 会从源所指的内存地址的起始位置开始拷贝 n 个字节,直到目标所指的内存地址的结束位置。但如果要拷贝的数据中包含指针,该函数并*不会*连同指针指向的数据一起拷贝。 因此如果是不包含指针的原生类型,那么按位复制(浅拷贝)等同于 clone,可如果是 Vec 这种在堆上开辟,在栈上存储胖指针的数据就不一样了,因为按位复制会拷贝胖指针本身,而其指向的堆中数据则不会拷贝,因此堆上的数据仍然只有一份。 最后,最好不用去实现 Copy。
作者回复: 对。不过实现 Copy 并不会影响程序的正确性。不会出现拷贝可能会被释放的内存的指针的问题。 Rust 在设计时就已经保证了你无法为一个在堆上分配内存的结构实现 Copy。所以 Vec / String 等结构是不能实现 Copy 的。因为这条路已经被堵死了:Copy trait 和 Drop trait 不能共存。一旦你实现了 Copy trait,就无法实现 Drop trait。反之亦然。 有同学看到裸指针 *const T/ *mut T 实现了 Copy,就会想如果我用 unsafe 把 Vec<T> 的指针取出来,组成一个数据结构,到处 Copy,然后其中一个 drop 后,岂不就造成 use after free,破坏了 Rust 的安全性保证?很遗憾,Rust 并不允许你这么做。因为你无法实现 Drop。 我写了一段代码,感兴趣的同学可以看一下: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4828e734f6f161dfce32098333a1aaa5 ```rust use std::{fmt, slice}; #[derive(Clone, Copy)] struct RawBuffer { ptr: *mut u8, len: usize, } impl From<Vec<u8>> for RawBuffer { fn from(vec: Vec<u8>) -> Self { let slice = vec.into_boxed_slice(); Self { len: slice.len(), // into_raw 之后,Box 就不管这块内存的释放了,RawBuffer 需要处理 ptr: Box::into_raw(slice) as *mut u8, } } } // 如果 RawBuffer 实现了 Drop trait,就可以在所有者退出时释放堆内存 // 然后,Drop trait 会跟 Copy trait 冲突,要么不实现 Copy,要么不实现 Drop // 如果不实现 Drop,那么就会导致内存泄漏,但它不会对正确性有任何破坏 // 比如不会出现 use after free 这样的问题。 // 你可以试着把下面注释掉,看看会出什么问题 // impl Drop for RawBuffer { // #[inline] // fn drop(&mut self) { // let data = unsafe { Box::from_raw(slice::from_raw_parts_mut(self.ptr, self.len)) }; // drop(data) // } // } impl fmt::Debug for RawBuffer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let data = self.as_ref(); write!(f, "{:p}: {:?}", self.ptr, data) } } impl AsRef<[u8]> for RawBuffer { fn as_ref(&self) -> &[u8] { unsafe { slice::from_raw_parts(self.ptr, self.len) } } } fn main() { let data = vec![1, 2, 3, 4]; let buf: RawBuffer = data.into(); // 因为 buf 允许 Copy,所以这里 Copy 了一份 use_buffer(buf); // buf 还能用 println!("buf: {:?}", buf); } fn use_buffer(buf: RawBuffer) { println!("buf to die: {:?}", buf); // 这里不用特意 drop,写出来只是为了说明 Copy 出来的 buf 被 Drop 了 drop(buf) } ```
2021-09-06835 - hughieyu感觉Rust就是把多重引用下的堆内存回收问题转变成了单一所有权下的值的使用问题
作者回复: 对的。堆内存成了像栈一样的受控内存,只不过栈内存是受栈帧控制,堆内存受其栈上的所有者控制。再回顾一下堆内存的使用场景:1. 存放栈无法处理的内存(过大,或者长度不定,或者需要动态增长),2. 在同一个调用栈中真正需要被多个数据结构共享 3. 在多个调用栈中共享 Rust 通过单一所有权的限制,让第一种也是使用情况最多的场景能够很好地处理。对于后两种,需要开个小口子,这就是我们接下来要讲的内容。
2021-09-06212 - 苏苏前两节课只是跑起来代码能运行感觉知识点大而懵, 但是像这节课这样干货比较多的,细节点讲的清清楚楚的就很喜欢。
作者回复: 第一周介绍为啥要学 Rust(why),上周的课让大家领略 Rust 能干啥(what),下面要解决怎么干(how)。:)
2021-09-0611 - pedro1,在 Rust 下,分配在堆上的数据结构可以引用栈上的数据么?为什么? 可以,以 Vec 为例,它可以存储栈上的引用,但必须注意一点那就是该引用的生命周期不能超过栈上数据的生命周期。 2,main() 函数传递给 find_pos() 函数的另一个参数 v,也会被移动吧?为什么图上并没有将其标灰? 很简单,i 是原生类型,默认实现了 Copy 语义,在传参时,默认不再是移动而是 copy。 提一下,rust 的参数传递是反直觉的,默认为 move 和不可变,而其它主流语言默认都是 copy 和可变的,想要达到一样效果,必须实现 Copy 以及加上 mut。 要充分记住这一点,这是 rust 安全的生命线。
作者回复: 1 / 2 完全正确。 > 其它主流语言默认都是 copy 这里不完全正确。很多语言会根据类型决定是传值还是传引用。传值是 Copy,传引用不是 Copy,类似 Rust 的借用,但又不一样。我们后续会讲到。
2021-09-0610 - blackonion可以理解为是否实现copy trait主要看rust编译器能否在编译时就能确定所需大小吗?
作者回复: 并不是。Vec<T> 在编译时可以确定大小(24 字节),但它不能实现 Copy trait。任何有资源需要释放的数据结构,都无法实现 Copy trait。
2021-09-064 - 永不言弃感觉会C++的来学这个应该比较容易懂, 我这种从Java和Go过来的,很多概念都听不懂,尤其是Rust的语法,我真是好蛋疼,有太多语法看不懂是啥意思
编辑回复: 既然语法不太懂,那可以先去看rust的官方文档把不同的语法大概过一遍,再深入学习。不用着急的,学习嘛,以自己的知识积累和能力进步为准,找准自己的阶段性问题,针对性攻克。加油 💪
2022-10-21归属地:北京1 - 一期一会感觉学完所有权、生命周期的内容,就可以开始用rust刷leetcode了。之前抄的别的语言的实现代码,一直莫名其妙的报错,原来就是这块问题。
作者回复: 👍 大部分应该可以
2021-09-161 - 老荀催更…第一次学习像追剧一样
作者回复: 哈,别急别急。让子弹飞一会 :)
2021-09-061