【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.mzph.cn/news/885539.shtml

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

相关文章

【国内中间件厂商排名及四大中间件对比分析】

国内中间件厂商排名 随着新兴技术的涌入,一批国产中间件厂商破土而出,并在短时间内迅速发展,我国中间件市场迎来洗牌,根据市占率,当前我国中间件厂商排名依次为:东方通、宝兰德、中创股份、金蝶天燕、普元…

Android自启动管控

1. 自启动管控需求来源 自启动、关联启动、交叉启动、推送启动等现象的泛滥除了对个人信息保护带来隐患外,还会导致占用过多的系统CPU和内存资源,造成系统卡顿、发热、电池消耗过快;还可能引入一些包含“恶意代码”的进程在后台隐蔽启动&…

C++上机实验|多态性编程练习

1.实验目的 (1)理解多态性的概念。 (2)掌握如何用虚函数实现动态联编 (3)掌握如何利用虚基类。 2.实验内容 设计一个飞机类 plane,由它派生出歼击机类fighter和轰炸机类 bomber,歼击机类fighter 和轰炸机类bomber 又共同派生出歼轰机(多用途战斗机)。利用虚函数和虚基类描述…

ctfshow(328)--XSS漏洞--存储型XSS

Web328 简单阅读一下页面。 是一个登录系统,存在一个用户管理数据库。 那么我们注册一个账号,在账号或者密码中植入HTML恶意代码,当管理员访问用户管理数据库页面时,就会触发我们的恶意代码。 思路 我们向数据库中写入盗取管理员…

Kubernetes的概述与架构

Kubernetes 的概述 Kubernetes 是一个可移植、可扩展的开源平台,用于管理容器化的工作负载和服务,方便进行声明式配置和自动化。Kubernetes 拥有一个庞大且快速增长的生态系统,其服务、支持和工具的使用范围广泛。 Kubernetes 这个名字源于…

crond 任务调度 (Linux相关指令:crontab)

相关视频链接 crontab 进行 定时任务 的设置 概述 任务调度:是指系统在某个时间执行的特定的命令或程序 任务调度的分类: 1.系统工作:有些重要的工作必须周而复始地执行。如病毒扫描等。 2.个别用户可能希望执行某些程序,比如…

408最后冲刺阶段,怎么做题才能考到120+?

C哥专业提供——计软考研院校选择分析专业课备考指南规划 重要性排序如下:真题占据首位,紧随其后的是王道模拟题,王道书与题目则紧随其后,而408统考配套习题(高教版)与之大致相当。 真题,无疑…

uniapp上拉刷新下拉加载

方法一: z-paging 的组件库: show-loading-more-no-more-view"false" 该属性控制是否显示 "加载更多" 或 "没有更多" 的提示。如果设为 false,则不会显示这些提示。如果设为 true,当数据加载完毕…

Java I/O(输入/输出)——针对实习面试

目录 Java I/O(输入/输出)什么是Java I/O流?字节流和字符流有什么区别?什么是缓冲流?为什么要使用缓冲流?Java I/O中的设计模式有哪些?什么是BIO?什么是NIO?什么是AIO&am…

AJAX 全面教程:从基础到高级

AJAX 全面教程:从基础到高级 目录 什么是 AJAXAJAX 的工作原理AJAX 的主要对象AJAX 的基本用法AJAX 与 JSONAJAX 的高级用法AJAX 的错误处理AJAX 的性能优化AJAX 的安全性AJAX 的应用场景总结与展望 什么是 AJAX AJAX(Asynchronous JavaScript and XML…

本地保存mysql凭据实现免密登录mysql

本地保存mysql凭据 mysql加密登录文件简介加密保存mysql用户的密码到本地凭据 mysql加密登录文件简介 要在 mysql客户端 上连接 MySQL 而无需在命令提示符上输入用户名和口令,下列三个位置可用于存储用户的mysql 凭证来满足此要求。 配置文件my.cnf或my.ini /etc…

图形几何之美系列:仿射变换矩阵(二)

“ 在几何计算、图形渲染、动画、游戏开发等领域,常需要进行元素的平移、旋转、缩放等操作,一种广泛应用且简便的方法是使用仿射变换进行处理。相关的概念还有欧拉角、四元数等,四元数在图形学中主要用于解决旋转问题,特别是在三维…

Jmeter的安装,设置中文,解决乱码问题

1.Jmeter安装 1-Jmeter如何下载 1---我这里提供一个下载快的方式 https://www.123684.com/s/lWZKVv-4jiav?提取码:4x4y 2---Jmeter官网下载地址 Apache JMeter - Download Apache JMeter 2-配置java环境 1---下载javaJDK 官方下载地址 https://www.oracle.com/java/techno…

【Uniapp】Uniapp Android原生插件开发指北

前言 在uniapp开发中当HBuilderX中提供的能力无法满足App功能需求,需要通过使用Andorid/iOS原生开发实现时,或者是第三方公司提供的是Android的库,这时候可使用App离线SDK开发原生插件来扩展原生能力。 插件类型有两种,Module模…

微信小程序——用户隐私保护指引填写(详细版)

✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…

reg注册表研究与物理Hack

reg注册表研究与物理Hack 声明:内容的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负。 目录 reg注册表研究与物理HackWindows注册表修改注册表实现应用程序开机…

[论文粗读][REALM: Retrieval-Augmented Language Model Pre-Training

引言 今天带来一篇检索增强语言模型预训练论文笔记——REALM: Retrieval-Augmented Language Model Pre-Training。这篇论文是在RAG论文出现之前发表的。 为了简单,下文中以翻译的口吻记录,比如替换"作者"为"我们"。 语言模型预训练…

深入浅出WebSocket(实践聊天室demo)

文章目录 什么是WebSocket?WebSocket连接过程WebSocket与Http的区别重连机制完整代码使用方法心跳机制实现聊天室demo(基于Socket.io)参考文章、视频小广告~什么是WebSocket? WebSocket 是一种在单个TCP连接上进行全双工通信的协议(计算机网络应用层的协议) 在 WebSocket A…

RabbitMQ队列详细属性(重要)

RabbitMQ队列详细属性 1、队列的属性介绍1.1、Type:队列类型1.2、Name:队列名称1.3、Durability:声明队列是否持久化1.4、Auto delete: 是否自动删除1.5、Exclusive:1.6、Arguments:队列的其他属性&#xf…

springboot029基于springboot的网上购物商城系统

🍅点赞收藏关注 → 添加文档最下方联系方式领取本源代码、数据库🍅 本人在Java毕业设计领域有多年的经验,陆续会更新更多优质的Java实战项目希望你能有所收获,少走一些弯路。🍅关注我不迷路🍅 项目视频 基于…