【C++】C++移动语义、左值右值、左值引用右值引用、移动构造函数、std::move、移动赋值操作符

二十五、C++移动语义、左值和右值、左值引用右值引用、移动构造函数、std::move、移动赋值操作符

本部分讨论一些更高级的C++特性:C++移动语义。但是讲移动语义之前我们得先了解什么左值右值、左值引用和右值引用。

1、C++的左值和右值、左值引用和右值引用
左值是有地址的值(located value),就是左值是有地址的。
左值大部分情况下是在等号的左边,右值在右边。

右值是一些,比如字面量、函数的一些返回结果等:

如果我通过返回int&,把上面的func函数的返回整成左值,会是什么情况?这里也引出什么是左值引用:

再看一个使用字符串的例子:

那有什么方法来检测某个值是左值还是右值吗?这里也引出什么是右值引用

所以此时我们写个重载函数:

小结
左值引用用一个&符号,右值引用则是用两个&&符号。
左值是带地址的数据,就是有存储支持的变量。右值是临时值,可以用右值引用&&来检测。
左值引用只能引用(接受)左值,除非加const,就也可以引用(接受)右值了。
右值引用只能应用(接受)右值。

左值、右值有什么用处呢?
尤其是在移动语义方面非常有用。移动语义我们后面还要讲。这里主要是想说分清左右值的目的在于优化。如果我们知道传入的是一个临时对象的话,我们就不需要担心它们是否活着、是否完整、是否拷贝,我们可以简单的偷用它的资源,给到特定的对象,或者在其他地方使用它们,因为我们知道它是暂时的,它不会存在很长时间,比如上面的ln+fn,就是暂时的,我们就可以从这个临时值中偷取资源,这对优化有很大帮助。能用右值就别用左值。所以有很多代码使用&&时,我们要知道这是右值引用。

2、C++移动语义:移动构造函数
其实移动语义底层逻辑也不复杂。但是你要非常清晰的说清什么是移动语义、用它做什么、实践中它是如何工作的等这些问题,就比较困难,因为牵扯到很多底层的、被包装了的、我们看不见的东西,所以很难说清。这里我尽量往细了说吧。

移动语义本质上就是允许我们移动对象。而这在C++11之前是不可能的,因为C++11才引入了右值引用,右值引用是移动语义必需的底层逻辑。通过上面的小标题,我们已经知道什么是右值,以及右值引用是什么。基本思想是,当我们写C++代码时,很多情况下,我们不需要或者不想把一个对象从一个地方复制到另一个地方,但又不得不复制,因为底层的设置就是要复制的。

举个例子:比如现在我要把一个对象当作参数,传递给一个函数。那么这个函数要获得那个对象的所有权,此时就只能copy这个对象。这里涉及到函数调用的相关知识点,不懂的可以参考【C++】数据类型、函数、头文件、断点调试、输入输出、条件与分支、VS项目设置_c++printf头文件-CSDN博客中的函数部分。

同理,当我们想从一个函数返回一个对象时也是一样的。我仍然需要在函数中创建那个对象,然后返回它,此时又是得复制数据了。不过现在有一种叫返回值优化的东西可以对这部分进行优化,所以这不再是个问题了。

就是说,我把一个对象当参数传入某个函数时,这个函数首先是需要得到这个对象的所有权或者其他,所以编译器或者操作系统首先是在当前堆栈帧中构造一个一次性对象,不管它在哪里,将它复制到我正在调用的函数中。然后才是开始执行这个函数的函数体。
当然,如果你的对象只是由一对整数或类似的东西组成,那么复制也没什么大不了。但如果你的对象需要堆分配内存之类的,就像下图的例子,它是一个字符串,需要复制它,就需要创建一个全新的堆分配。这就是一个沉重的复制过程。此时就是移动语义的用武之地(下下图)。如果我们只是移动对象而不是复制它,那么性能会更高。

写一个类作为例子,来演示这个沉重的复制过程:

从上图可见不管是代码K还是代码L,都调用了一次myString类的复制构造函数E。而E函数中还有堆分配new,是不是非常沉重。所以我们要用移动语义优化上面的代码。但是这里先不急着讲如何优化,下面我先讲透上面的代码:

(1)A和G处都是类的定义,定义一个类是不会引起内存分配的。定义一个类实例系统才会分配存储区,并把类实例名称引用到这块儿存储区。

(2)类实例对象的空间是在调用构造函数之前就分配好了的。调用构造函数是初始化这个实例的数据。

(3)实例化一个类会有一个this指针,类实例之间的区分就是通过this指针区分的。this指针就是一个地址;实例对象就是一些空间;构造函数、析构函数以及其它的函数,是一堆指令的集合。

(4)上图C处代码是myString类的无参构造函数。=default就表示,如果实例化myString类对象时没有参数,那就使用编译器生成的、默认的、构造函数。这也是在C++11标准中新引入的,编译器可以直接生成内联构造函数代码。
如果你的代码是myString s ;那实例对象s的m_data=nullptr,m_size=0。
此处再多说一句,=default只能用于特殊成员函数(构造函数、析构函数、拷贝/移动构造函数和拷贝/移动赋值操作符)。

(5)上图D处函数是myString类的有参构造函数。
(6)上图E处函数是myString类的复制构造函数。
(7)上图F处函数是myString类的析构函数。

(8)当我们实例化一个类实例时,系统是先分配一块不用初始化的内存空间,这块空间的大小是这个类的数据成员对齐后的大小。然后再执行这个类的构造函数。构造函数一般情况下就是初始化这块空间的,当然也会有其他功能,比如上面代码中还有new操作、mempy操作等。
当构造函数执行完毕后也就是这个类实例化完毕了。一个类实例化完毕,也就是说在内存中的一块存储区里存储了这个类的数据成员(而且一般是初始化完毕的)。

(9)当我们复制一个类实例时,如果这个类中有复制构造函数,那就类中的复制构造函数就自动重载了复制的操作。如果这个类中没有复制构造函数,那么就是底层的复制函数进行复制操作。

(10)不管是实例化一个类实例,还是复制一个类实例(不管是用复制构造函数复制的,还是用底层的复制函数复制的),都意味着创建新创建了一个类实例对象,当然也会同时生成一个指向这个对象的this指针。当这个类实例对象所在的作用域结束时,都会自动调用这个类的析构函数。

(11)有了上面的知识点铺垫,我们现在来理解代码K:
当操作系统开始执行上面的程序时,入口是main函数,所以代码K是程序执行的第一条指令。执行这条指令的过程是:
第一步是执行myString("liyuanyuan"),程序执行指针从K处跳到A处。也就是先去实例化一个没有名称的myString类实例。
系统先分配两个未初始化的资源:char* m_data(指针是4个字节)和uint32_t m_size(1个字节)。也就是B处的代码。
然后生成右值"liyuanyuan",当作构造函数D的参数,开始执行构造函数D,于是就打印了M、初始化了m_data和m_size:
uint32_t类型的m_size初始化的值是临时右值"liyuanyuan"的长度10;
char指针m_data初始化的值是:m_size为长度的、堆上的(因为是new嘛)、char数组的首地址。
并且同时把临时右值"liyuanyuan"也拷贝到堆上的char数组里面了。

这就是在main函数的栈上执行myString("liyuanyuan")的过程。执行完毕后的状态是:m_data、m_size是存储在main函数的栈内存上,这个.exe程序的进程堆上还有一个char数组,数组的首地址就是main函数线程中的m_data的值。
为了方便表述,这里生成的{m_data、m_size}这套数据暂时给个名字dataA吧。

第二步执行Entity e1(dataA),也是实例化一个名叫e1(这次是有名字的)的Entity类实例。于是执行指令跳到G处。
同理,系统先分配一块大小等于myString(4+1=5个字节)的、未初始化的栈空间(假设叫空间E)。
然后是在main函数的堆栈上复制一套dataA的数据,我们姑且将复制品称为dataB。为啥要复制dataA?因为为构造函数I准备参数啊。就是让dataB作为构造函数I的参数开始调用I,完成e1的实例化。但是这里的复制dataA操作就出现下面两种情况:

情况1:我在myString类中写了复制构造函数E,所以当系统复制dataA赋值给dataB时,会自动被E函数重载。那被E重载了,就打印了N、组成dataB的m_dataB的值就是10,组成dataB的m_sizeB就是E又在堆内存上new的另外一个字符串[liyuanyuan]的首地址。然后用dataB{m_dataB、m_sizeB}这套临时数据当作参数来实例化e1了。就是用{m_dataB、m_sizeB}初始化空间E(就是把{m_dataB、m_sizeB}拷贝到E中),并将名称e1引用到空间E上。实例对象e1就生成了。此时右值{m_dataB、m_sizeB}和堆上又new的数组就寿终正寝了,所以打印了P。至此代码K就执行完毕了。然后是执行下一条代码S,就是打印Q,最后执行到作用域结束T处,就释放第一步生成的、没名字的、数据是dataA{m_data、m_size}的那个myString类实例,于是又打印了个R。

情况2:如果我的myString类中没有写复制构造函数E呢?那系统是怎么赋值dataA的?那系统就用底层的复制函数把内存块dataA原原本本的拷贝到空间E里,将名称e1引用到空间E上。那此时的dataB的m_dataB就还是第一步时生成的地址,m_sizeB也是10。但是不管是调用E还是调用底层的复制函数,这都是一次生成一个新的myString类实例的操作。所以当系统把这个dataB当参数传入I并执行完毕后,释放dataB时,就调用了F析构函数,把第一步new的字符串数组也释放了。至此代码K算是执行完毕了,然后执行代码S,但是执行到T处时,第一步生成的myString类实例也该释放了,于是再次调用F,但是此时F就发现指针m_data指向的那块堆内存已经不见了(被m_dataB给释放了),于是没法delete了,就报崩溃了!!!

其实情况2就是浅拷贝,情况1是深拷贝。
情况2中dataB拷贝的是dataA中的m_data指针,这样就有两个指针指向堆上的同一个字符串数组,当dataB释放时就把堆上的字符串数组给释放了,那到作用域结束释放dataA时,m_data指向的内存就已经不存在了,就没法释放了,程序就崩溃了。也所以说上面的拷贝构造函数E是有必要写的,不然就崩溃了。

说明:
上面解释中看不懂类数据的内存分配的同学请参考:【C++】类、静态static、枚举、重载、多态、继承、重写、虚函数、纯需函数、虚析构函数_类 多态与重载-CSDN博客 中的类定义、类实例部分内容
看不懂复制、复制构造函数的请参考:【C++】理解C++中的复制、复制构造函数_c++ 复制函数-CSDN博客
看不懂堆栈的请参考:【C++】如何用C++创建对象,理解作用域、堆栈、内存分配_c++ 作用域 堆 内存-CSDN博客
看不懂进程线程的请参考:【C++】C++中的线程-CSDN博客
看不懂函数调用的请参考:【C++】数据类型、函数、头文件、断点调试、输入输出、条件与分支、VS项目设置_c++printf头文件-CSDN博客 中的函数部分
看不懂构造函数的请参考:【C++】类成员初始化列表、三元运算符、运算符及其重载、箭头操作符-CSDN博客 中的构造函数初始化列表部分

上面洋洋洒洒写了那么多,其实就是想说上面的代码其实并不优秀,因为拷贝过程太沉重了。如果说实例化时很沉重是无可奈何,那我只是拷贝一个一次性的、用完即丢的复制品都也这么沉重就太无语了。下面用移动构造函数优化代码:

加上上图中红框中的两个移动构造函数代码,就不会进行沉重的深拷贝了,就是进行了轻量的浅拷贝,而且加上C处的代码,程序也不会出现崩溃了。

上图A是对参数name进行了强制右值转换。这样初始化Entity实例对象时,如果有右值参数,就可以重载这个只接受右值参数的构造函数A了。如果没有代码A,那就得使用A上面的构造函数,这个构造函数即可接受左值也可接受右值。但是如果是右值参数传入,那它是先隐式转换,将右值转化为左值,然后才开始指向函数体的。所以也还是会发生深拷贝。所以我们一定要在Entity类中写一个只接受右值的构造函数A。

上图D处是使用std::move,这种写法等价于A。一般我们不建议使用A,因为不是什么对象都可以强制转换的。建议使用move,而这个下面一个小标题展开讲的内容。

3、移动语义:std::move与移动赋值操作符
上个小标题只讲了移动构造函数。其实移动语义还涉及到另外两个关键部分:std::move和move assignment operator(移动赋值操作符)。这两个小知识点是本不标题的讲解内容。例子还是我们的myString类和Entity类:

(1)std::move

(2)移动赋值操作符

待续。。。。

 

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

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

相关文章

三菱QD77MS定位模块速度更改功能

速度更改功能” 是以任意时机将控制中的速度更改为新指定的速度的功能。更改后的速度直接设置到缓冲存储器中,并根据速度更改指令([cd.15速度更改请求)或者外部指令信号执行速度更改。 但是,机械原点复位的情况下,检测出近点狗 ON 并开始向蠕…

【Django】视图函数

【Django】视图函数 视图函数的本质是Python中的函数,视图函数负责处理用户的请求并返回响应,该响应可以是网页的HTML内容、重定向、404错误、XML文档、图像或者任何东西,一般在应用中的views.py编写,示例代码如下: …

Git 入门篇(二)

前言 Git 入门篇(一) Git 入门篇(二) Git 入门篇(三) 目录 创建远程代码仓库 创建本地代码仓库 同步本地-远程代码仓库 代码托管 创建远程代码仓库 登录:gitee.com ​ 新建仓库 ​ 创建本…

PLC_博图系列☞基本指令”TOF:启动关断延时定时器“

PLC_博图系列☞基本指令”TOF:启动关断延时定时器“ 文章目录 PLC_博图系列☞基本指令”TOF:启动关断延时定时器“背景介绍TOF: 启动关断延时定时器说明参数脉冲时序图示例 关键字: PLC、 西门子、 博图、 Siemens 、 TOF 背…

【RabbitMQ】之高可用集群搭建

一、RabbitMQ 集群简介 1、默认集群原理1-1、RabbitMQ 集群简介 单台 RabbitMQ 服务器处理消息的能力是有瓶颈的,而且可靠性还无法保证,所以需要通过集群来提高消息的吞吐量和提高数据可靠性。 由于 RabbitMQ 本身是基于 Erlang 编写,而 Er…

改进系列(3):基于ResNet网络与CBAM模块融合实现的生活垃圾分类

目录 1. ResNet介绍 2. CBAM 模块 3. resnet cbam 3.1 添加在每个layer层后 3.2 关于训练的建议 4. 垃圾分类实战 4.1 数据集 4.2 训练 4.3 最好的权重 4.4 推理 5. 其它 1. ResNet介绍 ResNet(残差网络)是一种深度卷积神经网络模型&#xf…

Linux 服务器上部署 .NET Core 应用程序,值得收藏!

在 Linux 服务器上部署 .NET Core 应用程序,标志着传统的以微软为中心的部署平台的重大转变。.NET Core 的跨平台特性允许开发人员享受 Linux 环境的性能、可靠性和安全性。本指南提供了在各种 Linux 发行版上部署 .NET Core 应用程序的全面概述,重点是使…

2024-11-01 - 统一身份认证 - OpenLdap - 中间件 - 流雨声

摘要 2024-11-01 周五 杭州 暴雨 调查问卷: https://www.wjx.cn/vm/exIBFDM.aspx# 2024年转瞬即逝,可是生活还在继续,这里有一项关于人工智能和项目管理对于效能关系的调研问卷,AI 对工作的作用和影响。问卷不采集个人信息,在此…

前端页面性能优化的常见问题与解决方案

在当今互联网高速发展的时代,前端页面的性能对于用户体验至关重要。一个加载缓慢、交互卡顿的页面很可能会导致用户流失。本文将深入探讨前端页面性能优化中常见的问题以及相应的解决方案。 一、常见问题 (一)资源加载问题 文件体积过大 …

视频播放相关的杂记

基于QT FFMPEG设计一款 RTMP协议推流、视频录制软件 实现的功能: (1)将摄像头视频流 麦克风音频流合并,并推到流媒体服务器 (2)将摄像头视频流 麦克风音频流保存到本地磁盘 基于QtFFMPEG设计一款RTM…

Pycharm,2024最新版Pycharm下载安装配置教程!

目录 1、Pycharm 简介2、Pycharm下载3、环境变量的配置4、Pycharm的使用 1、Pycharm 简介 Pycharm资料领取不收米 PyCharm是一种Python IDE(Integrated Development Environment,集成开发环境),带有一整套可以帮助用户在使用Py…

Redis9:商户查询缓存3

欢迎来到“雪碧聊技术”CSDN博客! 在这里,您将踏入一个专注于Java开发技术的知识殿堂。无论您是Java编程的初学者,还是具有一定经验的开发者,相信我的博客都能为您提供宝贵的学习资源和实用技巧。作为您的技术向导,我将…

解锁函数的魔力:Python 中的多值传递、灵活参数与无名之美

文章目录 前言📖一、多值返回📚1.1 多值返回的概念📚1.2 工作原理📚1.3 应用场景📜总结 📖二、 多种参数传递形式📚2.1 位置参数(Positional Arguments)📚2.2…

若依框架-添加测试类-最新

1、在【ruoyi-admin】的pom.xml下添加依赖 <!-- 单元测试--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-test</artifactId><scope>test</scope></dependency><dependency>…

初识网络编程TCP/IP

目录 前言相关名词解释应用层协议——HTTP传输层协议socketTCP帧头格式三次握手、四次挥手 UDPTCP的socket实现 参考博文 前言 刚碰到网络编程&#xff0c;会出现一堆协议、概念、这层次那技术的&#xff0c;头都大了&#xff0c;还是得总结总结…… 相关名词解释 ✨✨网络…

【C++课程学习】:继承(上)(详细讲解)

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;C课程学习 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 一.继承的概念和定义 &#x1f384;继承的概念&#xff1a; &#x1f384;继承的定义&#xff1a; …

Linux笔记之pandoc实现各种文档格式间的相互转换

Linux笔记之pandoc实现各种文档格式间的相互转换 code review! 文章目录 Linux笔记之pandoc实现各种文档格式间的相互转换1.安装 Pandoc2.Word转Markdown3.markdown转html4.Pandoc 支持的一些常见格式4.1.输入格式4.2.输出格式 1.安装 Pandoc sudo apt-get install pandoc # …

封装的数字滚动组件的实现代码

效果&#xff1a; 学习啦&#xff1a; Vue 是一个渐进式框架&#xff0c;鼓励通过组件化来构建应用&#xff0c;其组件化优势&#xff1a; 代码复用&#xff1a;不同的视图和功能被封装成独立的组件&#xff0c;便于复用。易于维护&#xff1a;每个组件职责单一、耦合度低&…

Kafka面试题解答(二)

1.怎么尽可能保证 Kafka 的可靠性 kafka是可能会出现数据丢失问题的&#xff0c;Leader维护了一个动态的in-sync replica set&#xff08;ISR&#xff09;&#xff0c;意为和 Leader保持同步的FollowerLeader集合(leader&#xff1a;0&#xff0c;isr:0,1,2)。 如果Follower长…

分块——最为优雅的暴力

在信息学竞赛中&#xff0c;常常会遇到一些区间修改或区间查询的题目&#xff0c;如果直接敲暴力的话&#xff0c;时间复杂度是 O ( n m ) O(nm) O(nm) 可能会超时&#xff0c;如果写树状数组或线段树的话&#xff0c;又有一点复杂&#xff0c;不易理解&#xff0c;那么这时候…