C++11 右值引用

news2024/9/21 16:28:15

文章目录

  • 一. 左值?右值?
  • 二. 右值引用的使用
  • 三. 万能引用&完美转发
  • 四. 移动构造&移动赋值
  • 结束语

一. 左值?右值?

C++中,对于左值,右值,我们可能会理解为 = 赋值符号左边是左值,右边是右值。但是其实不是的。

首先,左值是什么?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,并且可以对其赋值。
左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。定义在const修饰符后的变量也是左值,虽然不能赋值,但是可以取地址。

int a = 10;
int *p = new int(7);
const char*str = "hello world";

这当中,除了10和"hello world",其他都是左值。因为他们都可以取地址。


而什么是右值呢?
10和"hello world"其实都是右值。
右值也是一个表示数据的表达式,如:字面常量,表达式返回值,函数返回值等等。右值都具有常性
右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址

int x=10;
int y=20;
x+y

x+y就是一个右值。

区分右值和左值的方式就是看是否能取地址


左值引用就是对左值的引用

int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;

需要注意的是,const 左值引用可以引用右值
因为2是右值,具有常性,直接左值引用其实是权限的放大,const左值引用时正确的


右值引用就是对右值的引用,给右值取别名

右值引用的语法:&&

//常见的右值
10;
x + y;
// 对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;

//右值引用可以引用move的左值
int a=10;
int &&r1=move(a);

需要注意的是,右值不能取地址,但是右值引用,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说,右值引用后的变量时左值。我们了解了右值的原理后可以有更好的理解。


稍微总结一下

  1. 左值可以取地址,右值不能取地址
  2. 左值引用只能引用左值,不能引用右值。但是const左值引用既可以引用左值,也可以引用右值
  3. 右值引用只能引用右值,不能引用左值。但是右值引用可以引用move后的左值

二. 右值引用的使用

首先,C++11还对右值进行分类:1.纯右值 2. 将亡值

int a=3;
int b=func(6);
int c=(x+y);

以上,3是纯右值
func(6)是函数的返回值,(x+y)是表达式的返回值。二者都是将亡值
将亡值其实就是中间过渡的临时变量,完成中间传递任务就要被销毁。


我们再回忆一下引用的目的:1. 给变量起别名 2. 减少拷贝
左值引用一般用于函数参数,函数返回值。因为传参和传值返回都是从一个域(函数)跳转到另一个域(函数),但在域中我们有申请堆区的数据,这份数据是每个域都可以访问的,而我们传递变量如果使用传参返回的话,会发生深拷贝,数据也不是原先的数据,还会多出拷贝的消耗。
但如果参数是引用或者传引用返回,就不会有深拷贝的出现

但是左值引用并没有解决全部的问题。我们还是会有传局部变量的情况,局部变量不能使用左值引用,因为局部变量出了作用域就销毁了,使用左值引用时非法访问空间。而当我们传参返回的是一个类时,拷贝的消耗会很大。所以右值引用就诞生了。


右值引用其实就是对局部变量,临时对象数据的转移。

首先,我们使用传值返回,可以看到a和b的地址其实并不一样,所以b是a的拷贝

在这里插入图片描述

因为a是局部变量,无法传引用返回。此处还只是因为a是int类型,拷贝代价低。但是如果是我们自定的类,那么拷贝的代价就高了。右值引用可以解决这样的问题。

在这里插入图片描述

这里我们看到,a和b的地址是一样的,其实右值引用就是将a这个局部变量的数据,转移到b中。

我们再以string作为例子了解右值引用的使用场景


首先,我们使用自己实现的简单的string,因为我们需要在构造处打印输出。方便我们观察。

string 如下:

namespace ljh
{
	class string
	{
	public:
		//单参数的构造
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}

		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}

		 移动构造
		//string(string&& s)
		//	:_str(nullptr)
		//	, _size(0)
		//	, _capacity(0)
		//{
		//	cout << "string(string&& s) -- 移动语义" << endl;
		//	swap(s);
		//}

		 移动赋值
		//string& operator=(string&& s)
		//{
		//	cout << "string& operator=(string&& s) -- 移动语义" << endl;
		//	swap(s);
		//	return *this;
		//}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		string operator+(char ch)
		{
			string tmp(*this);
			tmp += ch;
			return tmp;
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

我们在拷贝构造和赋值重载处加上输出语句,如果是深拷贝,那么就会调用二者之一。

在这里插入图片描述

我们看到,str2的构建是由两次深拷贝完成的。

在这里插入图片描述
两次深拷贝分别是第一个红框中的拷贝构造,和第二个传值返回,拷贝构造的临时对象。其实这里编译器还做了优化。原本还会有一个临时对象,传值返回的构造其实需要两次拷贝构造,但是编译器对于连续的构造会优化成一次拷贝构造。
在这里插入图片描述

这样拷贝的消耗其实是非常之大的。
其实在operator+中,创建的局部对象中的数据本来就可以使用,但是因为局部对象,无法直接将数据传递给别的域,所以需要拷贝。我们完全可以把局部对象的数据,转移到目标对象中,只需要一次交换,即减少拷贝,又不浪费资源


那右值引用如何使用呢?
首先,我们要在函数传参上区分二者

// 拷贝构造
string(const string& s)
	:_str(nullptr)
{
	cout << "string(const string& s) -- 深拷贝" << endl;
	string tmp(s._str);
	swap(tmp);
}

// 移动构造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	cout << "string(string&& s) -- 移动构造" << endl;
	swap(s);
}

当参数是一个右值时,会优先匹配下面的构造。使用右值引用,减少局部对象拷贝的构造,我们叫做移动构造

在这里插入图片描述

这次,传值返回时因为是临时对象,所以会优先匹配移动构造。

我们按步骤分析一下

在这里插入图片描述

首先,tmp是拷贝构造,也就是深拷贝,所以tmp和str1虽然内容一样,但是地址不同。

在这里插入图片描述
接着,tmp+=ch,扩容,地址改变。

在这里插入图片描述
移动构造,直接将扩容后的tmp的内容全部转移到str2中,二者内容相同,地址相同,说明是数据的直接转移。这就减少了拷贝的消耗了。

但是编译器此处还是存在优化
在这里插入图片描述

原本应该是先有一次拷贝构造,然后再移动构造。编译器优化成了只有一次移动构造。


右值引用参数的使用,在C++11的STL中都有实现

在这里插入图片描述

不仅如此,在相应的插入函数中,也有右值引用的重载版本

在这里插入图片描述

在C++98中,没有右值引用,没有移动构造,那么在使用STL时,比如list,拷贝的成本也很大。

在这里插入图片描述

像这样使用匿名对象,就会发生深拷贝。
但是如果是C++11,有右值引用和移动构造,就会是如下结果:

在这里插入图片描述
因为匿名对象时右值,生命周期也就一行,我们完全不需要重新拷贝一份匿名对象的数据,我们直接将匿名对象的数据转移就好了。

三. 万能引用&完美转发

我们先看下面这段代码

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

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

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


template<typename T>
void PerfectForward(T&& t)
{
 Fun(t);
}
int main()
{
 PerfectForward(10);           // 右值
 int a;
 PerfectForward(a);            // 左值
 PerfectForward(std::move(a)); // 右值
 const int b = 8;
 PerfectForward(b);      // const 左值
 PerfectForward(std::move(b)); // const 右值
 return 0;
}

运行结果如下:

在这里插入图片描述

可以看到,不管传入的参数是否是左值,函数模板的推导都是左值。
这就是万能引用

我们在第一部分也说了,右值引用的属性是左值,如下

int&&rr1=10;
rr1=20;
cout<<&rr1<<endl;

在这里插入图片描述

rr1既可以修改值,也可以取地址,所以是左值。

但是这就引出了一个问题。
在类似的函数模板或者类模板场景下,函数传参不就没有右值引用了吗?
而且在STL库中,右值引用不一定是在当前函数中使用,可能还会继续向下传递,但是如果是类模板的话,那就会出现万能引用的场景,右值引用就丢失了,变成左值引用了。

所以为了保持右值引用的属性,C++11提供了完美转发

完美转发:forward<函数模板>()
保持原本的左值属性(右值属性)

在这里插入图片描述

在这里插入图片描述

四. 移动构造&移动赋值

在C++中,类的默认成员函数有6个

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

前四个较为的重要,后两个使用不多。默认成员函数就是我们不写,编译器会生成一个默认的。
而在C++11中,又新增加了两个默认成员函数:移动构造和移动赋值重载

移动构造我们上述已经讲解过了,其实就是接收右值,然后将右值的数据交换。
而移动赋值重载,也是同理:接收右值,然后将数据交换。我们还以自己编写的string为例。移动赋值重载代码如下:

// 移动赋值
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) -- 移动构造" << endl;
	swap(s);
	return *this;
}

针对移动构造和移动赋值重载,二者默认生成的条件较为特殊,不同于前6个默认成员函数。

  • 如果没有实现移动构造函数,且析构函数,拷贝函数,拷贝赋值重载也都没有实现,那么编译器会自动生成默认移动构造。默认生成的移动构造,对于内置类型成员变会按字节拷贝,对于自定义类型成员,则需要看这个成员是否实现移动构造,有,则调用其移动构造,否则调用其拷贝构造

  • 如果没有实现移动赋值重载,且析构函数,拷贝函数,拷贝赋值重载也都没有实现,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值重载,对于内置类型成员会按字节拷贝,对于自定义类型,需要看该成员是否实现移动赋值重载,有则调用移动赋值重载,否则调用拷贝赋值重载

  • 如果提供了移动构造或者移动赋值重载,编译器则不会自动提供默认函数

小小总结一下:如果是深拷贝的类,那么析构,拷贝构造,拷贝赋值重载都需要编写,那么此时,也不会有默认的移动构造和移动赋值重载,需要我们自己编写。反之,浅拷贝的类,如果我们没有实现任意一种,则会生成默认的移动构造和移动赋值重载

结束语

感谢你的阅读

如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

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

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

相关文章

木棒 DFS经典题 剪枝优化 满注释版 java

&#x1f351; 算法题解专栏 &#x1f351; 题目地址 乔治拿来一组等长的木棒&#xff0c;将它们随机地砍断&#xff0c;使得每一节木棍的长度都不超过 50 50 50 个长度单位。 然后他又想把这些木棍恢复到为裁截前的状态&#xff0c;但忘记了初始时有多少木棒以及木棒的初始…

【数据结构与算法】最小生成树之普里姆(Prim)算法和克鲁斯卡尔(Kruskal)算法

&#x1f331;博客主页&#xff1a;大寄一场. &#x1f331;系列专栏&#xff1a;数据结构与算法 &#x1f618;博客制作不易欢迎各位&#x1f44d;点赞⭐收藏➕关注 目录 前言 一、最小生成树的概念 二、最小生成树的求解方法 三、练习题 四、最小生成树在实际应用中的例…

返回类对象时,什么时候调用拷贝构造函数,什么时候会进行返回值优化(RVO)

#include<iostream> using namespace std;class Person { public:Person(){}Person(int age){m_Age age;}Person(const Person& p){cout << "拷贝构造函数" << endl;}Person fun(){cout << "fun this" << " "…

一步步教你如何剪辑出专业水平的视频

1. 视频字幕制作。媒体梦工厂软件提供了强大的字幕制作功能&#xff0c;可以自主设计字幕的颜色、大小、字体等属性&#xff0c;使字幕更加具有视觉冲击力。"媒体梦工厂软件是一款广受欢迎的影视后期制作软件&#xff0c;自从软件发布以来在行业内有着广泛的应用。本文将会…

使用redis模拟手机验证码发送及消费者与生产者案例

规定一个手机号一天只能请求三次验证码&#xff0c;且每次请求的验证码只有一分钟就会过期 package com.doit.demo;import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool;import java.util.Random; import java.util.Scanner;public class PhoneNum {publ…

Spark基础入门篇 | MapReduce原理 + Spark原理 + PySpark环境搭建 + 简单实战

&#x1f604; 之前简单了解过Spark&#xff0c;并简单用别人的代码跑过pyspark的数据处理和模型的分布式推理&#xff0c;但没做系统的总结&#xff0c;那这篇博客就对Spark做个基础入门讲解&#xff0c;看完基本就算基础入门了&#xff0c;后面再实操就会轻松一些。 文章目录…

Windows本地提权 · 下篇

Windows本地提权&#xff0c;这种提权适用于有一本地个用户的基础上&#xff0c;有一定的权限&#xff0c;无法从webshell上进行提权 目录 BypassUAC提权 原理 关于UAC MFS绕过提权 UAC等级为低绕过测试 UAC等级为中绕过测试 UAC等级为高绕过测试 注意&#xff1a;bypa…

【综述】视频无监督域自适应(VUDA)的小综述

【综述】视频无监督域自适应&#xff08;VUDA&#xff09;的小综述 一篇小综述&#xff0c;大家看个乐子就好&#xff0c;参考文献来自于一篇综述性论文 链接&#xff1a;https://arxiv.org/abs/2211.10412 这次基于三篇有代表性的文章来讲解 X. Song, S. Zhao, J. Yang, H.…

第十篇、基于Arduino uno,用LCD1602(不带IIC的)显示屏显示字符——结果导向

0、结果 说明&#xff1a;可以在LCD1602屏幕上面显示字符&#xff0c;实时的变量&#xff0c;如果是你想要的&#xff0c;可以接着往下看。 1、外观 说明&#xff1a;注意是不带IIC通讯的LCD屏幕&#xff0c;外形如下。 2、连线 说明&#xff1a;需要连接十几根线。 uno——…

#机器学习--深度学习中的优化

#机器学习--深度学习中的优化 引言1、神经网络优化中的挑战1.1、病态1.2、局部极小值1.3、高原、鞍点和其它平坦区域1.4、悬崖1.5、长期依赖、梯度消失与梯度爆炸1.6、非精确梯度1.7、局部和全局结构间的弱对应1.8、优化的理论限制 2、优化算法2.1、随机梯度下降&#xff08;SG…

macos wireshark 抓取https包

1、启动浏览器 1.1 创建空文件 $ touch /Users/zhujl/Downloads/https/mysslkey.log 2、设置wireshark tls属性&#xff0c;指定tls密钥存储文件 2.1 进入Wireshark Preferfences > Protocols > TLS 属性配置 2.2 勾选上Reassemable TLS records spanning multiple …

【网络编程】https协议——加密与窃密的攻防战

目录 一、https协议的介绍 二、加密和解密 1、加密和解密的过程 2、为什么需要加密和解密 3、常见的加密方式 3.1对称加密 3.2非对称加密 3.3数据摘要&#xff08;数据指纹&#xff09; 3.4数字签名 三、https加密解密的方式选择和中间人攻击的方式 1、只使用对称加…

JUC 高并发编程基础篇

JUC 高并发编程基础篇 • 1、什么是 JUC • 2、Lock 接口 • 3、线程间通信 • 4、集合的线程安全 • 5、多线程锁 • 6、Callable 接口 • 7、JUC 三大辅助类: CountDownLatch CyclicBarrier Semaphore • 8、读写锁: ReentrantReadWriteLock • 9、阻塞队列 • 10、ThreadPo…

Android12之MediaMetricsService服务(一百五十四)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

谷歌云 | 你需要知道的关于软件开发中的人工智能

【本文由 Cloud Ace 整理&#xff0c;Cloud Ace 是谷歌云全球战略合作伙伴&#xff0c;拥有 300 多名工程师&#xff0c;也是谷歌最高级别合作伙伴&#xff0c;多次获得 Google Cloud 合作伙伴奖。作为谷歌托管服务商&#xff0c;我们提供谷歌云、谷歌地图、谷歌办公套件、谷歌…

底层课程导学

目录 一、底层导学 1.课程回顾 2.嵌入式系统分层 3.Linux层次结构 二、ARM课该怎么学 1.课程内容 2.学习方法 三、计算机基础知识 1.计算机的进制 2.总线 四、ARM存储模型 1.三级存储结 五、CPU工作原理 1.CPU工作原理 2.指令的执行过程 3.地址空间 六、ARM体系结构 1.ARM处理器…

自动化测试实战项目(二)连连看外挂

自动化测试和做外挂的原理很相似&#xff0c;都是模拟用户的鼠标和键盘操作, 给自己的程序写自动化就是做测试&#xff0c;给别人的程序写自动化就是外挂了。 本文使用的技术也同样适用制作“对对碰”&#xff0c;"找茬" 之类游戏的外挂。 阅读目录 QQ连连看外挂实…

《面试1v1》垃圾回收机制

我是 javapub&#xff0c;一名 Markdown 程序员从&#x1f468;‍&#x1f4bb;&#xff0c;八股文种子选手。 面试官&#xff1a; 小伙子,跟我聊聊垃圾回收机制吧。什么是垃圾?怎么回收? 候选人&#xff1a; 好的面试官,来吧!垃圾就是那些不再被程序使用的对象。Java 通过…

Spring Boot定时任务

目录 1.概述 2.Spring Boot定时任务 2.1.快速使用 2.2.cron表达式 3.业务示例 3.1.业务描述 3.2.业务实现 4.实现原理 5.自定义线程池 1.概述 在某些业务场景中&#xff0c;需要定时执行一些任务&#xff0c;有可能是定时统计然后生成报表&#xff0c;有可能是定时发…

Python自定义函数

目录 1. 语法 2. 常见用法 2.1. 函数的返回值 2.2. 函数互相调用 3. 实战练习 3.1. 定义执行Linux命令的函数 1. 语法 #定义函数 def 函数名(参数1, 参数2):函数体(代码块)......#调用函数(定义函数时使用了参数&#xff0c;调用也必须使用参数) 函数名(参数1, 参数2) 定…