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

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

所有权与移动语义

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

深浅复制

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

void Demo() {
  Obj o1; // 对象1
  auto 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要非0
  dt.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要非0
  dt.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++不同。

【未完,更新中……】

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python标准数据类型--元组常用方法

在Python中&#xff0c;元组&#xff08;Tuple&#xff09;是一种不可变的有序集合&#xff0c;它与列表类似&#xff0c;但是元组中的元素不能被修改。元组通常用于存储不可变的数据集合&#xff0c;例如一组常量或者一组固定的值。本篇博客将介绍一些Python中元组的常用方法&…

软考高级架构师:人工智能芯片概念和例题

一、AI 讲解 人工智能芯片是专门设计来处理与人工智能&#xff08;AI&#xff09;相关的任务的集成电路。这些芯片针对AI应用的高计算需求进行了优化&#xff0c;以提升处理速度和效率&#xff0c;同时降低能耗。它们在AI领域&#xff0c;如深度学习、机器学习和数据分析中发挥…

python爬虫获取豆瓣前top250的标题(简单)

今天是简略的一篇&#xff0c;简单小实验 import requests from bs4 import BeautifulSoup# 模拟浏览器的构成&#xff08;请求头&#xff09; headers {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Ch…

10 款最佳 Mac 数据恢复软件,在为时已晚之前值得尝试

查看 10 年适用于 Mac 的 2024 款最佳免费数据恢复软件&#xff0c;可在大多数在线搜索中找到。精选列表将帮助您做出明智的决定并节省您的时间和精力。 10 款最佳 Mac 数据恢复软件 很明显&#xff0c;您此后将面临数据丢失事件。理所当然地&#xff0c;您期待一款可靠、与您…

中文大模型隐私保护哪家强?InternLM 与 Baichuan2 胜出!

引言&#xff1a;中文大模型隐私保护能力探索 本文研究了大语言模型&#xff08;LLMs&#xff09;对隐私和安全的影响&#xff0c;采用了三层渐进框架对语言系统的隐私进行评估。主要目标是全面评估LLMs对私人信息的敏感性&#xff0c;并检查其在识别、管理和保护敏感数据方面…

微信小程序短链接工具推荐

现在微信小程序大行其道&#xff0c;但工作中大部分人选择了短链接的方式来推广微信小程序&#xff0c;那么微信小程序短链接工具哪个好?今天就分享一篇从网上看到的关于《微信小程序短链接工具推荐》文&#xff0c;作者是souki&#xff0c;一起来看看吧! 一、缩链 1、生成方…

【智能算法】阿基米德优化算法(AOA)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献 1.背景 2020年&#xff0c;Hashim等人受到阿基米德定律启发&#xff0c;提出了阿基米德优化算法&#xff08;Archimedes Optimization Algorithm&#xff0c;AOA&#xff09;。 2.算法原理 2.1算法思想 …

python导入本地当前目录下的文件和父目录下的文件

今天我想要导入本地当前目录下的文件和父目录下的文件&#xff0c;网上查了很多教程&#xff0c;但还都是报错&#xff0c;最后几经尝试&#xff0c;终于成功解决了这一问题&#xff0c;在这里详细记录一下过程&#xff0c;同时也希望能够对大家有所帮助~~~:&#xff09; 导入…

Python人工智能应用---中文分词词频统计

目录 1.中文分词 2.循环分别处理列表 &#xff08;1&#xff09;分析 &#xff08;2&#xff09;代码解决 3.词袋模型的构建 &#xff08;1&#xff09;分析需求 &#xff08;2&#xff09;处理分析 1.先实现字符串的连接 2.字符串放到新的列表里面 4.提取高频词语 &…

vivado 向 SVF 目标添加器件

向 SVF 目标添加器件 创建 SVF 目标后 &#xff0c; 可向其中添加器件以定义 SVF JTAG 器件链配置。 SVF JTAG 器件链配置应与目标硬件链相匹配 &#xff0c; 以 确保能正确执行 SVF 文件。 使用 Vivado IDE 单击“ ”按钮以向 SVF 链添加赛灵思器件或非赛灵思器件。…

程序·人生

诡异之极 2024.03.12 清新环境&#xff08;股票代码002573&#xff09;委托卖出 20000股&#xff0c;委托价4.58&#xff0c;当日最高价4.57 2024.03.11 清新环境&#xff08;股票代码002573&#xff09;委托卖出 20000股&#xff0c;委托价4.55&#xff0c;当日最高价4.54 …

【Python系列】读取 Excel 第一列数据并赋值到指定列

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

windwos安全加固

一、账号管理 按用户类型分配账号 目的&#xff1a;根据系统要求&#xff0c;设定不同账户和组&#xff0c;管理员、数据库 sa、审计用户、来宾用户等 实施方法&#xff1a; 打开本地用户和计算机管理器 ​ 1.打开运行&#xff0c;输入lusrmgr.msc 2.根据用户要求将账户加入…

鸡尾酒排序解读

在数据处理的海洋中&#xff0c;排序算法无疑是引领我们探索数据规律的灯塔。今天&#xff0c;我们要探讨的是一种有趣且独特的排序算法——鸡尾酒排序。鸡尾酒排序&#xff0c;也被称为定向冒泡排序、双冒泡排序或搅拌排序&#xff0c;是冒泡排序的一种变体&#xff0c;它通过…

[计算机效率] 磁盘空间分析工具:FolderSize

3.15 磁盘空间分析工具&#xff1a;FolderSize FolderSize是一款磁盘管理工具&#xff0c;提供预约交互式磁盘空间分析体验&#xff0c;可以可视化观察磁盘空间使用情况。程序可以帮助用户快速查看并统计硬盘中的各个分区所占用的空间大小以及文件夹和文件的大小&#xff0c;并…

CCleaner如何还原系统 CCleaner怎么恢复注册表 ccleaner官方下载

CCleaner是一款电脑清理软件&#xff0c;其中的注册表清理功能是该软件很重要的功能。注册表作为电脑的重要文件&#xff0c;不可以随便清理&#xff0c;而CCleaner可以帮我们安全&#xff0c;快速地清除注册表。同时&#xff0c;CCleaner还有还原系统的功能。下面将为大家介绍…

Windows与Linux路径分隔符对比及Java代码实战

在Windows中&#xff0c;磁盘中用反斜杠&#xff08;又称为右斜杠&#xff09;\表示路径的分隔。在浏览器中用正斜杠/来表示路径的分隔。 Linux则是统一用/表示路径的分隔的。下面给出Linux中一些常见的路径表示&#xff1a; / 表示根目录./ 表示当前目录…/ 表示上级目录 …

如果夸克网盘开了会员下载还是很慢怎么办

最近发现一个windows系统下很奇怪的bug&#xff0c;通过夸克网盘客户端下载别人分享的夸克网盘内容的时候&#xff0c;莫名其妙的会在10M/s和0M/s之间来回徘徊&#xff0c;速度慢到不能忍。 在尝试了几种方法之后&#xff0c;发现一种神奇的方法居然可以解决这个奇怪的bug...所…

C++:初步接触C++(2)

hello&#xff0c;各位小伙伴&#xff0c;本篇文章跟大家一起学习C&#xff0c;感谢大家对我上一篇的支持&#xff0c;如有什么问题&#xff0c;还请多多指教 &#xff01; 文章目录 内联函数1.概念2.特性 auto关键字1.auto简介2.auto的使用细则3.auto不能推导的场景 基于范围…

「每日跟读」英语常用句型公式 第3篇

「每日跟读」英语常用句型公式 第3篇 1. I don’t know how to ____ 我不知道如何_____ I don’t know how to play soccer (我不知道怎么踢足球) I don’t know how to study&#xff08;我不知道如何学习&#xff09; I don’t know how to play chess (我不知道如何下国…