面向C++程序员的Rust教程(二)

先序文章请看:
面向C++程序员的Rust教程(一)

所有权与移动语义

要说Rust语言跟其他语言最大的区别,那笔者觉得非数这个所有权和移动语义莫属。

深浅复制

对于绝大多数语言来说,变量/对象之间的赋值通常都是复制语义。例如C++中:

void Demo() {Obj o1; // 对象1auto o2 = o1; // 复制语义,o2是o1的复制
}

只不过深复制还是浅复制需要进一步研究。C++中由于完全支持栈上部署自定义类型以及自定义的拷贝构造/赋值函数,程序员需要自行判断内部指针/引用关系,决定使用深复制或是浅复制。

一些语言是把「结构体」和「类」做区分,结构体仅用于做数据聚合,部署在栈上,而类则添加更多OO特性,部署在堆上(然后栈上给一个指针)。比如说Swift和C#就是如此。那么这种情况下栈上部署的类型,复制就为深复制,而堆上部署的类型复制就为浅复制。

还有一些语言索性不允许自定义类型在栈上部署(比如java、OC),那么这种情况下也就是限定了默认的复制均为浅复制,例如下面OC的例子:

void Demo() {Object *o1 = [[Object alloc] init];Object *o2 = o1; // 由于栈上只有指针,因此复制一定是浅复制
}

总之,统一的原则都是「栈上做深复制」,所以如果栈上是完整数据那么就是深复制,如果栈上只有指针/引用,那么就是浅复制。

rust移动语义

但Rust非常特殊,他根本不在这里纠结深复制还是浅复制的问题,而Rust默认为「移动语义」而非「复制语义」。当然,这只针对自定义类型来说,对于整数、浮点数这些它仍然是简单的值复制。我们来看一个例子:

fn main() {let mut a = 5;let b = a;a = 10;println!("{},{}", a, b); // 10,5
}

这种基本类型看上去无可厚非,但如果换成自定义类型结果可能大大超出预期:

struct Test {a: i32,b: i32
}fn main() {let mut t = Test{a: 1, b: 2};let t2 = t;t.a = 8; // ERROR
}

我们会发现,在尝试更改t.a的时候,编译报错了,报错信息如下:
移动语义报错

意思就是说,我们尝试去操作了一个已经被移动的变量t。换句话说,let t2 = t;这一行语句,隐含了「移动语义」。

由于Test是自定义类型,因此它会被部署在堆上,main函数栈中的t则是它的一个指针。之后我们把t赋值给t2的时候,相当于把「对象的所有权」「转移」给了t2,也就是说,赋值之后,t2成为了指向原始对象的指针,同时,t不可以再被使用

如果和C++做对比,大致上可以等价于下面的代码:

struct Test {int32_t a;int32_t b;
};int main() {Test *t = new Test(1, 2); // 自定义类型部署在堆上auto t2 = t; // 所有权转交t = nullptr; // 原始指针废弃return 0;
}

当然,事实上还是有一些区别的,比如说C++中,这里的t仍然可以复用,而rust中它就是完全不可再用的状态(除非定义重影,这个语法后续章节详细讨论)。

对于一些C++程序员来说,可能会把rust的这种「移动语义」与C++中的「移动语义」混淆,甚至可能认为「rust的赋值相当于自带std::move」,但其实并非如此,一来std::move是为了触发移动构造/赋值函数,从而触发浅复制,而rust的赋值中根本没有任何复制的语义,而是「所有权转交」;二来std::move并不能使原本的指针失效,但rust中的赋值是可以的,这一点希望读者一定要区分。

如果一定要与C++的语法做对比,rust的行为倒是更加符合std::unique_ptr的行为,unique_ptr不可复制只可移动,移动时转交对象所有权,原本的指针清空:

void Demo() {auto t = std::make_unique<Test>(1, 2); // 对象部署在堆中,栈上用指针指向auto t2 = std::move(t); // 赋值时做所有权转交// 这时t已经被清空了,不再指向原始对象t->a = 8; // ERROR
}

当然,rust的机制更先进一些,一个是它不用套壳,不需要理解所谓智能指针和std::move的概念,二来如果对已经释放的指针做操作,报错是在编译阶段,而如果是C++的unique_ptr(例如上面例程),报错则是在运行阶段,而且报的是解空指针错误。

Rust的一个世界观

相信很多读者会对rust的所有权转交这一机制非常不适应,甚至非常不解。那么这里我们就不得不讨论一下Rust的一个重点世界观,就是手Rust希望「尽可能在编译阶段发现和避免更多的潜在问题」。也就是说,Rust它不希望程序问题留给运行期,而是在编译期,就把可能会出现的一些错误都发现(或者干脆避免掉)。

因此,每当我们发现一些Rust奇怪的限制或机制的时候,都应当思考这样限制所希望避免的问题。下面用C++来举几个例子,读者可以体会一下传统的复制语义在这里会出现的问题:

示例1:

void f1(Obj obj) {// 使用obj做一些事情
}void Demo() {Obj pre_obj;f1(pre_obj); // 构造pre_obj只是为了传给f1// 后面也不会使用pre_obj
}

上面这种场景下,我们在Demo中构造pre_obj,只是为了传给f1使用,但如果f1使用了复制语义,那么就会平白多一次无意义的复制,如果Obj类型比较大,或者是拷贝构造比较复杂,那么这里的效率就会很低。

示例2:

void f1(Obj &&obj) { // 右值引用类型,希望强制获取所有权// 使用obj做一些事情
}void Demo() {Obj pre_obj;f1(std::move(pre_obj));// 照理说后边不可以再使用pre_obj,但这是软约束pre_obj.set_xxx(yyy); // OK不会报错
}

上面这个例子中,尽管我们用了右值引用,「企图」让外界传参时把obj的「所有权」交给函数内部,但在C++中这种移动语义是一种软约束,如果不小心在外界操作了pre_obj仍然是合法的。

示例3:

class Test {public:Test(int a): pa_(new int(a)) {}~Test() {delete pa_;}private:int *pa_;
};void Demo() {Test t1(1);Test t2 = t1;
} // 析构时出现重复delete问题

上面这个例子中,我们实现Test类,虽然遵从了构造时new析构时delete的原则,但却没有考虑到复制语义的问题,由于t2t1的一个浅复制,因此在函数结束时,t1t2都会对同一片堆空间进行delete

Rust的世界观中,为了避免这些乱七八糟的内存分配和释放问题,干脆直接在语义上杜绝了这种影响。首先,自定义类型只能部署在堆空间,就不存在浅复制的问题;其次,栈上的变量同时只能有一个持有对象,也不会存在重复释放的问题;最后,由于栈变量和堆对象是1对1的关系,那么他们的生命周期可以做强绑定,也就是说当栈变量释放时,所持有的堆空间就进行析构。

struct Point {x: f32,y: f32
}fn Demo() {let p1 = Point{x: 0.5, y: 1.2}; // p1持有对象let p2 = p1; // p2持有对象,p1不再可用
} // p2生命周期结束,对象同时释放

上例中,由于Point对象只能被一个变量持有,当p1交接给p2后,p1就跟这个对象没关系了。后面当p2结束时,自然也不会有其他变量持有这个对象,当然可以放心把它释放。

所以看出来了吗?Rust为什么不需要垃圾回收机制,也不需要什么引用计数器,就能做到避免内存泄漏或者重复释放?答案很简单,因为它根本不允许多重引用。

借用

上一节我们讲解了Rust中自定义类型的所有权问题,相信大家应该能够意识到,这种语言特性在很多场景下是很不方便的。

举例来说,在一个程序流程中,我需要先检验一下输入的参数是否合法,然后再对数据做一些处理。比如说:

struct Data {dt1: i32,dt2: u32
}fn check_args(dt: Data)->bool {// 判断dt1和dt2要非0dt.dt1 != 0 && dt.dt2 != 0
}fn main() {let mut dt = Data{dt1: 1, dt2: 3};// 先检查数据if !check_args(dt) {// 一些处理} else {// 后续逻辑dt.dt1 += 5; // ERROR}
}

如果按照上面这种写法,在检查完参数以后,这个dt的所有权就转交了,然后在check_args函数结束后就被释放了,这显然是不符合预期的。同时编译也会报错。

但仔细分析这种场景,这里有一个非常重要的特点,就是说check_args中,dt相当于只读,不会对其做任何更改。那么也就是说,check_args的调用不会改变dt的值,而且因为只是做检查,因此原本的dt后续还需要使用的。

那么这种场景下并不应当「转交所有权」,而是应当「借用」一下dt。所谓「借用」,形象来说就相当于借别人东西,你只是在借用的过程中可以使用而已,但东西还是人家的,用完了要还回去,并且,你使用的过程中不能损坏。

C++解决这个问题的办法是常引用做参数,这样一来不用复制,二来内部不可改变。

// 用常引用解决问题
bool check_args(const Data &dt) {return dt.dt1 != 0 && dt.dt2 != 0;
}int main() {Data dt {1, 3};if (!check_args(dt)) {// ... } else {// ...dt.dt1 = 5;}return 0;
}

无独有偶,Rust中解决这个问题的办法也是利用引用,而且是不可变引用。

fn check_args(dt: &Data)->bool {// 判断dt1和dt2要非0dt.dt1 != 0 && dt.dt2 != 0
}fn main() {let mut dt = Data{dt1: 1, dt2: 3};// 先检查数据if !check_args(&dt) { // 注意传参时要显式取引用// 一些处理} else {// 后续逻辑dt.dt1 += 5; // OK}
}

前面章节我们已经初步介绍过引用,他有点像C++中引用和指针的结合体,所以这里用作引用传参时也一定要注意,要显式用&表示取引用,这一点与C++不同。

【未完,更新中……】

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

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

相关文章

微信开发工具——进行网页授权

微信开发工具——进行网页授权 微信公众平台设置 1.在首页创建好自己的订阅号 网站&#xff1a;https://mp.weixin.qq.com/ 点击立即注册,在选择订阅号&#xff08;个人创建使用&#xff09; 之后按流程填写后&#xff0c;点击设置与开发-------->基本配置&#xff0c;这…

JAVA八股--redis

JAVA八股--redis 如何保证Redis和数据库数据一致性redisson实现的分布式锁的主从一致性Redis脑裂现象及解决方案介绍I/O多路复用模型undo log 和 redo log&#xff08;没掌握MyISAM 和 InnoDB 有什么区别&#xff1f; 如何保证Redis和数据库数据一致性 关于异步通知中消息队列…

Kubernetes(k8s):精通 Pod 操作的关键命令

Kubernetes&#xff08;k8s&#xff09;&#xff1a;精通 Pod 操作的关键命令 1、查看 Pod 列表2、 查看 Pod 的详细信息3、创建 Pod4、删除 Pod5、获取 Pod 日志6、进入 Pod 执行命令7、暂停和启动 Pod8、改变 Pod 副本数量9、查看当前部署中使用的镜像版本10、滚动更新 Pod11…

基于Java+SpringBoot+Mybaties+layui+Vue+elememt 实习管理系统 的设计与实现

一.项目介绍 前台功能&#xff1a;用户进入系统可以实现首页&#xff0c;系统公告&#xff0c;个人中心&#xff0c;后台管理等功能进行操作 后台由管理员&#xff0c;实习单位&#xff0c;教师和学生&#xff0c;主要功能包括首页&#xff0c;个人中心&#xff0c;班级管理&am…

【C++学习】哈希的应用—位图与布隆过滤器

目录 1.位图1.1位图的概念1.2位图的实现3.位图的应用 2.布隆过滤器2.1 布隆过滤器提出2.2布隆过滤器概念2.3如何选择哈希函数个数和布隆过滤器长度2.4布隆过滤器的实现2.4.1布隆过滤器插入操作2.4.2布隆过滤器查找操作2.4.3 布隆过滤器删除 2.5 布隆过滤器优点2.6布隆过滤器缺陷…

Linux:make/makefile的使用

一、什么是makefile/make 会不会写makefile&#xff0c;从一个侧面说明了一个人是否具备完成大型工程的能力 一个工程中的源文件不计数&#xff0c;其按类型、功能、模块分别放在若干个目录中&#xff0c;makefile定义了一系列的 规则来指定&#xff0c;哪些文件需要先编译&am…

设置Chrome打开链接在新标签页显示

Chrome版本 版本 123.0.6312.106&#xff08;正式版本&#xff09; &#xff08;64 位&#xff09; 下面这两个页面都有设置按钮&#xff1a; https://www.google.com/?pli1或者https://www.google.com/?hlzh-CN 要先退出账号&#xff0c;要不然看不到右下角的 “设置” 。…

TCP/IP协议、HTTP协议和FTP协议等网络协议包简介

文章目录 一、常见的网络协议二、TCP/IP协议1、TCP/IP协议模型被划分为四个层次2、TCP/IP五层模型3、TCP/IP七层模型 三、FTP网络协议四、Http网络协议1、Http网络协议简介2、Http网络协议的内容3、HTTP请求协议包组成4、HTTP响应协议包组成 一、常见的网络协议 常见的网络协议…

vivado 配置存储器器件编程2

为双 QSPI (x8) 器件创建配置存储器文件 您可使用 write_cfgmem Tcl 命令来为双 QSPI (x8) 器件生成 .mcs 镜像。此命令会将配置数据自动拆分为 2 个独立 的 .mcs 文件。 注释 &#xff1a; 为 SPIx8 生成 .mcs 时指定的大小即为这 2 个四通道闪存器件的总大小。…

QA测试开发工程师面试题满分问答5: 内存溢出和内存泄漏问题

概念阐述 内存溢出&#xff08;Memory Overflow&#xff09;和内存泄漏&#xff08;Memory Leak&#xff09;是与计算机程序中的内存管理相关的问题&#xff0c;它们描述了不同的情况。 内存溢出是指程序在申请内存时&#xff0c;要求的内存超出了系统所能提供的可用内存资源…

SSM框架学习——Eclipse创建Spring MVC maven项目

Spring MVC项目创建 什么是Spring MVC Spring MVC是Spring内置的&#xff0c;实现了Web MVC设计模式的框架。 它解决了Web开发过程中很多的问题&#xff0c;例如参数接收、表单验证等。另外它采用松散耦合可插拔组件等结构&#xff0c;具有相对较高的灵活性和扩展性。 Spri…

vue创建项目下载动态路由v-for mounted websocket :style :class store使用说明

在Vue中创建一个项目&#xff0c;并整合动态路由、v-for、mounted生命周期钩子、WebSocket、:style、:class以及Vuex的store&#xff0c;涉及到多个Vue核心特性的使用。下面我将简要说明如何逐步整合这些特性。 1. 创建Vue项目 使用Vue CLI创建项目&#xff1a; 2. 配置动态路…

C++ 类(初篇)

类的引入 C语言中&#xff0c;结构体中只能定义变量&#xff0c;在C中&#xff0c;结构体内不仅可以定义变量&#xff0c;也可以定义函数。 而为了区分C和C我们将结构体重新命名成class去定义 类的定义 标准格式&#xff1a; class className {// 类体&#xff1a;由成员函…

【计算机网络】epoll

IO多路转接 - epoll 一、I/O多路转接之 epoll1. epoll 接口&#xff08;1&#xff09;epoll_create()&#xff08;2&#xff09;epoll_wait()&#xff08;3&#xff09;epoll_ctl() 2. epoll 原理3. epoll 的优点4. epoll 的使用5. epoll 的工作模式&#xff08;1&#xff09;水…

实验四 Java图形界面与事件处理(头歌)

实验四 Java图形界面与事件处理(头歌) 制作不易&#xff01;点个关注&#xff01;给大家带来更多的价值&#xff01; 目录 实验四 Java图形界面与事件处理(头歌) 制作不易&#xff01;点个关注&#xff01;给大家带来更多的价值&#xff01;代码如下&#xff1a; 代码如下&…

case语句

Oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 CASE 语句的执行方式与 IF...THEN...ELSIF 语句的执行方式类似&#xff0c;但是它是通过一个表达式的值来决定执行哪个分支 CASE 选择器表达式 WHEN 条件 1 THEN 语句序列 …

Linux: linux常见操作指令

目录 01.ls 指令 02. pwd命令 03. cd 指令 04. touch指令 05.mkdir指令&#xff08;重要&#xff09; 06.rmdir指令 && rm 指令&#xff08;重要&#xff09; 07.man指令&#xff08;重要&#xff09; 07.cp指令&#xff08;重要&#xff09; 08.mv指令&#…

H.264 压缩与编解码原理

H.264 压缩与编解码原理 H.264 压缩与编解码原理H.264 简介视频编码的总体思路H.264 压缩技术帧内预测压缩什么是空间冗余&#xff1f;具体预测方法 帧间预测压缩什么是时间冗余&#xff1f;具体预测方法&#xff1a;运动估计 概念&#xff1a;Group of Pictures&#xff08;GO…

java-网络编程socket-聊天室-先导

这边我会简单介绍一下聊天室的组成部分,和思路的引导 涉及知识点 java 中异常处理机制 和 io流和网络编程socket 简单回顾异常机制 Java中的异常机制是一种用于处理程序运行期间出现的错误或异常情况的机制。这种机制允许程序员定义在特定情况下可能发生的错误&#xff0c;并…

mysql慢sql排查与分析

当MySQL遇到慢查询&#xff08;慢SQL&#xff09;时&#xff0c;我们可以通过以下步骤进行排查和优化&#xff1a; 标题开启慢查询日志&#xff1a; 确保MySQL的慢查询日志已经开启。通过查看slow_query_log和slow_query_log_file变量来确认。 如果没有开启&#xff0c;可以…