【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)移动赋值操作符

待续。。。。

 

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

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

相关文章

uniapp实现H5和微信小程序获取当前位置(腾讯地图)

之前的一个老项目,使用 uniapp 的 uni.getLocation 发现H5端定位不准确,比如余杭区会定位到临平区,根据官方文档初步判断是项目的uniapp的版本太低。 我选择的方式不是区更新uniapp的版本,是直接使用高德地图的api获取定位。 1.首…

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

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

(十三)JavaWeb后端开发——MySQL2

目录 1.DQL数据查询语言 1.1基本查询 1.2条件查询 where关键字 1.3分组查询 1.4排序查询 1.5分页查询 2.多表设计 3.多表查询——联查 4.多表查询——子查询​ 5.MySQL 事务 6.事务管理(事务进阶) 7.MySQL 索引 1.DQL数据查询语言 分为五大…

C++虚继承演示

在继承中如果出现: 这种情况,B和C都继承了A,D继承了B、C 在D中访问A的成员会出现: 这样的警告 是因为在继承时A出现两条分支:ABD、ACD 编译器不知道访问的A中的元素是经过B继承还是C继承 所以B、C在继承A时要用到…

【论文速看】DL最新进展20241109-图像超分、物理信息神经网络、扩散模型

目录 【图像超分】【物理信息神经网络】【扩散模型】 【图像超分】 [2024 红外图像超分] Infrared Image Super-Resolution via Lightweight Information Split Network 论文链接:https://arxiv.org/pdf/2405.10561v2 代码链接:无 单图像超分辨率&…

革命性AI搜索引擎!ChatGPT最新功能发布,无广告更智能!

文章目录 零、前言一、ChatGPT最新AI搜索引擎功能操作指导实战1:搜索新闻实战2:搜索天气实战3:搜索体育消息 二、感受 零、前言 大人,时代变了。 最强 AI 助力下的无广告搜索引擎终于问世。我们期待已久的这一刻终于到来了,从今天起,ChatGPT…

qt QLocale详解

1、概述 QLocale是Qt框架中的一个类,用于处理与本地化相关的操作。它能够方便地实现日期、时间、数字和货币的格式化和解析,支持不同的语言、区域设置和字符集。QLocale提供了一种跨平台的方式来获取当前系统的语言设置,并返回该语言的本地化…

Linux基础4-进程4(环境变量,命令行参数详解)

上篇文章:Linux基础4-进程3(进程优先级,竞争,独立,并行,并发,进程切换)-CSDN博客 本章重点: Linux中环境变量的理解和使用 目录 一. 环境变量概念和查看环境变量 1.1 环境变量概念 1.2 查看环境变量 二. 获取环境变…

Java学习路线:Maven(一)认识Maven

目录 认识Maven 新建Maven文件 导入依赖 认识Maven Maven是一个Java的项目管理工具,通过Maven,我们可以实现: 项目自动构建,包括代码的编译、测试、打包、安装等依赖管理,快速完成依赖的导入 在学习Maven之前&…

Node.js——fs模块-文件删除

1、在Node.js中,我们可以使用unlink或unlinkSync来删除文件。 2、语法: fs.unlink(path,callback) fs.unlinkSync(path) 参数说明: path 文件路径 callback 操作后的回调函数 本文的分享到此结束,欢迎大家评论区一同讨论学…

PG逻辑复制的REPLICA IDENTITY几种设置

前两天同事问了一个PG的错误,创建一张普通表,insert插入正常,但是执行update和delete时,提示这个错误, 代码语言:javascript 复制 SQL 错误 [55000]: ERROR: cannot delete from table "temp_tb&qu…

【人工智能】利用大语言模型(LLM)实现机器学习模型选择与实验的自动化

文章目录 引言环境准备数据集说明 项目结构主要文件说明 导入必要的软件包软件包功能简述 辅助函数定义加载配置文件加载数据集预处理数据集函数功能详解 集成LLM进行模型选择调用LLM的函数定义函数功能详解 清理和验证LLM的输出清理超参数建议提取模型名称验证超参数修正超参数…

机器学习在时间序列预测中的应用与实现——以电力负荷预测为例(附代码)

📝个人主页🌹:一ge科研小菜鸡-CSDN博客 🌹🌹期待您的关注 🌹🌹 1. 引言 随着数据采集技术的发展,时间序列数据在各个领域中的应用越来越广泛。时间序列预测旨在基于过去的时间数据来…

Python学习------第四天

Python的判断语句 一、布尔类型和比较运算符 二、 if语句的基本格式 if语句注意空格缩进!!! if else python判断语句的嵌套用法:

Vue3版本的uniapp项目运行至鸿蒙系统

新建Vue3版本的uniapp项目 注意,先将HbuilderX升级至最新版本,这样才支持鸿蒙系统的调试与运行; 按照如下图片点击,快速升级皆可。 通过HbuilderX创建 官方文档指导链接 点击HbuilderX中左上角文件->新建->项目 创建vue3…

让Apache正确处理不同编码的文件避免中文乱码

安装了apache2.4.39以后&#xff0c;默认编码是UTF-8&#xff0c;不管你文件是什么编码&#xff0c;统统按这个来解析&#xff0c;因此 GB2312编码文件内的中文将显示为乱码。 <!doctype html> <html> <head><meta http-equiv"Content-Type" c…

LabVIEW实验室液压制动系统

压制动系统是许多实验设备的重要安全组件&#xff0c;尤其在高负荷、高速实验环境下&#xff0c;制动系统的性能对设备和操作人员的安全至关重要。传统的实验室液压制动系统监测方法存在数据采集实时性差、精度低、故障预警不及时等问题。为了提高实验安全性和设备运行的稳定性…

Qt教程(007):资源文件添加

文章目录 7.1 创建新的项目7.2 添加资源文件7.2 设置页面7.1 创建新的项目 选择创建项目类型 输入项目名称 勾选UI界面 7.2 添加资源文件 选中项目名称,右键,选择【Add New】 添加资源文件 选择Qt Resource File文件

【Python爬虫实战】深入解锁 DrissionPage:ChromiumPage 自动化网页操作指南

&#x1f308;个人主页&#xff1a;易辰君-CSDN博客 &#x1f525; 系列专栏&#xff1a;https://blog.csdn.net/2401_86688088/category_12797772.html ​ 目录 前言 一、ChromiumPage基础操作 &#xff08;一&#xff09;初始化Drission 和 ChromiumPage 对象 &#xff0…

如何保证kafka生产者数据可靠性

ack参数的设置&#xff1a; 0&#xff1a;生产者发送过来的数据&#xff0c;不需要等数据落盘应答 假如发送了Hello 和 World两个信息&#xff0c;Leader直接挂掉&#xff0c;数据就会丢失 生产者 ---> Kafka集群 一放进去就跑 数据可靠性分析&#xff1a;丢数 1&#…