rust编程-rust所有权理解(chapter 4.1)

news2024/9/20 23:26:09

目录

1. 什么是所有权

1.1 堆与栈

1.2 变量作用域

1.3 String类型

1.4 内存和分配

1.5 变量和数据交互的方式

1.5 所有权和函数

1.6 返回值和作用域


所有权 是Rust独有的特性,该设计开创了编程语言中的先河。所有权使得Rust能保证内存的安全,且不需要类似Java、Golang类语言的垃圾回收(GC)机制,理解Rust的所有权对于Rust编程至关重要。该章节着重用介绍与所有权相关的借用(borrowing)、引用(Reference)、切片(Slices),以及Rust在内存中的数据布局。

1. 什么是所有权

所有权是Rust用于管理程序内存的一组规则。程序在运行时,必须管理它们使用计算机内存的方式。

一些编程语言具有(内存)垃圾收集功能:在程序运行时,不断查找并收集不再被使用的内存。

另外一些编程语言中,程序员必须显式地分配和释放内存(C/C++)。

Rust独创了第三种方法:通过一个拥有一组规则的所有权系统,来管理内存,编译器负责在编译时检查的该组规则。如果违反所有权系统中定义的任何规则,程序将无法编译。所有权的特性不会影响程序运行时的性能,这一点非常重要,对比GC机制具有巨大优势。

所有权,对许多程序员来说是一个新概念,所以需要一些时间来适应。 使用Rust所有权的规则的学习曲线非常高, 好消息是,对于Rust和所有权系统的规则,Rust编程经验越丰富,就越容易自然而然地开发出安全而高效的代码。

在本章节中,通过Rust中strings数据结构的一些实例来学习所有权。

1.1 堆与栈

许多编程语言不要求对栈和堆的理解。但在像Rust这样的系统编程语言中,一个值是在堆栈上还是在堆上都影响该Rust的行为,以及编程人员必做的一些选择。所有权的部分将在本章后面的部分中与堆栈和堆的关系进行描述,下面进行一个简短的说明:

栈和堆都是代码可以在运行时使用的内存,但是它们以不同的方式构造的。

  • 栈按顺序获取的内存并存储值,并以相反的顺序删除,这被称为后进先出。存储在堆栈上的所有数据必须具有已知的固定大小。编译时大小未知或大小可能发生变化的数据,必须存储在堆中。

  • 堆的组织性不如栈,当将数据放到堆上时,需要一定量的空间。内存分配器在堆中找到一个足够大的空点,将其标记为正在使用,并返回一个指针,这是该位置的地址。这个过程称为堆上的分配,有时简写为分配(将值压到栈上并不认为是分配)。因为指向堆的指针是已知的固定大小,所以可以将指针存储在栈上,但是当需要实际数据时,必须进行寻址(依据指针从堆上获取)。

压栈分配要比在堆上分配要快的多,因为不需要分配器来寻找存储新数据的位置,这个位置总是在栈的顶部。相比之下,在堆上分配空间需要更多的工作,因为分配器必须首先找到足够大的空间来保存数据,然后记录分配状态,为下一次分配做准备。

访问堆中的数据要比访问堆栈中的数据慢,因为必须对指针进行寻址。在堆上分配大量空间也十分耗时。

当进行函数调用时,传递给函数的值(包括指向堆上数据的指针)和函数的局部变量被推入栈中。当函数返回时,这些值从栈中弹出。

Rust所有权要解决的问题包括:跟踪代码的哪些部分使用了堆上的哪些数据;最小化堆上重复数据数量;清理堆上未使用的数据以免耗尽空间。一旦理解了所有权,就不再需要经常考虑栈和堆,但了解所有权的主要目的是管理堆数据,可以帮助解释它为什么以这种方式工作。

首先,看下所有权系统的主要规则:

1)Rust中每一个值都有一个变量关联,这个变量称作owner

2)一个值某一时刻只能有一个owner

3) 当owner的作用域结束,对应值的生命周期也结束,会即时被销毁

1.2 变量作用域

作用域(Scope)是Rust程序中变量(等)的作用范围,离开作用域就不再有效。

{                      // s is not valid here, it’s not yet declared
    let s = "hello";   // s is valid from this point forward

    // do stuff with s
}   // this scope is now over, and s is no longer valid

如上s变量有两个关键点:s进入作用域时({之后),s有效;离开作用域(}之后),s无效。

1.3 String类型

为了示例Rust所有权,需要一个略复杂点的rust数据结构,该数据结构不能是rust基本类型以至于在栈上自动进行存储和拷贝,需要使用到堆来存放该数据结构的变量。这里使用Rust标准库中提供的String类型,第8章节将详细介绍String类型。使用其他复杂类型,道理也是一样的。

String类型值,在堆上进行分配,且是可变的,大小在编译时不可知。如下:

fn main()
{
    let mut s = String::from("hello");

    s.push_str(", world!"); // push_str() appends a literal to a String

    println!("{}", s); // This will print `hello, world!`
}

String::from中的双冒号,限定了使用String类型命名空间中的from方法。

1.4 内存和分配

字符串常量是不可变的,这是因为在编译期我们即知道其内容,因此被直接硬编码到了最终的可执行文件中。对于大小不确定的变量,无法在二进制中这样做,在运行时,其大小是可以改变的。

对于String类型,为了支持可变的、可增长的字符串,我们需要在堆上分配一定数量的内存量(在编译时未知)来保存内容。这意味着

1)必须在运行时从内存分配器请求内存

2)当String类型变量声明期结束,能够归还其内存给分配器

第(1)部分是由编程完成的:当调用String::from时,它的具体实现请求了所需的内存。这在编程语言中非常普遍。

第(2)部分则是不尽相同的:在具有垃圾收集器(GC)的语言中,GC会跟踪并清理不再使用的内存,编程时不需要考虑归还和释放问题;在大多数没有GC的语言中,编程人员负责识别何时不再使用内存并调用代码显式地释放内存,像请求内存时一致。正确无遗漏地申请和释放一直是一个困难的编程问题。如果忘记了释放,就会造成内存泄漏问题。如果释放太早,就会产生无效变量(指针)。如果我们释放两次或多次,将是个bug。必须对一次分配函数和释放函数进行配对。

Rust采用了截然不同的方法:一旦拥有值所有权的变量离开作用域,内存就被自动释放。 如下:

{
        let s = String::from("hello"); // s is valid from this point forward

        // do stuff with s
}                                  // this scope is now over, and s is no
                                       // longer valid

上述代码,存在一个很自然的内存释放(归还给内存分配器)的点,即当s离开其作用域时。Rust在这个点,插入了一个特殊函数的调用,该函数被称为drop函数,Rust在右大括号的位置,自动调用drop释放内存。

1.5 变量和数据交互的方式

不同的变量可以与相同内存的值存在不同形式的交互。主要有move和clone两种。

  • 移动(move)

如下实例:

  let x = 5;
  let y = x;

由于x,y是简单的整型变量,具有已知、固定的大小,因此y的赋值实际上是直接拷贝生成了一个新的值(=5)并绑定到了y变量。x,y的值都被压入了栈上。

再如:

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

如上s1和s2则完全不同,s1到s2的赋值,并不会拷贝生成一个新的字符串值。如下图来展示String类型在标准库中的一些实现细节:

 String类型内部由三部分组成:一个指向字符串内容内存的指针(ptr),一个整型的长度变量(len),一个整型的容量变量(capacity)。该三部分数据是直接在栈上存放的,但ptr则指向了堆上的内存。

具体释义如下:

(1)len标识当前字符串使用的内存字节数量,这里是5;

(2)capacity标识当前ptr指向内存的总大小,这里是可以大于len的,未来后续len扩展考虑。capacity实际也是内存分配器返回的内存大小。

当使用s1赋值给s2时,String内部的数据被拷贝给s2,即:拷贝了ptr指针,拷贝了len长度,拷贝了capacity容量。但ptr指向的内存区并没有进行拷贝,这段内存位于堆上。 拷贝完成,结果如下图:

 假如,Rust同时也拷贝ptr指向的堆内存区,则该赋值操作将是非常耗时和性能低下的,最终结果如下图:

 前面所述:当变量离开作用域时,Rust自动调用drop函数为该变量清理堆内存。但是图4-2显示了两个数据指针都指向同一个位置。这里有一个问题:当s2和s1超出作用域时,它们都会尝试释放相同的内存。这里就产生了著名的双重释放(double free)问题,这也是前面提到的内存安全错误之一。两次释放会导致内存损坏,可能会导致安全漏洞。

为了保证内存安全,在let s2 = s1的赋值完成后,Rust认为s1已经是无效的。因此Rust在s1离开作用域后,不再需要释放操作(drop)。可以知道,后续对s1的访问都会是非法的。这是区别于大多数变成语言的。 如下:

 fn main()
 {
    let s1 = String::from("hello");
    let s2 = s1;

    println!("{}, world!", s1);
}

编译该代码,将产生一个非法引用的编译错误:

# cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 | 
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error

在其它编程语言中,有浅拷贝和深拷贝的说法。String中如上的赋值只拷贝了ptr指针,看似属于一种“浅拷贝”。但Rust实际上将赋值者(s1)进行了失效操作,因此这里被称作移动(move)。 因此,可以说:“s1移动到了s2”。实质上发生的的操作如下:

 赋值完成后,只有s2字符串是有效的。此外,在设计上,Rust遵循一个原则:永远不会自动进行“深拷贝”数据的赋值操作。 也因此,任何运行时的自动拷贝,都是高效不损害性能的操作。

  • 克隆(clone)

如果确实需要“深拷贝”的赋值操作,而非仅仅拷贝栈上内容,Rust可以调用一个通用的clone方法来实现。如下:

    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1 = {}, s2 = {}", s1, s2);

上述代码的结果,恰恰如4-3图所示。调用clone方法,将执行一些会影响运行时性能的操作。

  • 纯栈数据的拷贝(copy)

某些类型的赋值,虽然没有调用clone,但两个变量都不会失效。如下:

    let x = 5;
    let y = x;

    println!("x = {}, y = {}", x, y);

原因是:诸如整型等编译时大小可知的简单类型是完全存储在栈上的,因此可以快速复制其值。在赋值创建变量y后,没有必要使x失效。换句话说,这里的深拷贝和浅拷贝是没有区别的。

Rust有一个Copy trait 的特殊注解语法,可以把该注解放在像整数一样存储在栈上的类型上(第10章节将详细讨论trait注解的内容)。

如果一个类型实现了Copy trait ,那么一个变量在赋值给另一个同类型变量后,将仍然有效。

如果一个类型或其任何部分实现了Drop trait,则Rust不允许用Copy trait来注解一个类型。这将产生编译错误,后续章节将介绍如何给自定义类型添加Copy trait注解。

那么都有哪些类型实现了Copy trait呢?

可以查看给定类型的文档来以确定。作为一般规则,任何简单的标量值以及其组成的组类型,都可以实现Copy trait。任何需要分配堆内存或某种形式的资源的类型,都不能实现Copy trait。

下面是实现Copy的一些类型:

  • 所有的整数类型,例如u32

  • 布尔bool类型,值为true和false

  • 所有浮点类型,如f64

  • 字符char类型

  • 部分元组类型,如果它们只包含也实现Copy trait的类型,例如,(i32, i32)实现Copy。但(i32, String)不实现Copy trait。

1.5 所有权和函数

给函数传递参数的语义,与变量赋值的语义类似。也遵循上述变量和数据的交互方式。给函数传递一个变量,也会移动(move)或拷贝(copy)变量。如下示例:

fn main() {
    let s = String::from("hello");  // s作用域开始

    takes_ownership(s);             // s的值移动到了函数takes_ownership中
                                    // ... s不再有效

    let x = 5;                      // x作用域开始

    makes_copy(x);                  // x copy到了函数makes_copy中
                                    // 由于x是i32简单类型,因此这里是栈Copy
                                    // x随后使用时合法的

} // Here, x goes out of scope, then s. But because s's value was moved, 
  // nothing special happens.

fn takes_ownership(some_string: String) { // some_string进入作用域
    println!("{}", some_string);
} // some_string 离开作用域,drop被自动调用,其内存被释放

fn makes_copy(some_integer: i32) { // some_integer进入作用域
    println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.

在takes_ownership(s);调用后使用s将会产生编译错误,这种静态编译时检查可以防止编程出错。

1.6 返回值和作用域

函数返回值也会转移所有权。 如下示例:

fn main() {
    let s1 = gives_ownership();         // gives_ownership移动了其返回值的
                                        // 所有权到s1中

    let s2 = String::from("hello");     // s2进入作用域

    let s3 = takes_and_gives_back(s2);  // s2所有权被移动到了takes_and_gives_back
                                        // 函数中。
                                        // takes_and_gives_back函数随后又移动其
                                        // 返回值所有权到s3中
} //  s1,s3离开作用域,自动调用drop释放内存。
  // s2的所有权已经移走,因此这里无操作

fn gives_ownership() -> String {             // gives_ownership将移动其返回值
                                             // 的所有权到调用者函数中

    let some_string = String::from("yours"); // some_string进入作用域

    some_string                              // some_string返回,并移动所有权到
                                             // 函数外的调用者
}

// 该函数拿到一个string的所有权,并返回并移动走起所有权
fn takes_and_gives_back(a_string: String) -> String { // a_string进入作用域

    a_string  // a_string返回,所有权移动到调用该函数的函数中
}

变量的所有权遵循相同的模式:变量赋值,并转移值的所有权到另外一个变量中。当包含堆上数据的变量离开作用域时,除非数据的所有权已移动到另一个变量,否则该值将被drop清理。

虽然调用函数获得所有权,函数返回同时返回所有权,这种模式是可以工作的。但从编程上,非常繁琐不友好。尤其,元组类型当函数参数比较多时,多个变量所有权的转移和返回,Rust可以使用元组这样操作,但在编程上非常啰嗦,如下:

fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of a String

    (s, length)
}

那如何能让一个函数使用一个值,且不必要获得所有权呢?

Rust有一个使用值而不转移所有权的特性,称为引用,下一章节介绍。

关于作者:

犇叔,浙江大学计算机科学与技术专业,研究生毕业,而立有余。先后在华为、阿里巴巴和字节跳动,从事技术研发工作,资深研发专家。主要研究领域包括虚拟化、分布式技术和存储系统(包括CPU与计算、GPU异构计算、分布式块存储、分布式数据库等领域)、高性能RDMA网络协议和数据中心应用、Linux内核等方向。

专业方向爱好:数学、科学技术应用

关注犇叔,期望为您带来更多科研领域的知识和产业应用。

内容坚持原创,坚持干货有料。坚持长期创作,关注犇叔不迷路

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

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

相关文章

游程编码(Run Length Coding)

游程编码游程编码基本介绍示例1示例2游程编码适用的场景游程编码 游程编码(Run Length Coding,简称RLC)又称游程编码、行程长度编码、变动长度编码 等,是一种统计编码。主要技术是检测重复的比特或字符序列,并用它们的…

亚马逊云科技re:Invent 2022 Ruba Borno主题演讲

2022亚马逊云科技re:Invent全球大会精彩内容应接不暇,亚马逊云科技全球渠道与联盟副总裁Ruba Borno在2022亚马逊云科技re:Invent大会的全球合作伙伴峰会上,为合作伙伴带来一系列全新的合作伙伴创新服务。 云上发展持续加速的当下,上云好比一场…

使用Fiddler对手机App抓包

目录 一、查看 Fiddler 的 ip 地址 二、设置 Fiddler 允许远程连接 三、进行手机端 App 的抓包 3.1.准备工作 3.2.手机设置 3.3.安装根证书 四、可能会遇到的问题 一、查看 Fiddler 的 ip 地址 有两种方法都可以查询到。 第一种方法: 打开 Fiddler&#xff…

[附源码]计算机毕业设计springboot在线招聘网站

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

一分钟带你了解音视频开发进阶(先收藏了)

FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发学习路线 随着基础设施的完善(光纤入户、wifi覆盖、5G普及)的影响,将短视频、直播、视频会议、在线教育、在线医疗瞬间推到了顶峰,人们对音视频的需求和要求也越来越强烈 音视…

实现java项目idea打包发布至服务器(完整版)

问题:如何快速部署本地代码到服务器? 今天介绍的是使用idea的一款插件(Alibaba Cloud Toolkit)实现,首先需要在自己的服务器上安装运行环境,包括:jdk、maven、mysql等,这些操作就不详细说了,可…

Inductive Representation Learning on Large Graphs 论文/GraphSAGE学习笔记

1 动机 1.1 过去的方法 现存的方法大多是transductive的,也就是说,在训练图的时候需要将整个图都作为输入,为图上全部节点生成嵌入,每个节点在训练的过程中都是可知的。举个例子,上一次我学习了GCN模型,它…

考研数据结构大题整合_组三(LZH组)

考研数据结构大题整合 目录考研数据结构大题整合三、LZH组LZH 组一LZH 组二LZH 组三LZH 组四LZH 组五LZH 组七三、LZH组 LZH 组一 给出如图所示的无向图G的邻接矩阵和邻接表两种存储结构. (2)解答下面的问题(6分) (…

二、进程管理(五)死锁

目录 5.1死锁的定义和产生条件 5.2死锁的处理策略 5.2.1死锁预防 5.2.2死锁避免 5.2.3死锁检测和解除 5.1死锁的定义和产生条件 在并发环境下,各进程因竞争资源而造成的一种互相等待对方手里的资源,导致各进程都阻塞,都无法向前推进的现…

【GlobalMapper精品教程】034:创建漫游动画并制作漫游视频的方法

本实例讲解在globalmapper中根据路径创建漫游动画,并制作漫游视频的方法。 文章目录 一、绘制漫游路径二、创建3D虚拟漫游三、播放虚拟漫游四、保存虚拟漫游实验数据可以是点云数据、DEM、三维模型等,本文加载数字表面模型DSM进行演示。 一、绘制漫游路径 同创建矢量线状数据…

海带软件分享——日常办公学习软件分享(收藏)

>>>深度学习Tricks&#xff0c;第一时间送达<<< &#x1f680; 写在前面 &#x1f431;‍&#x1f3cd; 本期开始&#xff0c;小海带会定期推荐一些日常办公学习软件及趣味网址&#xff0c;供大家交流参考 ~ 小伙伴们记得一键三连喔&#xff01;&#x1f6…

几款好用到爆炸的在线画图工具

前言 实际工作中&#xff0c;我们经常会编写文档以及制作图表。尤其是对一名优秀的攻城狮来说&#xff0c;经常会用各种各样的软件来制作流程、思维导图、思维笔记等。一个良好的思维导图能系统概括项目工程的整体结构和开发的系统框架。要想制作一个完美的流程图、思维导图离不…

菜狗杯Misc一层一层一层地剥开我的♥wp

目录一、原题二、解题步骤对jpg图片的处理对文件名是一个心形的数据文件的处理base100解码这题完全是看着官方wp复现的&#xff0c;感觉涉及的步骤比较多但每一步本身不难&#xff0c;多记录一遍加深印象。 一、原题 原题给的是一个叫myheart.zip的文件&#xff0c;但尝试解压…

高通开发系列 - ALSA声卡驱动中音频通路kcontrol控件

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 目录 高通开发系列 - ALSA声卡驱动中音频通路kcontrol控件问题背景高通音频通路如何建立widget和routemixer类控件名组合mixer类控件名拼接…

CRM客户关系管理系统(含源码+论文+答辩PPT等)

该项目采用技术&#xff1a;JSP Servlet MySQLjdbccssjs等相关技术&#xff0c;项目含有源码、文档、配套开发软件、软件安装教程、项目发布教程等 项目功能介绍&#xff1a; 系统管理&#xff1a;用户登录退出、个人资料修改 客户管理&#xff1a;客户信息管理、客户来源、联系…

Softmax回归——动手学深度学习笔记

Softmax回归&#xff0c;虽然它的名称叫做回归&#xff0c;其实它是一个分类问题。 回归VS分类 回归估计一个连续值 如&#xff1a;回归估计下个月的房价 分类预测一个离散类别 如&#xff1a; &#xff08;1&#xff09;MNIST&#xff1a;手写数字识别&#xff08;10类&…

初识springmvc

狂神的servlet回顾就不在这里写了。可以翻之前的笔记。 原生开发&#xff1a; 创建webapp的maven项目。 也就是四个文件 &#xff08;不用思考里面的代码&#xff0c;直接CV先走一遍流程&#xff09; HelloController&#xff1a; package com.Li.controller;import org.sp…

SecureCRT之Xmodem操作步骤

以锐捷S3760为例&#xff1a; 故障现象&#xff1a;s3760无法加载&#xff0c;需要重刷RGOS。 一、使用控制线连接s3760&#xff0c;开机加载引导&#xff0c;按Ctrl_B进入“BOOT MENU”页面&#xff1a; 选择【0】进入XModem操作界面&#xff1a; 说明&#xff1a; 0--更新…

android 开发——疑难杂症ANR简单介绍与解析

一、ANR介绍 ANR-application not response&#xff0c;应用无响应&#xff0c;应用开发者一般是关注自己的APP进程有没有出现&#xff0c;系统开发者会关注当前系统运行起来后整体上所有的APP进程有没有出现ANR&#xff0c;从这句话可以知道&#xff0c;只有应用进程的主线程…

多元正态分布-参数估计-书后习题回顾总结

重点考察知识点汇总 协方差矩阵 协方差矩阵为对称矩阵协方差矩阵的对角线为各分量的方差&#xff0c;其余位置(i,j)(i,j)(i,j)表示的是分量iii和分量jjj的协方差 多元正态分布的线性组合仍然服从多元正态分布 设X∼Np(μ,Σ)X\sim N_{p}(\mu,Σ)X∼Np​(μ,Σ)&#xff0c;B…