深入浅出 - Rust 所有权与内存管理机制

news2024/9/30 5:31:44

一、从变量说起

fn main() {
    // 基本数据类型
    let a = 5;
    let b = a;
    // 指针
    let ptr_a = &a;
    let ptr_b = &b;
    println!("a value = {}", a);
    println!("b value = {}", b);
    println!("ptr_a value = {:p}", ptr_a);
    println!("ptr_b value = {:p}", ptr_b);
}

// 输出
a value = 5
b value = 5
ptr_a value = 0x8bdecff6b0
ptr_b value = 0x8bdecff6b4

在这个例子中,我们定义了变量 a 的值为 5,并且将变量 a 的值赋值给变量 b,接着定义 ptr_a 、ptr_b 分别取变量 a、b 的地址,然后调用 prinltn 函数输出各个变量的值。

思考一个问题:如果不使用变量,直接在输出函数中把数值 5 作为参数、字符串作为参数是否也可以呢?

可以。但是这样的话只能做到静态输出,而不能动态输出。如果需要用户输入了某个数据,需要做加减运算,或者做字符拼接,那么就需要引入变量实现动态处理。

那变量又是如何跟某个数据绑定起来的呢?

程序运行时,数据保存都在内存中,如果我们直接通过内存地址 0x8bdecff6b0 获取到里面的数值 5,那就非常的不方便。因此我们给这个内存地址起了一个别名,称为变量 a,访问变量 a 就是访问内存地址里面的值,从而可以获取数值 5,这样就方便多了。有时候就是想知道数据的内存地址,那么我们可以通过取地址符 & 来获取变量的地址,&a 为取变量 a 的地址 0x8bdecff6b0,而这个地址赋值给了变量 ptr_a ,访问 ptr_a 就是访问变量 a 的地址 0x8bdecff6b0,而不是访问内存地址里面的值 5要想访问值,只能通过解地址符 * 来访问内存地址里面的值,访问 *ptr_a 就是访问变量 a 地址 0x8bdecff6b0 里面的值,即数值 5。

而事实上,这样的解释根本不严谨,因此我们结合栈和堆的概念再深入解析。

二、栈和堆

栈和堆的核心目标就是为程序在运行时提供可供使用的内存空间。

——Rust Course

1、栈 Stack

栈中的所有数据都必须占用已知且固定大小的内存空间,遵循先进后出原则,把变量 a 增加到栈里面称为进栈,把变量 ptr_b 从栈定移除称为出栈在执行函数 main 时,变量依次进栈,程序结束运行时,变量依次出栈,对应的内存释放,从而实现内存回收。

2、堆 Heap

与栈相反,堆中的数据大小空间都是未知的,甚至会发生变化。要想在堆上存储数据,必须向操作系统申请一块能容纳该数据的内存空间,由操作系统返回一个内存地址,该过程也称为在堆上分配内存最后该内存地址进栈,跟某个变量进行绑定

3、栈和堆的性能对比 

写入性能快。入栈时操作系统无需分配新的空间,只需要将新数据放入栈顶即可。慢。必须先向操作系统申请内存空间后才能存入数据。
读取性能快。栈数据直接存储在 CPU 高速缓存中,不需要访问内存。慢。堆数据只能存储在内存中,必须先访问栈再通过栈上的指针来访问内存。

可见,在栈上处理数据是最高效的。

4、拷贝

栈上的数据复制称为浅拷贝(Cpoy),堆上的数据复制称为深拷贝(Clone)。

三、所有权与栈堆

 所有的程序都必须和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成了重中之重,也是所有编程语言设计的难点之一。在计算机语言不断演变过程中,出现了三种流派:

  • 垃圾回收机制(GC),在程序运行时不断寻找不再使用的内存,典型代表:Java、Go
  • 手动管理内存的分配和释放, 在程序中,通过函数调用的方式来申请和释放内存,典型代表:C++
  • 通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查

其中 Rust 选择了第三种,最妙的是,这种检查只发生在编译期,因此对于程序运行期,不会有任何性能上的损失。

——Rust Course

理解了变量与栈堆原理之后,我们正式步入今天的主题——Rust 所有权与内存管理机制,来看这样一段代码:

fn main() {
    // 复合数据类型
    let x = String::from("hello");
    let y = String::from("world");
    let ptr_x = &x;
    let ptr_y = &y;
    println!("x value = {}", x);
    println!("y value = {}", y);
    println!("ptr_x value = {:p}", ptr_x);
    println!("ptr_y value = {:p}", ptr_y);
}

// 输出
x value = hello
y value = world
ptr_x value = 0x3cddcffa78
ptr_y value = 0x3cddcffa90

根据堆栈知识,从堆上分配了内存空间来存储 hello 字符串,并返回内存地址 0x9bdecff001 ,由变量 x 保存该地址,另一个字符 world 同理。到这里程序能正常运行,我们稍微改动一下代码,把 x 的数据赋值给变量 y:

6 |     let x = String::from("hello");
  |         - move occurs because `x` has type `String`, which does not implement the `Copy` trait
7 |     let y = x;
  |             - value moved here
8 |     println!("x value = {}", x);
  |                              ^ value borrowed here after move

在将变量 x 的值赋给变量 y 后,输出变量 x 的值发生报错,提示在第 7 行代码发生了所有权转移,因为 String 类型的变量 x 没有实现 Copy 方法。我们不管这个 Copy 方法,先解释一下为什么发生了所有权转移。

Rust Course 介绍了所有权原则:

谨记以下所有权原则:

  1. Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
  2. 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

——Rust Course

因此,在栈堆中应该是这样的,

1、首先从堆上分配了内存空间来存储 hello 字符串,并返回内存地址 0x9bdecff001 ,由变量 x 保存该地址;

2、执行 let y = x; 后,变量 y 也绑定了变量 x 中的数据内存地址 0x9bdecff001

3、变量 x 不再指向数据内存地址 0x9bdecff001 ,把所有权转移给变量 y 。因此再次访问变量 x 时,因为数据内存地址无效,所以编译报错。

思考一下:为什么变量 x 要把所有权转给变量 y 呢?变量 y 在堆上再复制(Clone)一份 hello 不行吗?

前面提到,在栈上处理数据(Copy)是比堆上高效的,因此可以认为,再复制(Clone)一份 hello 不够高效而且浪费内存;而在回收变量内存时,是通过出栈的方式逐一释放,如果变量 x 跟 y 都指向同一个内存地址,则会导致一个内存地址释放两次,这就导致内存污染。

四、Rust 所有权与内存管理机制

ok,现在让我们来梳理一下以上内容:

1、Rust 通过所有权来管理内存,一个值只能被一个变量绑定拥有

// hello 与 x 绑定
let x = String::from("hello");
// x 将 hello 所有权转移给 y ,hello 与 与绑定
let y = x;

2、Rust 变量在执行时按顺序进栈,程式结束时,依次出栈来释放内存

// 1、x 进栈 4、x 出栈,释放 x
let x = String::from("hello");
// 2、y 进栈 3、y 出栈,释放 y
let y = x;

3、Rust 基本类型在栈上做 Copy ,不会发生所有权转移

// 1 与 x 绑定
let x = 1;
// 栈上 Copy 一份 1 与 y 绑定,此处没有所有权转移
let y = x;

4、Rust 复合类型默认不能在栈上做 Copy,会发生所有权转移

// hello 与 x 绑定
let x = String::from("hello");
// x 将 hello 所有权转移给 y ,hello 与 与绑定
let y = x;

5、Rust 复合类型可以通过在堆上做 Clone 后进栈,不会发生所有权转移

// hello 与 x 绑定
let x = String::from("hello");
// x 将 hello 在堆上 Clone 一份,返回另一份 hello 的内存地址 y 与绑定,此处没有发生所有权转移
let y = x.clone();

6、函数传值与返回同样遵守所有权与栈堆原则

fn main() {
    // hello 与 x 绑定
    let x = String::from("hello");
    // 调用函数时,变量、参数同样也是进栈操作,结束调用时依照出栈次序释放内存
    // 因此,x 是复合类型,不能在栈上做复制,只能将 hello 所有权转移到函数参数 s 
    // 函数返回又将 hello 所有权转移给 y
    let y = move_ownership(x);
    // 后续不能再调用 x

    // 基本类型在调用函数时,变量、参数进栈时都是 Copy ,因此不会发生所有权转移
    let a = 1;
    let b = plus_one(a);
    // 后续可以再调用 a
}

fn move_ownership(s: String) -> String {
    s
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

五、借用与引用

ok,在上面我们已经梳理完 Rust 的所有权与内存管理机制,它贯穿于整个 Rust 语法当中,但是我们总将某个变量的所有权移来移去,非常麻烦,有没有一种机制可以借用某个变量的所有权而不是直接转移所有权呢?没错,这就是 Rust 借用与引用机制。

fn main() {
    // 从堆上分配内存存储 hello 并返回地址指针给 x
    // 实际上 x 为一个结构体,结构体字段为 ptr 指针、len 长度 、cap 容量
    // 变量绑定后,x 的属性为 ptr 指向 hello ,len 为 5 ,cap 为 5
    let x = String::from("hello");
    // 变量 y 借用了 变量 x 的所有属性
    let y = &x;
    // 输出变量 y 的内存地址,y 借用 x 的 len 字段属性
    println!("{:p} {:?}", &y, y.len());
    // 输出变量 x 的内存地址,x 自身持有的 len 字段属性
    println!("{:p} {:?}", &x, x.len());
}

// 输出
0x8a94f9f4f0 5
0x8a94f9f4d8 5

访问变量 y ,实际上是访问变量 x 的地址,从而可以借用到 x 的字段;而 *y 相当于读取变量 x 里面的值,即 ptr 指向的 hello 。

程序运行结束时,按出栈顺序释放内存,回收变量 y 、回收 hello、回收变量 x。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

未来笔试重点(1)

一起成为更优秀的人 1.数组笔试重点考察 1.1整型数组与sizeof 1.2字符数组与sizeof 1.3sizeof与字符串 1.4strlen与字符串 2.指针笔试重点考察 2.1指针与sizeof 2.2指针与strlen sizeof与整型数组 int a[] { 1,2,3,4 };printf("%d\n", sizeof(a));printf(…

NMS与Soft NMS算法解析以及numpy实现

1. NMS算法 1.1 什么是NMS算法 NMS全称为Non Maximum Suppression,中文意思是非极大值抑制,字面意思就是不是极大值的元素被抑制掉,其实就是筛选出局部最大值得到最优解。NMS算法被广泛运用于目标检测算法处理网络输出的边界框。 1.2 为什…

浅谈Linux内核页面换入换出

【推荐阅读】 浅谈linux 内核网络 sk_buff 之克隆与复制 深入linux内核架构--进程&线程 了解Docker 依赖的linux内核技术 怎么在Windows下使用Makefile文件 浅析linux内核网络协议栈--linux bridge 0x00内存页面分类与换入换出规则 内存页面分为用户页面和内核页面。…

GDT践行(第一期):起床---运动--阅读

这里记录每周GDT践行记录.【2022】年第【51】周的第【6】天 封面图 第三部分:起床---运动--阅读 ❝ 小贴士: 在学校,出租房,宾馆,工位很多因素导致 学习区,运动区 卧室 都是同一个地方 环境导致你彻底彻底糊…

[附源码]Python计算机毕业设计Django课程在线测评系统

项目运行 环境配置: Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术: django python Vue 等等组成,B/S模式 pychram管理等等。 环境需要 1.运行环境:最好是python3.7.7,…

JVM部分知识点

目录 JVM主要组成部分及其作用? JAVA程序运行机制详情 JVM运行时的数据区 堆和栈的区别? Java垃圾回收机制 Java中有哪些引用类型? 如何判断对象是否可以被回收? JVM中的永久代会发生垃圾回收吗? JVM有哪些垃圾…

我国航空煤油行业发展趋势:燃油附加费复收 或将缓解企业经营压力

根据观研报告网发布的《中国航空煤油市场现状深度研究与投资前景分析报告(2022-2029年)》显示,航空煤油是石油产品之一,别名无臭煤油,主要由不同馏分的烃类化合物组成。航空煤油密度适宜,热值高&#xff0c…

【C语言】浮点型的存储方式

目录 一、浮点型和整型存储方式一样吗? 二、浮点型的存储规则 2.1 S,M,E求法 2.2 如何存放S,M,E 2.2.1 IEEE 754规定 2.2.2 特别的规定 2.2.3 验证 2.3 取出规则 2.3…

改造冰蝎马,实现免杀之default_aes php

本专栏是笔者的网络安全学习笔记,一面分享,同时作为笔记 文章目录 文章目录文章目录前文链接前言效果目标密码验证功能免杀后话前文链接 WAMP/DVWA/sqli-labs 搭建burpsuite工具抓包及Intruder暴力破解的使用目录扫描,请求重发,漏…

[附源码]Python计算机毕业设计黑格伯爵国际英语贵族学校官网Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置: Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术: django python Vue 等等组成,B/S模式 pychram管理等…

5G无线技术基础自学系列 | 单站点验证概述

素材来源:《5G无线网络规划与优化》 一边学习一边整理内容,并与大家分享,侵权即删,谢谢支持! 附上汇总贴:5G无线技术基础自学系列 | 汇总_COCOgsta的博客-CSDN博客 单站点验证是指在基站硬件安装调试完成…

RNA-seq 详细教程:可视化(12)

学习内容 了解如何为可视化准备数据了解如果利用可视化来探索分析结果火山图可视化热图可视化可视化结果 当我们处理大量数据时,以图形方式显示该信息以获得更多信息,可能很有用。在本课中,我们将让您开始使用探索差异基因表达数据时常用的一…

【数电实验】移位寄存器与计数器

实验四 移位寄存器与计数器 一 实验目的 1 掌握任意进制计数器的构成方法; 2 熟悉双向移位寄存器的使用方法。 二 实验内容 1 任意进制计数器的构成方法: 用中规模集成计数器74HC161和与非门74LS00,构成十进制计数器。要求分别使用同步预…

精华推荐 | 【深入浅出RocketMQ原理及实战】「性能原理挖掘系列」透彻剖析贯穿RocketMQ的事务性消息的底层原理并在分析其实际开发场景

什么是事务消息 事务消息(Transactional Message)是指应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。RocketMQ的事务消息提供类似 X/Open XA 的分布事务功能,通过事务消息能达到分布式事务的最终一致。 事务消息所对应的场景 在一些对…

docker学习笔记(五)单个服务镜像部署

引言 当前微服务项目已经大面积普及,对于新需求迭代上线有许多疑惑的部分,比如线上的某些功能不能重启,在这种情况下我们需要部署和启动项目就不能搞大范围重启或干脆重新制作镜像,这种方式都是不可取的,这时候就需要…

重学webpack系列(二) -- webpack解决的问题与实现模块化的具体实践

只是根据几个想法,我们便创造出了webpack打包工具,它能够根据我们在前端项目中遇到的疑难杂症对症下药,那么这一章我们就一起来探讨一下我们项目落地所遇到的种种问题。 前端实践中的问题 Jsx / Tsx编译问题Less / Scss编译问题TypeScript编…

【Pintos】实现自定义 UserProg 系统调用 | 添加 syscall-nr 系统调用号 | 编写新的参数调用宏

💭 写在前面:本文讲解的内容不属于 Pintos 的 Project 项目,而是关于 userprog 如何添加系统调用的,学习如何额外实现一些功能到系统调用中以供用户使用。因为涉及到 src/example 下的Makefile 的修改、lib 目录下 syscall-nr 系统…

门诊排队叫号系统,有序叫号就诊,适用医院医院、门诊部、诊所等

排队叫号系统,是将互联网信息技术与门诊预约、签到、提醒、叫号、接诊等环节相结合,实现门诊流程式便捷叫号服务。 为助力门诊营造一个良好有序的就诊环境,打造科学合理的就诊流程,今天给大家推荐一款一款便捷排队叫号系统&#x…

Linux基本权限(2)

Linux基本权限(2) 📟作者主页:慢热的陕西人 🌴专栏链接:Linux 📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言 本博客主要讲解了目录权限,和目录&#…

2022年底,我手里一共负责了30套系统

2022年真是不平凡的一年,往常熙熙攘攘的办公室人越来越少,真是像曹操说的兄弟相继凋零,好似风中落叶啊。 结果人少了,手里的系统一个没少,慢慢年底了,我汇总了一下,手里的系统达到了30来个。 搞…