【C++】C++11新特性——右值引用,来看看怎么个事儿

news2024/11/26 1:46:51
头像
🚀个人主页:@小羊
🚀所属专栏:C++
很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~

动图描述

目录

  • 前言
  • 一、左值引用和右值引用
  • 二、右值引用和移动语义
    • 2.1 移动构造
    • 2.2 移动赋值
    • 2.3 STL容器插入接口
    • 2.4 左值右值相互转换
    • 2.5 完美转发
  • 三、类的新功能
    • 3.1 新默认成员函数
    • 3.2 新关键字


前言

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。


一、左值引用和右值引用

引用简单来说就是给对象取别名,我们刚开始接触C++的时候就学过,这里又区分出左值引用和右值引用,它们有什么不同?要想探讨这个问题,首先应该了解清楚具体什么是左值什么是右值。

  • 左值: 一个表示数据的表达式,一般情况可以赋值(如果被const修饰就不能修改),左值可以出现在“=”的左边或右边,最关键的特性是左值可以取地址
  • 右值: 一个表示数据的表达式,一般不能修改,通常是字面常量、临时对象、匿名对象等,右值在“=”的右边,不能在左边,右边不能取地址
int main()
{
	//以下的a、p、b、*p、s[0]都是左值
	int a = 1;
	int* p = &a;
	const int b = a;
	*p = 10;
	string s("abcdef");
	s[0];

	//以下都是右值
	10;
	x + y;
	fmin(x, y);//函数返回值
	string("1234");

	return 0;
}

引用都是给对象取别名,左值引用就是给左值取别名,右值引用就是给右值取别名。

那左值引用能不能给右值取别名,右值引用能不能给左值取别名呢?

如果左值引用不能给右值取别名,那C++11出来之前右值是不是都不能取别名?猜测一下也知道大概率不是的。
左值引用一般是不能给右值取别名的,但是可以用const修饰就行了。因为前面也说了右值一般都是字面常量、临时对象、匿名对象等,而这些值都具有常性,如果不用const修饰就存在权限放大的问题。所以早期右值引用没出来之前右值也可以通过左值引用给取别名。

const int& r1 = 10;
const int& r2 = x + y;
const int& r3 = fmin(x, y);
const string& r4 = string("abcdef");

例如下面的场景:

int main()
{
	vector<string> v;
	string s("1111");
	v.push_back(s);
	v.push_back(string("2222"));
	v.push_back("3333");
	
	return 0;
}

前面我们模拟实现List的push_backvoid push(const T& x)加上const修饰另一个目的也是为了既能接收左值又能接收右值,这样我们既可以插入一个有名对象,又能插入匿名对象了。

同样的右值引用也一般不能给左值取别名,但是可以通过move(左值)的方式来给左值取别名。move()可以看作像是强制类型转换,所以也不会改变操作对象本身的属性。

int main()
{
	int a = 1;
	int* p = &a;
	const int b = a;
	*p = 10;
	string s("abcdef");
	s[0];

	int&& r1 = move(a);
	int*&& r2 = move(p);
	const int&& r3 = move(b);
	string&& r4 = move(s);
	string&& r5 = (string&&)s;

	return 0;
}

右值不能取地址,但是给右值取别名后,右值会被存储到特定位置,且可以取到该位置的地址,可以修改,如果不想被修改可以用const修饰。


二、右值引用和移动语义

引用的意义是减少拷贝。 在右值引用出现之前,左值引用还不太全面,有些传返回值的场景只能传值返回,不能传引用返回。比如传局部对象:

yjz::string to_string(int value)
{
	bool flag = true;
	if (value < 0)
	{
		flag = false;
		value = 0 - value;
	}
	yjz::string str;
	while (value > 0)
	{
		int x = value % 10;
		value /= 10;
		str += ('0' + x);
	}
	if (flag == false)
	{
		str += '-';
	}
	std::reverse(str.begin(), str.end());
	return str;
}

这里的str是一个局部对象,出了作用域就销毁,传引用会造成野引用,所以只能传值,返回值传值会先拷贝构造一个临时对象,再用临时对象拷贝构造目标对象。


2.1 移动构造

右值可分为纯右值和消亡值,纯右值比如字面常量,消亡值比如临时对象。临时对象用完就要消亡,再对它拷贝构造显得有点多余,既然它的结局已经注定了还不如把它的东西直接拿过来,这里就引出了移动构造,所以移动构造直接将构造的对象和被构造的对象数据交换(掠夺)一下就行。

移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

//拷贝构造
string(const string& str)
{
	_str = new char[str._capacity + 1];//多开一个存'\0'
	strcpy(_str, str._str);
	_size = str._size;
	_capacity = str._capacity;
}

//移动构造
string(string&& str)
{
	swap(str);
}

虽然即使没有移动构造,只有上面的拷贝构造也能因为有const修饰而接收左值和右值,但是有了移动构造编译器会走最匹配的。

1、string类只有拷贝构造,没有移动构造

在这里插入图片描述

2、string类有拷贝构造,也有移动构造

在这里插入图片描述

虽然str是一个左值,但是它出了作用域就消亡,和临时对象的结局是一样的,所以可以把str作为一个右值来走移动构造,这里是隐式的将strmove为右值。返回局部的大对象,调用移动构造的代价非常低,很实用。

也不是说所有的局部对象传值返回都要走移动构造,只有需要深拷贝的对象移动构造才有意义,像日期类这种对象拷贝构造和移动构造没有区别。

上面我们提到了像VS2022这种比较激进的编译器优化比较夸张,它一步到位优化为直接构造,这里str就像ret1的左值引用一样。

在这里插入图片描述
那既然编译器都优化的这么好了,那移动构造还有意义吗?并且它是直接构造,而走移动构造的话是构造+移动构造。既然右值引用现在被广泛使用了,就说明移动构造还是有重要意义的。

  1. 移动构造代价很小
  2. 不是所有的编译器都像VS2022这样做极致的优化
  3. 有其他场景下优化不了

2.2 移动赋值

除了移动构造,还有移动赋值,本质还是一样的。下面我们来看下有移动赋值和没有移动赋值有什么区别。

//赋值重载
string& operator=(const string& str)
{
	//防止自己给自己赋值
	if (this != &str)
	{
		delete[] _str;
		_str = new char[str._capacity + 1];
		strcpy(_str, str._str);
		_size = str._size;
		_capacity = str._capacity;
	}
	return *this;
}

//移动赋值
string& operator=(string&& str)
{
	swap(str);
	return *this;
}

在这里插入图片描述

有调用赋值重载的情况时编译器不能像之前一样优化为直接构造,因为这里调用to_string前ret1是已经存在的对象,编译器就没办法优化了。
这里还是把左值str隐式作为右值调用了移动赋值,因为虽然str是左值,但它是局部对象,终归是为ret1服务的,出了作用域就消亡,和临时对象的意义差不多。

所以不管是移动拷贝还是移动赋值,都是有意义的,编辑器的极致优化也处理不了所有情况,相比之下这里编译器的极致优化反倒显得意义不大,因为即使多了移动拷贝和移动赋值这一步骤,它们的消耗也是非常小的。

不管是移动构造还是移动赋值,处理的都是传值返回的问题。


2.3 STL容器插入接口

右值引用解决的不只是传值返回的问题,还有一些容器插入接口的问题。

在这里插入图片描述

int main()
{
	std::list<yjz::string> lt;

	yjz::string s1("111111");
	lt.push_back(s1);

	lt.push_back(yjz::string("222222"));

	lt.push_back("333333");

	lt.push_back(move(s1));

	return 0;
}

有了右值引用,我们就可以很方便的插入一些匿名对象,这样写不仅简单还会少一次拷贝构造。所以以后我们可以插入匿名对象,少了一次拷贝构造,消耗更低一些。

这里插入匿名对象时还有一个奇怪的现象:

在这里插入图片描述
其中红色箭头是实际执行路径。首先插入了一个string类型的一个匿名对象,push_back调到了右值引用的函数没问题,但下一步调用insert函数时为什么调到了左值引用的函数呢?

探讨这个问题前我们先来看这个:

yjz::string&& r1 = yjz::string("11111111");

这是一个右值引用没错,但右值是yjz::string("11111111"),而r1却是一个左值,所以右值引用本身是一个左值。
虽然右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。 例如:不能取字面量10的地址,但是r1引用后,可以对r1取地址,也可以修改r1。如果不想r1被修改,可以用const int&& r1去引用。
其实右值引用本身是左值也不奇怪,如果右值引用本身是右值,右值一般不能修改,那还怎么通过移动语义来掠夺资源呢。

再回到上面的问题,虽然匿名对象push_back时调到了右值引用的接口,但是接口中的x却是一个左值,所以接下来调用insert时就调到了左值引用的接口。
x本身一个左值,其引用的对象是一个右值,在调用insert时我们期望调用右值引用的接口,是参数匹配的问题,所以可以考虑用move进行类似强转的操作。

void push_back(T&& x)
{
	insert(end(), move(x));
}

这样就完了吗?还没完,在insert函数内部也存在着相同的问题。
在这里插入图片描述

这样就完了吗?还没完,这里move(x)传过去是一个右值,所以Node的构造函数也需要有一个右值引用为接口的版本。
在这里插入图片描述

这样就完了吗?还没完,这里的x也是一个左值,所以下面初始化_data时也需要move强转一下。
在这里插入图片描述

这样就完了吗?是的,这次真的完了。
上面的层层转换过程少一步都不行,这类似一个属性退化的问题。


2.4 左值右值相互转换

x是一个左值,如果我们需要它是一个右值也只是一句代码的事,同样右值如果我们需要它是一个左值也可以强转得到。
在这里插入图片描述

通过上面的一些实例可以看出,左值和右值可以相互转换,其实说到底左值和右值在底层没什么区别,其能不能取地址也只是语法层面上的约束,当然现阶段的我们还不适合过多关注底层,因为底层和语法层在某些地方是相悖的,这不利于我们小萌新学习,我们学习主要还是以语法层为主的。


2.5 完美转发

上面看到C++11后STL容器插入接口基本都对左值和右值做了对应的函数,那以后类似这样的场景我们都要写两个甚至更多的版本吗?为了方便C++11又引入了万能引用

template<class T>
void func(T&& x)
{
	//...
}

函数模版中,这里的T&& x不再是前面我们见到的右值引用,而是万能引用。它不是具体的左值引用或右值引用,而是根据传过去的参数自动推导引用类型。传左值就是左值引用,传右值就是右值引用。
在各种场景下它帮助我们实例化出下面四种函数:

void func(int& x);        //左值
void func(const int& x);  //const 左值
void func(int&& x);       //右值
void func(const int&& x); //const 右值

为什么那些容器接口没有使用万能引用呢?

  1. 那些函数是在类模版中的,类模版实例化后函数中的参数就是一个确定的类型,除非再套一层模版
  2. 历史遗留的原因,因为前面已经有左值引用的版本了

模板的万能引用只是提供了既能接收左值又能接收右值的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值

void func(int& x) { cout << "左值引用" << endl; };
void func(const int& x) { cout << "const 左值引用" << endl; };
void func(int&& x) { cout << "右值引用" << endl; };
void func(const int&& x) { cout << "const 右值引用" << endl; };

//函数模版
template<class T>
void PerfectForward(T&& t)
{
	func(t);
}

int main()
{
	PerfectForward(10);        //右值

	int a = 1;
	PerfectForward(a);         //左值

	PerfectForward(move(a));   //右值

	const int b = 2;
	PerfectForward(b);         //const 左值

	PerfectForward(move(b));   //const 右值
	return 0;
}

在这里插入图片描述

我们希望能够在传递过程中保持它的左值或右值的属性,就需要完美转发。

std::forward<T>(t)完美转发在传参的过程中保持了t的原生类型属性。

在这里插入图片描述

std::forward<T>(t)move的区别是:我们提前知道它是一个退化的右值,需要继续保持它的属性就用move;如果我们提前不知道它是左值还是右值,就用std::forward<T>(t),不管对于左值还是右值,完美转发都会保持它原本的属性。


三、类的新功能

3.1 新默认成员函数

前面我们学了类的6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载

默认成员函数是我们不写编译器会默认生成的函数,其中最后两个不常用。
C++11后又增加了两个默认的成员函数:移动构造和移动赋值重载。 它们两个的特点是:

  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
  • 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝(值拷贝),自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造
  • 移动赋值重载和移动构造基本类似

在这里插入图片描述
在这里插入图片描述

但是只要破坏了其中的一个条件就不会生成默认的移动构造,比如实现了析构函数:

在这里插入图片描述

可能有同学觉得移动构造和移动赋值重载这两个默认成员函数的自动生成条件有点苛刻,其实不然。
如果某个类需要显示写析构,就说明有资源释放,那就需要显示的写拷贝构造和复制重载,那就需要显示的写移动构造和移动赋值,它们是一体化的。对于日期类这样的不需要深拷贝的类,这些默认成员函数使用编译器默认生成的就可以了。所以对于这些默认成员函数,要么都自己写,要么都使用编译器默认生成的。
而默认生成的移动构造和移动赋值主要是作用于上面Person这样的类,它本身的成员并不需要深拷贝,但是其有自定义类型的成员,一般这个自定义类型成员都有自己的移动构造和移动赋值,那就会调用这个自定义类型自己的移动构造和移动赋值。

从这里也可以得出,如果一个类不需要写析构、拷贝构造等,就不要去多此一举了,因为这样可能会在其他地方出问题。


3.2 新关键字

  • default:强制生成默认成员函数关键字

如果因为某些原因我们真的需要默认成员函数,则可以用default强制生成。
在这里插入图片描述

  • delete:禁止生成默认成员函数关键字

如果正常情况下某些成员函数会默认生成,但是我们不想让它生成,可以使用delete声明该函数为删除函数。
在这里插入图片描述

  • final:禁止父类被继承,禁止虚函数被重写
  • override:检查虚函数是否重写

这两个关键字在《多态》中已经有详细介绍,这里就不再赘述。


本篇文章的分享就到这里了,如果您觉得在本文有所收获,还请留下您的三连支持哦~

头像

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

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

相关文章

【C++复习】经典笔试题

文章目录 八大排序快排过程 卡特兰数反转链表链表的回文结构左叶子之和另一棵树的子树归并排序类与对象编程训练杨辉三角字符串乘积二叉树前序遍历成字符串数组的交集二叉树的非递归前序遍历连续子数组的最大乘积 八大排序 插冒归稳定 快排过程 以 [3,4,6,1,2,4,7] 为例&#…

【计网笔记】以太网

经典以太网 总线拓扑 物理层 Manchester编码 数据链路层 MAC子层 MAC帧 DIX格式与IEEE802.3格式 IEEE802.3格式兼容DIX格式 前导码&#xff08;帧开始定界符SOF&#xff09; 8字节 前7字节均为0xAA第8字节为0xAB前7字节的Manchester编码将产生稳定方波&#xff0c;用于…

steam游戏模拟人生3缺少net framework 3.5安装不成功错误弹窗0x80070422怎么修复

模拟人生3在Steam上运行时提示缺少.NET Framework 3.5并出现错误代码0x80070422&#xff0c;通常意味着.NET Framework 3.5功能没有正确启用&#xff0c;或者安装过程中出现了问题。以下是解决这个问题的步骤&#xff1a; 1.启用Windows功能 按下Win R键&#xff0c;输入opti…

【论文学习与撰写】论文里的Mathtype公式复制粘贴,跨文档复制后错码/错位问题的解决

1、描述 问题&#xff1a;论文的草稿已经写好&#xff0c;里面的公式之类的都已经一个个打上去了 但是把草稿里的正文和公式粘贴在另一个文档里的时候&#xff0c;会出些公式格式错误的情况 那该怎么操作保证复制后的公式保持原格式呢 选中复制的内容&#xff0c;在另一个文…

MySQL【知识改变命运】10

联合查询 0.前言1.联合查询在MySQL里面的原理2.练习一个完整的联合查询2.1.构造练习案例数据2.2 案例&#xff1a;⼀个完整的联合查询的过程2.2.1. 确定参与查询的表&#xff0c;学⽣表和班级表2.2.2. 确定连接条件&#xff0c;student表中的class_id与class表中id列的值相等2.…

【c++篇】:解析c++类--优化编程的关键所在(一)

文章目录 前言一.面向过程和面向对象二.c中的类1.类的引入2.类的定义3.类的封装和访问限定符4.类的作用域5.类的实例化6.类对象模型 三.this指针1.this指针的引出2.this指针的特性3.C语言和c实现栈Stack的对比 前言 在程序设计的广袤宇宙中&#xff0c;C以其强大的功能和灵活性…

[k8s理论知识]6.k8s调度器

k8s默认调度器 k8s调度器的主要职责&#xff0c;就是为一个新创建出来的pod寻找一个适合的节点Node。这包括两个步骤&#xff0c;第一&#xff0c;从所有集群的节点中&#xff0c;根据调度算法挑选出所有可以运行该pod的节点&#xff0c;第二&#xff0c;从第一步的结果中&…

Java项目-基于springboot框架的企业客户信息反馈系统项目实战(附源码+文档)

作者&#xff1a;计算机学长阿伟 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、ElementUI等&#xff0c;“文末源码”。 开发运行环境 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBoot、Vue、Mybaits Plus、ELementUI工具&#xff1a;IDEA/…

Windows环境下Qt Creator调试模式下qDebug输出中文乱码问题

尝试修改系统的区域设置的方法&#xff1a; 可以修复问题。但会出现其它问题&#xff1a; 比如某些软件打不开&#xff0c;或者一些软件界面的中文显示乱码&#xff01; 暂时没有找到其它更好的办法。

10-Docker安装Redis

10-Docker安装Redis Docker安装Redis 以 Redis 6.0.8 为例&#xff1a; docker pull redis:6.0.8直接pull会出现以下错误 [rootdocker ~]# docker pull redis:6.0.8 Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request can…

[Python学习日记-50] Python 中的序列化模块 —— pickle 和 json

[Python学习日记-50] Python 中的序列化模块 —— pickle 和 json 简介 pickle 模块 json 模块 pickle VS json 简介 什么叫序列化&#xff1f; 序列化指的是将对象转换为可以在网络上传输或者存储到文件系统中的字节流的过程。序列化使得对象可以被保存、传输和恢复&#…

3D Slicer 教程二 ---- 数据集

上一章下载3d slicer的软件,这章从加载数据集来弄清楚3dslicer怎么使用. 一. 加载数据集 如果没有数据集,也可用用样本数据. (1) "File" --> "add Data" 可以添加图片文件夹,(试了MP4不行,内镜的视频估计不支持),添加单个图片的话,会出现一些选项, …

C++贪心

前言 C算法与数据结构 打开打包代码的方法兼述单元测试 简介 贪心算法&#xff08;Greedy Algorithm&#xff09;是一种在每一步选择中都采取在当前状态下最好或最优&#xff08;即最有利&#xff09;的选择&#xff0c;从而希望导致结果是全局最好或最优的算法策略。贪心算…

【设计模式系列】抽象工厂模式

一、什么是抽象工厂模式 抽象工厂模式&#xff08;Abstract Factory Pattern&#xff09;是一种创建型设计模式&#xff0c;它提供了一个接口&#xff0c;用于创建一系列相关或相互依赖的对象&#xff0c;而无需指定它们具体的类。这种模式允许客户端使用抽象的接口来创建一组…

AUTOSAR_EXP_ARAComAPI的5章笔记(17)

☞返回总目录 相关总结&#xff1a;AutoSar AP CM通信组总结 5.7 通信组 5.7.1 目标 通信组&#xff08;Communication Group&#xff0c;CG&#xff09;是由 AUTOSAR 定义的复合服务模板。它提供了一个通信框架&#xff0c;允许在 AUTOSAR 应用程序之间以对等方式和广播模…

第6章 元素应用CSS作业

1.使用CSS对页面网页元素加以修饰&#xff0c;制作“旅游攻略”网站。 浏览效果如下&#xff1a; HTML代码如下&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8"><title>旅游攻略</title><link type"t…

[JAVAEE] 线程安全问题

目录 一. 什么是线程安全 二. 线程安全问题产生的原因 三. 线程安全问题的解决 3.1 解决修改操作不是原子性的问题 > 加锁 a. 什么是锁 b. 没有加锁时 c. 加锁时 d. 死锁 e. 避免死锁 3.2 解决内存可见性的问题 > volatile关键字 (易变的, 善变的) a. 不加…

【Linux】多线程安全之道:互斥、加锁技术与底层原理

目录 1.线程的互斥 1.1.进程线程间的互斥相关背景概念 1.2.互斥量mutex的基本概念 所以多线程之间为什么要有互斥&#xff1f; 为什么抢票会抢到负数&#xff0c;无法获得正确结果&#xff1f; 为什么--操作不是原子性的呢&#xff1f; 解决方式&#xff1a; 2.三种加锁…

git add操作,文件数量太多卡咋办呢,

git add介绍 Git的add命令是用于将文件或目录添加到暂存区&#xff08;也就是索引库&#xff09;&#xff0c;以便在后续的提交&#xff08;commit&#xff09;操作中一并上传到版本库的。具体来说&#xff0c;git add命令有以下几种常见用法&#xff1a; 添加单个文件&#…

4、.Net 快速开发框架:DncZeus - 开源项目研究文章

DncZeus 是一个基于 ASP.NET Core 和 Vue.js 的前后端分离的通用后台管理系统框架&#xff0c;其愿景是成为一个易于使用且功能丰富的 .NET Core 通用后台权限管理模板系统基础框架。项目名称 "DncZeus" 由 "Dnc"(.NET Core 的缩写)和 "Zeus"(古…