面向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.hqwc.cn/news/586460.html

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

相关文章

算法-小记

Integer&#xff0c;Long&#xff0c;BigInteger字符转化 在 Java 中&#xff1a; 如果字符串超过 333333 位&#xff0c;不能转化为 Integer 如果字符串超过 656565 位&#xff0c;不能转化为 Long 如果字符串超过 500000001位&#xff0c;不能转化为 BigInteger 牛顿迭代…

EXCEL通过VBA字典快速分类求和

EXCEL通过VBA字典快速分类求和 汇总截图 Option ExplicitOption Explicit Sub answer3() Dim wb As Workbook Dim sht As Worksheet Set wb ThisWorkbook Set sht wb.Worksheets(2) Dim ss1 As Integer Dim ss2 As Integer Dim i As Integer Dim j As Integer j 1Dim aa()…

安卓系统框架和Framework概述

目录 一、安卓系统框架1.1 系统应用层1.2 Java 框架层1.3 Native C/C系统库和 Android Runtime1.4 硬件抽象层(HAL)1.5 Linux Kernel 内核层 二、Framework2.1 关于Framework层:2.2 Android Framework的三大核心功能2.3 多语言编写的好处 一、安卓系统框架 图为 Google 官方提…

基于DCT(离散余弦变换)的图像水印算法,Matlab实现

博主简介&#xff1a; 专注、专一于Matlab图像处理学习、交流&#xff0c;matlab图像代码代做/项目合作可以联系&#xff08;QQ:3249726188&#xff09; 个人主页&#xff1a;Matlab_ImagePro-CSDN博客 原则&#xff1a;代码均由本人编写完成&#xff0c;非中介&#xff0c;提供…

红杉资本:2024年关于AI的4大预测

四大预测 预测一&#xff1a;Copilot 将逐渐向 AI Agent 转变。 2024 年&#xff0c;AI 将从辅助人类的 Copilot 转变为真正能替代一些人类工作的Agent。AI 将更像是一个同事&#xff0c;而不仅仅是一个工具&#xff0c;这点在软件工程、客服等行业已经初步显现。 预测二&…

AI音乐创作生成翻唱h5公众号流量主小程序开发

AI音乐创作生成翻唱h5公众号流量主小程序开发 五音不全? Ai音乐小程序系统让你秒变音乐家 分享赚钱 分享小程序给好友充值使用即可或分佣 Ai音乐素材 媒体配乐的绝佳利器 生成步骤 输入灵感/歌词 可手动输入&AI自动输入 ↓ 输入歌名 可手动输入&AI自动输入 ↓ 选择…

基于python爬虫与数据分析系统设计

**单片机设计介绍&#xff0c;基于python爬虫与数据分析系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于Python爬虫与数据分析系统的设计是一个结合了网络数据抓取、清洗、存储和数据分析的综合项目。这样的系统通常…

【智能算法】蜣螂优化算法(DBO)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献 1.背景 2022年&#xff0c;Xue等人受到自然界中蜣螂生存行为启发&#xff0c;提出了蜣螂优化算法&#xff08;Dung beetle optimizer, DBO&#xff09;。 2.算法原理 2.1算法思想 DBO模拟了自然界蜣螂种…

Maplesoft Maple 2024(数学科学计算)mac/win

Maplesoft Maple是一款强大的数学计算软件&#xff0c;提供了丰富的功能和工具&#xff0c;用于数学建模、符号计算、数据可视化等领域的数学分析和解决方案。 Mac版软件下载&#xff1a;Maplesoft Maple 2024 for mac激活版 WIn版软件下载&#xff1a;Maplesoft Maple 2024特别…

【SpringBoot整合系列】SpirngBoot整合EasyExcel

目录 背景需求发展 EasyExcel官网介绍优势常用注解 SpringBoot整合EaxyExcel1.引入依赖2.实体类定义实体类代码示例注解解释 3.自定义转换器转换器代码示例涉及的枚举类型 4.Excel工具类5.简单导出接口SQL 6.简单导入接口SQL 7.复杂的导出&#xff08;合并行、合并列&#xff0…

Linux利用Jenkins部署SpringBoot项目保姆级教程

在当今快速发展的软件开发领域&#xff0c;持续集成和持续部署&#xff08;CI/CD&#xff09;已经成为提升开发效率、缩短产品上市时间的关键实践。Linux系统以其稳定性和开源友好性&#xff0c;成为众多开发者和企业的首选平台。而Spring Boot&#xff0c;作为一个轻量级的Jav…

程序组织单元POU介绍(CODESYS)

CODESYS任务配置详细介绍请参考下面文章链接&#xff1a; 1、任务配置 CODESYS任务配置介绍-CSDN博客文章浏览阅读32次。看门狗是一种控制器硬件式的计时设备&#xff0c;看门狗的主要功能是监控程序执行时出现的异常或内部时钟发生的故障。当程序进入死循环时&#xff0c;看…