C++之移动语义与左值右值深入学习:从入门到精通!

简介

本文详细阐述了 C++ 中关于移动语义、左值右值等技术的基本概念和常用技巧。

问题的产生

每一项技术的诞生都是为了解决某一个问题,移动语义、左值右值也是一样,因此我们先来看看问题产生的背景。

先来看一段代码:

#include <iostream> 
#include <string>
#include <string.h>#pragma warning(disable:4996)using namespace std;class String {
public:String() :_pstr(nullptr) {cout << "无参构造String()" << endl;}String(const char* pstr) : _pstr(new char[strlen(pstr) + 1]()) {cout << "C风格字符串构造 String(const char* )" << endl;strcpy(_pstr, pstr);}String(const String& rhs):_pstr(new char[strlen(rhs._pstr)+1]()) {cout << "拷贝构造 String(const String &)" << endl;}String& operator=(const String& rhs) {cout << "拷贝赋值运算符函数String& operator=(const String&)" << endl;if (this != &rhs) { //1、防止自复制delete[] _pstr; //2、释放左操作数_pstr = nullptr;//深拷贝_pstr = new char[strlen(rhs._pstr) + 1]();strcpy(_pstr, rhs._pstr);}return *this;}~String() {cout << "~String()" << endl;if (_pstr) {delete[] _pstr;_pstr = nullptr;}}friend ostream& operator<<(ostream& os, const String& rhs);
private:char* _pstr;
};ostream& operator<<(ostream& os, const String& rhs) {if (rhs._pstr) {os << rhs._pstr;}return os;
}void test() {// 会调用含 const char* 参数的构造函数String s1("hello");cout << "s1 = " << s1 << endl;cout << endl;// 会调用拷贝构造函数String s2 = s1;cout << "s1 = " << s1 << endl;cout << "s2 = " << s2 << endl;cout << endl;// 从结果上看,好像是调用 const char* 参数的构造函数?String s3 = "world";cout << "s3 = " << s3 << endl;
}int main() {test();return 0;
}

运行结果:

在这里插入图片描述

从运行结果上看,貌似 s1 和 s3 这两种初始化方式都调用的是 C 风格字符串参数的构造函数:

String s1("hello");
String s3 = "world";

但是实际上,String s3 = "world"; 这行代码本应该是和 s1 调用不一样的构造函数,为什么?

因为 "world" 属于一个 C 风格的字符串,而 s3 可以看作是一个 C++ 风格的字符串。

从 C 风格字符串向 C++ 风格字符串进行赋值这个行为如果能执行的话,其背后一定会有个隐式类型转换的操作,因为转换的时候只能是同种类型之间的转换。

因此下面这行代码的行为:

String s3 = "world";

实际上背后还执行了一步:

String("world");

而 s1 则是直接调用了对应的构造函数完成初始化:

String s1("hello");// s1 直接调用 String(const char*) 构造函数完成初始化了

因此对于 s3 来说,它先将 “world” 转换成了 String(“world”) 这个临时对象,然后再从 String(“world”) 这个临时对象又转换成了实际的对象 s3 。

正因为转换过程中产生了 String(“world”) 这个临时对象,因此其会调用一次含 C 风格字符串参数的构造函数一次,所以最后从运行结果上来看,似乎 s1 和 s3 的初始化过程相同,看起来好像都是通过含 C 风格字符串参数的构造函数完成对象初始化的。

实际上,在临时对象转换为 s3 对象这个过程,会调用一次拷贝构造函数,又因为这是个临时对象,被临时创建完之后就会立马被销毁,那么自然而然就会调用一次析构函数,但是因为现在的编译器给我们做了编译优化导致最后的运行结果中没有出现这个现象。

不过通过一些编译指令我们还是可以人为的看到这个现象:

在这里插入图片描述

对比两次的编译结果,不难发现上面多了一次拷贝构造的调用以及一次析构函数的调用。

最后可以得出一个结论,String("world") 属于一个右值或者说一个临时对象,它的生命周期就只在这一行代码中:

String s3 = "world";

由此不难发现,这多出来的一次拷贝构造和析构函数的调用并没有多大意义(因为只是临时的,创建完之后立马就会销毁),相反还会浪费资源和时间,会让程序执行的效率降低。

因此对于这种情况,实际上我们是可以做相应的处理的,怎么做?

如果说我们能去将相应的右值能够识别出来,那么这个右值里所涉及到的内存空间我们就不让它销毁而直接赋值给我们的 s3,那么这个时候问题就解决了。

具体的说,临时对象 String("world") 的创建是没有问题的也是肯定得创建的,它会去正常的申请堆内存空间,但这个时候如果说我们能够有一种语法规则能把该临时对象给识别出来然后把该临时对象所申请的堆内存空间直接转给我们的 s3,那么这个时候 s3 去进行创建的时候就不会需要再 new 一次了,问题也就圆满解决了。

那么如何区分出右值呢?这就要进入到我们的下一节内容了。

左值与右值的区分

直接上代码:

#include <iostream>
#include <string>
using namespace std;void test() {int a = 10;int b = 20;//不难发现,对于 a 和 b ,我们都是可以正常取地址的//因此此时的 a 和 b 就都是左值&a;&b;//我们使用指针变量,发现对于指针变量 pflag 也可以取地址//因此 pflag 也是左值int* pflag = &a;&pflag;&*pflag;//对于string类型,也可以用类似的方法测试//不难发现一样是可以取地址的,因此 s1 和 s2 也是左值string s1 = "hello";string s2 = "world";&s1;&s2;//对于自增的情况&(++a); //正确//&(a++); 报错,因为后置++这个过程返回的是一个临时对象,无法取地址,因此是右值//对于表达式的情况//&(a + b); 报错,是一个右值
}int main() {test();return 0;
}

由上述代码不难推知,在 C++ 中,左值和右值的区别就是左值是可以取地址的,而右值不可以。

另外引用相关的概念也与左值右值有关联,我们也来看一下,依然是看代码:

#include <iostream>
#include <string>
using namespace std;void test() {int a = 10;int b = 20;//对 ref 取地址是 ok 的const int& ref = a; //const左值引用可以绑定到左值&ref;// int& ref1 = 10; 报错,左值引用不可以绑定到右值上const int& ref1 = 10; //但const左值引用可以绑定到右值上//这也是为什么我们的拷贝构造函数的参数必须要写成const左值引用的原因//因为这样不管传的是左值还是右值进入拷贝构造函数就都是ok的
}int main() {test();return 0;
}

因为 10 不能被取地址,因此上面的代码注释中将其也归入了右值一列,但它其实还有个名字,叫做字面值常量。

因此简单的对左值和右值进行分类的话大致如下:

左值:可以取地址。

右值:不能进行取地址。包括临时变量、临时对象、字面值常量。

但依据上面的内容,我们现在也只能做到把左值给区分出来,而没办法有效区分出右值。

区分左值的话我们只需要使用左值引用即可:

在这里插入图片描述

而 const 左值引用既可以绑定到左值又可以绑定到右值,因此其也是区分不出来右值的。

因此现在我们需要一种新的语法手段,来将右值给区分出来。

右值引用的概念

这个语法就是右值引用,看代码:

#include <iostream>
#include <string>
using namespace std;void test() {//&&表示右值引用,这是C++11标准提出来的//使用右值引用,发现可以绑定到右值上int&& rref = 10;//那么右值引用可以绑定到左值上面吗?int a = 10;//int&& rref = a; 报错,说明右值引用无法绑定到左值上
}int main() {test();return 0;
}

通过上面的代码,说明了通过右值引用此时我们可以有效地区分出右值了。

右值引用可以识别右值,但不能识别左值。

这就意味着如果我们将原来的拷贝构造函数的左值引用给换成右值引用的话,那么在传进一个右值的时候应该就会调用这个有右值引用的 “拷贝构造函数” 了吧?

移动构造函数

当然!

依然是之前的 String 例子的代码,如下所示:

String(String&& rhs){cout << "右值引用的拷贝构造 String(const String &&)" << endl;
}

那么此时当我们再执行程序到下面这一行时:

String s3 = "world";

之前我们说过这行代码会产生一个临时对象,为了避免效率降低我们提出的方法是让这个临时对象申请的堆空间资源不要销毁,而是直接转给 s3,这样就能有效提升效率,那么这个右值引用的拷贝构造函数要做的就应该是这么一件事情,我们来实现一下:

//在执行 String s3 = String("world");的时候调用下面的右值拷贝构造
//直接将传进来的 _pstr 转给 s3
//_pstr是s3的成员变量,rhs._pstr是临时对象的成员变量
//此时进行浅拷贝,将s3的_pstr指向了临时对象所申请的内存空间
//此时s3就不需要再次申请空间,就解决了我们之前提出的问题
String(String&& rhs):_pstr(rhs._pstr) {cout << "右值引用的拷贝构造 String(const String &&)" << endl;//这里还要做一步,为了防止临时对象被销毁时调用delete造成二次析构//因此这里需要将临时对象的指针置为空rhs._pstr = nullptr;
}

而这就是我们一直在说的移动语义,而我们一直说的右值形式的构造函数其正名为:移动构造函数。

“ 移动 ” 二字的意思以上面的例子就是说将临时对象 String(“world”) 申请的堆空间直接转移给 s3 对象的数据成员 _pstr 了。

完整的代码如下:

#include <iostream> 
#include <string>
#include <string.h>#pragma warning(disable:4996)using namespace std;class String {
public:String() :_pstr(nullptr) {cout << "无参构造String()" << endl;}String(const char* pstr) : _pstr(new char[strlen(pstr) + 1]()) {cout << "C风格字符串构造 String(const char* )" << endl;strcpy(_pstr, pstr);}String(const String& rhs):_pstr(new char[strlen(rhs._pstr)+1]()) {cout << "拷贝构造 String(const String &)" << endl;strcpy(_pstr, rhs._pstr);}//移动构造函数//在执行 String s3 = String("world");的时候调用下面的右值拷贝构造//直接将传进来的 _pstr 转给 s3//_pstr是s3的成员变量,rhs._pstr是临时对象的成员变量//此时进行浅拷贝,将s3的_pstr指向了临时对象所申请的内存空间//此时s3就不需要再次申请空间,就解决了我们之前提出的问题String(String&& rhs):_pstr(rhs._pstr) {cout << "右值引用的拷贝构造 String(const String &&)" << endl;//这里还要做一步,为了防止临时对象被销毁时调用delete造成二次析构//因此这里需要将临时对象的指针置为空rhs._pstr = nullptr;}String& operator=(const String& rhs) {cout << "拷贝赋值运算符函数String& operator=(const String&)" << endl;if (this != &rhs) { //1、防止自复制delete[] _pstr; //2、释放左操作数_pstr = nullptr;//深拷贝_pstr = new char[strlen(rhs._pstr) + 1]();strcpy(_pstr, rhs._pstr);}return *this;}~String() {cout << "~String()" << endl;if (_pstr) {delete[] _pstr;_pstr = nullptr;}}friend ostream& operator<<(ostream& os, const String& rhs);
private:char* _pstr;
};ostream& operator<<(ostream& os, const String& rhs) {if (rhs._pstr) {os << rhs._pstr;}return os;
}void test() {// 会调用含 const char* 参数的构造函数String s1("hello");cout << "s1 = " << s1 << endl;cout << endl;// 会调用拷贝构造函数String s2 = s1;cout << "s1 = " << s1 << endl;cout << "s2 = " << s2 << endl;cout << endl;// 从结果上看,好像是调用 const char* 参数的构造函数?String s3 = "world";cout << "s3 = " << s3 << endl;
}int main() {test();return 0;
}

此时再编译运行,可以发现已经达到了我们要的效果(注意去掉编译器优化嗷):

在这里插入图片描述

最后注意:针对于右值而言,移动构造函数优先于拷贝构造函数的执行。

移动赋值运算符函数

同样的道理,我们的赋值运算符函数也存在这样的移动语义的问题。

来看代码:

//省略其它代码void test() {// 会调用含 const char* 参数的构造函数String s1("hello");cout << "s1 = " << s1 << endl;cout << endl;// 会调用拷贝构造函数String s2 = s1;cout << "s1 = " << s1 << endl;cout << "s2 = " << s2 << endl;//调用赋值运算符函数cout << endl;s2 = String("world");cout << "s2 = " << s2 << endl;
}int main() {test();return 0;
}

运行结果如下:

在这里插入图片描述

可以发现在进行赋值运算的时候也存在这样关于移动语义的问题,解决方法也是一样的。

根据之前的经验,我们可以写出移动赋值运算符函数如下:

//在执行 s2 = String("world"); 时就会调用下面这个移动赋值运算符函数了
String& operator=(String&& rhs) {cout << "移动赋值运算符函数String& operator=(const String&&)" << endl;//虽然右值没办法取地址//但是右值引用在作为函数形参的情况下是可以取地址的(函数的形参都是左值)//因为右值引用在作为函数形参时本身是可以绑定到某个右值上的//这意味着这里的右值引用是个左值(后面会举例子什么时候右值引用其实是个右值)//因此下面的 &rhs 成立if (this != &rhs) { //1、防止自复制delete[] _pstr; //2、释放左操作数_pstr = nullptr;//浅拷贝_pstr = rhs._pstr;rhs._pstr = nullptr;}return *this;
}

此时运行结果:

在这里插入图片描述

可以看到移动赋值运算符函数被调用了,这就印证了我们的想法。

std::move() 的使用

在上面的移动赋值运算符函数中:

//在执行 s2 = String("world"); 时就会调用下面这个移动赋值运算符函数了
String& operator=(String&& rhs) {cout << "移动赋值运算符函数String& operator=(const String&&)" << endl;//虽然右值没办法取地址//但是右值引用在作为函数形参的情况下是可以取地址的(函数的形参都是左值)//因为右值引用在作为函数形参时本身是可以绑定到某个右值上的//这意味着这里的右值引用是个左值(后面会举例子什么时候右值引用其实是个右值)//因此下面的 &rhs 成立if (this != &rhs) { //1、防止自复制delete[] _pstr; //2、释放左操作数_pstr = nullptr;//浅拷贝_pstr = rhs._pstr;rhs._pstr = nullptr;}return *this;
}

乍一看貌似好像这个防止自复制的操作是多余的,其实不然,因为在 C++ 中有一个函数可以将左值变成右值,它就是 std::move() 。

因此如果不考虑自复制的情况的话,当有下面代码的时候:

s2 = std::move(s2);

此时就一定会出问题了,因此防止自复制这一步一定不能省嗷。

std::move() 的作用就是将左值转换为右值,表明不想再使用该左值了。

这个转换的过程实际上在底层中就是发生了一次强制转换 static_cast<T &&> (lvalue) 而已。

一点点细节补充

为什么右值引用不考虑 const

因为没有意义啊,右值引用引用的都是右值,而我们拿到右值都是要修改的,比如上面代码我们要将临时对象的指针置空,加上 const 反而无法执行置空操作了。另外对于比如字面值常量等右值而言,它本来就是没办法改变的,加上 const 虽然不报错但是也没有什么额外的收益:

const int&& ref = 10;

因此对于右值引用我们一般不会考虑 const 的情况。

拷贝控制语义与移动语义的概念

拷贝构造函数与赋值运算符函数,编译器会自动提供;但是移动构造函数与移动赋值运算符函数,编译器不会自动提供,必须要手写。

将拷贝构造函数与赋值运算符函数称为具有拷贝控制语义的函数;将移动构造函数与移动赋值运算符函数称为具有移动语义的函数。

最后,移动语义函数的调用优先级要高于拷贝语义的函数。

总结

综上所述,我们所谓的移动语义呢其实指的就是类里面所具有的两个函数:一个叫做移动构造函数,一个叫做移动赋值运算符函数。这样当我们传递进来的是右值的时候我们就不必要再去执行深拷贝而只要去执行浅拷贝即可。

最后再简要总结一下本文的内容:

在这里插入图片描述

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

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

相关文章

JavaEE: Thread类

Thread的常见构造方法 Thread的常见属性 ID 是线程的唯一标识,不同线程不会重复名称是在使用各种调试工具时会用到的状态表示线程当前所处的情况优先级高的线程理论上来说更容易被调度到关于后台线程,需要记住:JVM会在一个进程的所有非后台线程结束后,才会结束运行是否存活,即r…

社交及时通讯平台完整版源码,uniapp技术,可打包成app

源码简介&#xff1a; 全原生&#xff0c;从底层开始结构就完全不一样&#xff0c;mongodb的库&#xff0c;uniapp混编手端&#xff0c;二开难度要比视酷或者酷信容易很多。全开源&#xff0c;带开发文档。前端用的是uniapp技术&#xff0c;所以是多端合一&#xff0c;可以做h…

【JVM基础14】——垃圾回收-强引用、软引用、弱引用、虚引用的区别

目录 1- 引言&#xff1a;为什么分多种引用类型2- ⭐核心&#xff1a;2-1 强引用2-2 软引用2-3 弱引用2-4 虚引用 3- 小结&#xff1a;3-1 强引用、软引用、弱引用、虚引用的区别&#xff1f; 1- 引言&#xff1a;为什么分多种引用类型 在 Java 中&#xff0c;引用类型被分为强…

linux 6.10.0 CXL/reg.c 详解

文章目录 前言Ref正文1. cxl_setup_regs2. cxl_probe_regs()3. cxl_probe_component_regs()4. cxl_probe_device_regs()5. cxl_map_device_regs()6. cxl_count_regblock() / cxl_find_regblock_instance() 前言 CXL 是一个比较新的技术&#xff0c;内核版本迭代太快&#xff0…

python爬虫预备知识三-多进程

python实现多进程的方法&#xff1a;fork、multiprocessing模块创建多进程。 os.fork方法 os.fork方法只适合于unix/linux系统&#xff0c;不支持windows系统。 fork方法调用一次会返回两次&#xff0c;原因在于操作系统将当前进程&#xff08;父进程&#xff09;复制出一份…

在Linux中,什么叫做线程

在Linux中&#xff0c;什么叫做线程&#xff1f; CPU调度的基本单位。 在Linux中&#xff0c;什么叫做进程&#xff1f; 内核视角&#xff1a; 承担分配系统资源的基本实体。 一个进程内部可以有多个执行流。 task_struct可以理解为轻量级进程。 线程是进程内部的一个分支…

【python】Python中位运算算法详细解析与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

数据库扩展新篇章:主流分库分表中间件全解析

摘要&#xff1a; 随着企业数据量的激增&#xff0c;传统的单体数据库架构已经无法满足日益增长的性能需求和数据管理复杂性。分库分表技术作为解决这一问题的有效手段&#xff0c;通过将数据水平或垂直地分散到多个数据库中&#xff0c;提高了系统的扩展性和处理能力。本文将详…

LISA: Reasoning Segmentation via Large Language Model

发表时间&#xff1a;CVPR 2024 论文链接&#xff1a;https://openaccess.thecvf.com/content/CVPR2024/papers/Lai_LISA_Reasoning_Segmentation_via_Large_Language_Model_CVPR_2024_paper.pdf 作者单位&#xff1a;CUHK Motivation&#xff1a;尽管感知系统近年来取得了显…

鸡舍环控系统

在现代养殖业中&#xff0c;科技的进步正悄然改变着传统模式&#xff0c;其中&#xff0c;基于物联网和自动控制技术的鸡舍环控系统正逐渐成为行业内的新宠。这套系统不仅能够集成温湿度、光照度等参数的监测与控制&#xff0c;还能通过智能化手段减轻日常养殖工作量&#xff0…

探索 Python 异步通信的奥秘:WebSockets 库的神奇之旅

文章目录 探索 Python 异步通信的奥秘&#xff1a;WebSockets 库的神奇之旅背景&#xff1a;为何选择 WebSockets&#xff1f;什么是 websockets 库&#xff1f;安装 websockets 库5个简单的库函数使用方法场景应用示例常见问题与解决方案总结 探索 Python 异步通信的奥秘&…

sqli-labs-php7-master\Less-1

1&#xff0c;进入mysql数据库 mysql -u root -p 接着&#xff1a; show databases; use security; select * from where id1 LIMIT 0,1; 函数的基本用法 system_user() #当前系统用户 user() #当前登录用户 current_user() #当前登录用…

electron-updater实现electron全量更新和增量更新——主进程部分

同学们可以私信我加入学习群&#xff01; 正文开始 前言更新功能所有文章汇总一、更新插件选择二、在main.js中引入我们的更新模块三、更新模块UpdateController.js暴露的方法checkUpdate四、更新模块UpdateController.js中的监听4.1监听是否有新版本需要更新&#xff1f;4.2 监…

怎样配置虚拟机IP

目录&#xff08;三步走&#xff09; 配置本机IP 配置虚拟机外部IP 配置虚拟机内部IP 参考链接&#xff1a; 配置本机IP 打开“网络和共享中心”——>更改“适配器设置” 找到“VMnet8”&#xff0c;然后右键“属性”&#xff0c;弹出下列窗口 输入本机IP&#xff08;你…

浅谈操作系统

我们前面谈到了一个可执行程序首先会到内存进行预先加载~而在我们的计算机中第一个被加载的软件就是操作系统~ 操作系统的主要工作就是对软硬件资源进行管理~ 这里我们先从操作系统下层开始讲起~ 我们把操作系统类比为校长&#xff0c;驱动程序类比为辅导员&#xff0c;底层硬件…

【学术会议征稿】第四届电气工程与计算机技术国际学术会议(ICEECT2024)

第四届电气工程与计算机技术国际学术会议&#xff08;ICEECT2024&#xff09; 2024 4th International Conference on Electrical Engineering and Computer Technology 第四届电气工程与计算机技术国际学术会议&#xff08;ICEECT2024&#xff09;将于9月27日-29日在哈尔滨举…

吴恩达机器学习COURSE1 WEEK2

COURSE1 WEEK2 多维特征 在线性回归中&#xff0c;往往特征不止一个&#xff0c;而是具有多维特征 例如&#xff0c;在预测房价的例子中&#xff0c;我们知道更多的信息&#xff1a; x 1 x_1 x1​&#xff1a;房屋的面积 x 2 x_2 x2​&#xff1a;卧室的数目 x 3 x_3 x3​&a…

微信小程序 - 自定义计数器 - 优化(键盘输入校验)

微信小程序通过自定义组件&#xff0c;实现计数器值的增加、减少、清零、最大最小值限定、禁用等操作。通过按钮事件触发方式&#xff0c;更新计数器的值&#xff0c;并修改相关联的其它变量。通过提升用户体验&#xff0c;对计数器进行优化设计&#xff0c;使用户操作更加便捷…

蜂窝网络架构

2G/3G 4G eNB RF-RRU eCPRI RRU-BBU 光纤 5G From 38.300 AMF处理信令等&#xff0c;UPF 用户面&#xff0c;后面还有SMF

医院不良事件监测预警上报系统,PHP不良事件管理系统源码

不良事件上报系统&#xff0c;支持医院进行10大类医疗安全&#xff08;不良&#xff09;事件的上报管理&#xff1b;帮助医院管理部门更好把控不良事件的发生趋势&#xff0c;分析医院内部潜在的问题和风险&#xff0c;采取适当的管理措施&#xff0c;有效加强质量控制&#xf…