本篇文章包含的内容

  • 1 重新从堆和栈开始考虑
  • 2 所有权规则
  • 3 变量和数据(值)的交互方式
    • 3.1 移动 Move
    • 3.2 克隆 Clone
    • 3.3 复制 Copy
  • 4 函数与所有权
    • 4.1 参数传递时的所有权转移
    • 4.2 函数返回时的所有权转移
  • 5 引用和借用
  • 6 切片

前面两篇仅仅介绍了一些Rust的语法以及一些程序书写特点。如果是其他语言,其实已经可以说完成了六成以上的学习,可以开始着手项目,以实践驱动学习了。但所有权和生命周期才是Rust的魅力所在,真正的难点现在才刚刚开始(噔噔咚)。


1 重新从堆和栈开始考虑

所有权是Rust最独特的特性之一,使得它与Java、C#等语言相比不需要GC(Garbage Collector,垃圾收集器)就可以保证内存安全,同时也不需要像C/C++一样手动释放内存。为了理解所有权,我们必须了解Rust的内存分配机制,这是在之前学习的语言中基本不会注意的点。

无论哪种语言编写的程序,都必须考虑他们运行时对计算机内存的操作方式。Rust并不相信程序员,但是也摒弃了GC算法这种低效的方式,取而代之的是引入所有权的概念,使程序中的内存操作错误在编译时就基本解决,并且这种做法不会造成任何的运行时开销。

在程序运行时,堆(Heap)和栈(Stack)都是程序可用的内存,它们的本质区别是内存组织的方式不同。栈内存先入后出,永远有一个指针指向栈顶,内存的存储是连续的,所有存储在栈中的数据必须有已知的或者固定的大小;而堆内存相对比较混乱,程序使用的内存是碎片化的,一般在运行时申请的动态内存都属于堆内存,操作系统在申请Heap时,需要申请一个足够大的空间,并返回一个额外的指针变量记录变量的存储位置(并且需要做好记录和管理方便下次分配),这导致程序运行时的指针可能存在大范围的跳转。总之,栈内存效率更高,堆内存以牺牲效率为代价换取了更多的灵活性。

所有权解决了以下问题:

  • 跟踪代码的哪些部分正在使用Heap的哪些数据;
  • 最小化Heap上的重复数据量;
  • 及时清理Heap上未使用的数据以避免空间不足。

2 所有权规则

Rust中所有权有以下三条规则(它很重要,先记下来再慢慢理解):

  1. 每个值都有一个变量,这个变量就是这个值的所有者;
  2. 每个值同时只能有一个所有者;
  3. 当所有者超出作用域(Scope)时,该值将被删除。

下面是一个关于作用域(Scope)的简单例子。作用域的概念在其他编程语言中也有,这里需要理解的是,s是变量,“hello”就是这个变量的值(一个字符串字面值)。

// s 无效
fn main() {// s 无效let s = "hello";	// s 可用// s 继续有效
}	// s 的作用域从这里结束

通过第一部分的解释,这里就比较好理解变量s的存储方式了。它的值在编译时就已经全部确定,并且不会随之变化(如果需要变化则需要引入String类型),所以这个变量和它的值在编译时就会被全部写入可执行文件中。

与之相比,String类型在堆上分配,这使得它可以存储在编译时未知数量的文本。下面的例子中,s超出作用域时会自动调用一个特殊的名为drop的函数来释放内存。所以String类型是一个实现了Drop trait(trait,接口)的类型。

fn main() {let mut s = String::from("Hello");s.push_str(", world!");println!("{}", s);		
}	// s 会自动调用一个drop函数

看到这里你可能依然一头雾水(这家伙在说什么呢.jpg),这些概念和C/C++以及其他语言难道做不到吗?超出作用域释放内存难道不是理所当然的吗?既然如此我还为什么要学Rust?Rust究竟好在哪?所谓的内存安全就这?

别急,这个Drop方法看似人畜无害,但是它会导致一个非常严重的bug。

3 变量和数据(值)的交互方式

3.1 移动 Move

首先看下面这个例子,创建了两个简单的整数变量,由于它们的大小是确定的,所以两个变量都将被压入栈中,值发生了复制。像整数这样完全存放在栈上的数据实现了Copy trait。

let x = 5;
let y = x;		// value copied here

但是下面这个例子不同,s1在内存中的索引信息存储在栈中,s1所对应的内容需要被存放在堆中(出于值的长度可变的需要)。栈中包含一个指向字符串存储位置的指针,一个字符串实际长度,一个从操作系统中获得的内存的总字节数。

let s1 = String::from("hello");

在这里插入图片描述
如果接下来接着执行这一语句,那么栈中s1的信息会被复制一份,但是堆中字符串的值不会复制(有点像浅拷贝),s1的所有权将会直接被递交给s2,同时s1会直接失效,这时我们说值的所有权发生了移动(Move)。这样做的目的是避免两个字符串离开作用域时调用两次drop函数,从而导致严重的Double Free错误。

let s2 = s1;			// value moved here
println!("{}", s1);		// 编译直接报错

请添加图片描述

3.2 克隆 Clone

对于上面的s1s2的例子,如果想同时拷贝栈和堆中的信息,可以使用clone()方法。这样的操作明显是比较浪费资源的。

在这里插入图片描述

fn main() {let s1 = String::from("hello");let s2 = s1.clone();println!("{} {}", s1, s2);
}

3.3 复制 Copy

总之,如果一个变量存在Copy trait,那么旧变量在“移动”后依然可用;如果一个类型或者该类型的一部分实现了Drop triait(例如定义的元组的一部分是String的情况),那么Rust就不允许它再实现Copy trait了,编译时就会进行检查,在移动后旧变量就不再可用,除非使用了clone()方法。

4 函数与所有权

Rust中的变量总是遵循下面的规则:

  • 把一个变量赋值给其他变量就会发生移动(除非变量存在Copy trait);
  • 当变量超出其作用域后,存储在Heap上的数据就会被销毁(Drop trait),除非它的所有权已经被转移。

4.1 参数传递时的所有权转移

在Rust中,如果函数参数的类型是一个实现了Drop trait的类型(例如String类型),把值传递给函数中往往伴随着所有权的转移,也就是说旧变量对值的所有权会发生丢失,这里发生的事情和把变量赋值给另一个变量是类似的。看下面这个例子:

fn main() {let s1 = String::from("hello");take_ownership(s1);// println!("{}", s1);      // 编译报错let x = 1;makes_copy(x);println!("the x is {}", x);
}fn take_ownership(some_string: String) {println!("{}", some_string);
}fn makes_copy(some_integer: i32) {println!("{}", some_integer);
}

对于String这种类型的变量,直接将其作为函数参数时,传入参数时helloString的所有权会从s1转换到函数内部的some_string,程序运行到take_ownership函数之外时会自动调用Drop trait,字符串的值的内存会被释放。但是对于实现了Copy trait的类型,例如i32,参数传递时会发生copy,而不是move,这样在函数调用后x变量依然是可用的。

4.2 函数返回时的所有权转移

这个比较好理解,看下面一个例子:

fn main() {let s1 = gives_ownership();let s2 = String::from("hello");let s3 = takes_and_gives_back(s2);
}fn gives_ownership() -> String {let s = String::from("hello");s
}fn takes_and_gives_back(a_string: String) -> String {a_string
}

对于gives_ownership函数,在函数内部创建了一个新的String,函数返回时不会将其销毁,而是把它的所有权交给主函数的s1;而takes_and_gives_back函数获取到s2到的所有权,s2之后会失效,返回时将String的所有权交还给主函数的s3

5 引用和借用

但有些时候,我们只想获得变量的值,而不想它的所有权发生转移(甚至丢失),这时候就可以使用引用(Reference)。

fn main() {let s1 = String::from("hello");let lenth = calculate_length(&s1);println!("The length of '{}' is {}.", s1, lenth);
}fn calculate_length(s: &String) -> usize {s.len()
}

在上面的例子中,calculate_length函数使用了String的引用作为参数,函数计算返回字符串长度后s1仍然是可用的。引用相当于一个指针,它可以获取到变量对应的值,但是不拥有它,所以当其离开作用域时也无法销毁它。像这样,把引用作为函数参数这个行为称为借用(Borrow)
在这里插入图片描述

在Rust中,引用和变量类似,也分为可变的引用和不可变的引用,创建的引用默认同样是不可变的。下面是一个使用可变引用的例子。

fn main() {let mut s1 = String::from("hello");let lenth = calculate_length(&mut s1);println!("The length of '{}' is {}.", s1, lenth);
}fn calculate_length(s: &mut String) -> usize {s.push_str(", world!");s.len()
}

需要注意引用的特殊限制:在特定的作用域内,一个变量只能同时拥有一个可变的引用;并且不能同时存在可变的引用和不可变的引用。一个变量可以拥有多个不可变的引用。Rust从编译层面解决了数据竞争的问题。

let mut s = String::from("hello");let s1 = &mut s;
let s2 = &mut s;	// 非法
let mut s = String::from("hello");
{let s1 = &mut s;
}
let s2 = &mut s;	// 合法

这样的做法还带来了另一个好处,即永远不会存在“悬空引用”(Dangling Reference,一个引用或者指针指向一块内存,但是这一块内存可能已经被释放或者被其他人使用了)或者“野指针”。

总之,引用一定满足下面的规则

  • 引用一定有效;
  • 引用一定满足下列条件之一,不可能同时满足:
    • 存在一个可变引用;
    • 存在任意数量的不可变引用。

6 切片

切片(Slice)是指一段数据的引用。这里的一段数据可以是String类型,也可以是数组。字符串切片的写法如下所示,类型名在程序中是&str

let s = String::from("hello world");let hello = &s[0..5];	// 左闭右开,此时相当于 &s[..5]
let world = &s[6..11]	// 此时相当于 &s[6..]let whole = &s[..]		// 整个字符串的切片

在这里插入图片描述

需要注意,字符串切片的索引必须发生在有效的UTF-8字符边界内(就是不能把字符切“坏”了),否则程序就会报错退出。

为什么要使用切片?看下面这个例子:获取字符串中的各个单词,如果字符串中没有空格,则返回整个字符串。

fn main() {let s = String::from("hello");let word_index = first_word(&s);println!("{}", word_index);
}fn first_word(s: &String) -> usize {let bytes = s.as_bytes();   // 将String转换为字符数组for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return i;}}s.len()
}

上面这个程序虽然能完成一部分功能(获取第一个空格的位置),但是这个程序存在一个重要的结构性缺陷:变量word_index和Strings之间没有任何联系,即使s被释放,或者被修改,word_index也无法感知。

使用字符串切片重写上面的例子:

fn main() {let s = String::from("hello world");let word = first_word(&s);      // 把s作为不可变的引用发生借用,之后s都不可变// s.clear();       // s不可变println!("{}", word);
}fn first_word(s: &String) -> &str {let bytes = s.as_bytes();   // 将String转换为字符数组for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return &s[..i];}}&s[..]
}

请添加图片描述

字符串子面值也是切片。利用这一特点,我们可以将函数的参数类型改为字符串切片&str,使得函数可以直接接收字符串子面值作为参数,这样函数就可以同时接收String和字符串切片两种类型的变量作为参数了

fn main() {let word = first_word("hello world");      println!("{}", word);
}fn first_word(s: &str) -> &str {let bytes = s.as_bytes();   // 将String转换为字符数组for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return &s[..i];}}&s[..]
}

其他数组类型也存在切片,例如使用下面的方法创建一个i32类型的切片,程序中用&[i32]表示该类型。

let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice = &a[1..3];		// slice类型是&[i32]

  原创笔记,码字不易,欢迎点赞,收藏~ 如有谬误敬请在评论区不吝告知,感激不尽!博主将持续更新有关嵌入式开发、FPGA方面的学习笔记。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/918864.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/918864.shtml
英文地址,请注明出处:http://en.pswp.cn/news/918864.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis 知识点与应用场景

1. Redis 简介与核心特性Redis(Remote Dictionary Server)是一款开源的内存数据存储系统,支持多种数据结构,兼具高性能、持久化、分布式等特性,广泛用于缓存、数据库、消息中间件等场景。其核心特性包括:高…

日常反思总结

1.group by和order by的区别

易贝 (eBay (eBay) 关键字搜索 API 实战:从认证到商品列表获取全流程解析

在跨境电商开发领域,eBay 作为全球最大的在线交易平台之一,其开放 API 为开发者提供了丰富的商品数据获取能力。本文将聚焦 eBay 关键字搜索商品列表接口的实现,涵盖 OAuth2.0 认证、高级搜索参数配置、分页策略及完整代码实现,帮…

敏捷数据开发实践:基于 Amazon Q Developer + Remote MCP 构建本地与云端 Amazon Redshift 交互体系

敏捷数据开发实践:基于 Amazon Q Developer Remote MCP 构建本地与云端 Amazon Redshift 交互体系 新用户可获得高达 200 美元的服务抵扣金 亚马逊云科技新用户可以免费使用亚马逊云科技免费套餐(Amazon Free Tier)。注册即可获得 100 美元的…

【SpringBoot】11 概念理解 - 深入理解 Java 和 Spring 中的容器、组件、类、对象与 Bean

文章目录引言1. 基本概念解析1.1 类(Class)1.2 对象(Object)1.3 组件(Component)1.4 Bean 实例(Bean Instance)1.5 容器(Container)2. 运行时 vs. 非运行时的…

【学习嵌入式day-25-线程】

exec函数族exec函数族利用进程空间执行另一份代码#include "../head.h"int main(void) {char *parg[5] {"./hello","how","are","you",NULL,};printf("execl-up\n");//execl("./hello", "./hello…

Rust 中 Box 的深度解析:作用、原理与最佳实践

Rust 中 Box 的深度解析:作用、原理与最佳实践 Box 是 Rust 中最基础且最重要的智能指针类型,它在 Rust 的内存管理和所有权系统中扮演着核心角色。以下是关于 Box 的全面解析: Box 的核心作用 #mermaid-svg-m6liFZlmqOHRfIZB {font-family:&…

【测试用例】

需求背景部分金融/政企等行业客户,企业内部安全要求较高,且因为某些原因未接入 sso 登录,会要求 MG 提供较为复杂的密码规则甚至提供强更机制;且每个客户的安全要求不一样目前 MG 线上密码规则: 8 位以上,包…

Klipper-probe模块

配置信息[probe] pin: !PD4 x_offset: 0 y_offset: 0 z_offset: -0.20 #the distance between nozzle and level switch speed: 10 samples: 2 #probe one point three times get an average samples_result: average sample_retract_dist: 5 samples_tolerance: 0.05 # …

Excel多级数据结构导入导出工具

Excel多级数据结构导入导出工具 这是一个功能强大的Excel导入导出工具库,专门用于处理复杂的多级嵌套数据结构。通过自定义注解配置,可以轻松实现Java对象与Excel文件之间的双向转换。 核心功能特性 1. 多级数据结构支持 嵌套对象处理: 支持任意层级的对…

基于UniApp的新大陆物联网平台温湿度检测系统开发方案

新大陆物联网平台对接要点 认证方式: 使用AccessToken进行API认证 Token存储在本地缓存中 数据格式: 温度数据单位:摄氏度(C) 湿度数据单位:百分比(%) 时间格式:ISO 8601或时间戳 设备状态: online:…

Git、JSON、MQTT

GIT简介:Git是什么?Git是目前世界上最先进的分布式版本控制系统作用:版本控制(版本的备份--->版本的回溯和前进)多人协作优势:SVN(集中式)劣势:过度依赖服务器和网络,容灾性差Git…

yolo目标检测技术之yolov11项目实战(三)

yolo目标检测技术之yolov11项目实战(三) 文章目录yolo目标检测技术之yolov11项目实战(三)一、 基于 YOLO11 的火焰与烟雾检测系统(实战代码)项目目标环境搭建创建虚拟环境安装依赖1.1 数据集准备1. 下载地址…

CF思维小训练(二)

清晰的缤纷的都可以 脏兮兮的甜的也都有转机 不想太小心 错过第一百零一场美丽 CF思维小训练(二) 书接上回CF思维小训练-CSDN博客 虽然代码很短,都是每一道题的背后都思维满满; 目录CF思维小训练(二)Arbo…

分布式锁:从理论到实战的深度指南

1. 分布式锁是啥?为什么它比单机锁更“硬核”?分布式锁,听起来高大上,其实核心问题很简单:在多个机器、进程或服务同时抢夺资源时,怎么保证不打架? 想象一下,你在双十一抢购限量款球…

基于UniApp的智能在线客服系统前端设计与实现

了解更多,搜索“程序员老狼”一、引言在当今数字化时代,客户服务已成为企业竞争力的重要组成部分。本文将详细介绍一款基于UniApp框架开发的跨平台智能客服系统前端实现方案,该系统不仅具备传统客服功能,还融入了现代即时通讯和人…

react与vue的对比,来实现标签内部类似v-for循环,v-if等功能

前言:在vue中我们提供了很多标签方法,比如用的比较多的v-for循环内容,v-if/v-show等判断,可以直接写在标签中,大大提高了我们的开发效率,那么在react中有没有类似的方法呢?我们这里来说一说。re…

PCB工艺-四层板制作流程(简单了解下)

一)流程:四层板的内层芯板,是由一张双面覆铜板PP*2铜箔*2覆铜板蚀刻好线路,就是我们的芯板了PP全名叫半固化片,主体是玻璃纤维布环氧树脂,是绝缘介质铜箔片,是单独一张铜箔,很薄&…

无人机三维路径规划

文章目录 1、引言 2、背景知识 3、核心算法 4、挑战与优化 5、初始效果 6、需要改进地方 7、水平方向优化路线 8、垂直方向优化路线 9、与经过路线相交的网格都绘制出来 1、引言 介绍三维路径规划的定义和重要性:在无人机、机器人导航、虚拟现实等领域的应用。 概述文章目标和…

Spring-解决项目依赖异常问题

一.检查项目的Maven路径是否正确在确保新项目中的依赖在自己的电脑中已经存在的情况下:可以检查项目的Maven路径是否正确在拿到一个新项目时,要检查这个项目的Maven路径是自己电脑上设置好的Maven路径吗?如果不是,项目依赖会出问题…