【C++11】C++11新增语法特性 右值引用/移动语义/完美转发

news2024/11/22 15:58:36

C++11 右值引用

  • 1 右值引用
    • 1.1 左值 、 右值
    • 1.2 左值引用 VS 右值引用
    • 1.3 谈谈C++11引入右值引用的意义
    • 1.4 左值引用和右值引用的一些细节问题
  • 2 移动语义
  • 3 完美转发
  • 4 总结

1 右值引用

1.1 左值 、 右值

在C++中所有的值不是左值就是右值。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束后就不再存在的临时对象。有名字的对象都是左值,右值没有名字。

区分左值和右值最便捷的方法就是看能不能对表达式取地址。如果能,则为左值,否则就是右值。


C++11扩展了右值的概念,将右值分为纯右值将亡值

纯右值

1.非引用返回的临时对象
2.运算表达式产生的结果
3.字面常量(C风格字符串除外,它是地址)

将亡值

与右值引用相关的表达式。
例如:将要被移动的对象,T&&的函数返回值,std::move()的返回值,转换成T&&类型的转换函数的返回值

左值右值代码示例:

class A
{
public:
	int a;
};
A getTemp()
{
	return A();
}
	int i = 3; //i是左值,3是右值
	int j = i + 8;//j是左值,i+8是右值
	A a1 = getTemp();//a1是左值,getTemp()返回的临时变量是右值引用

1.2 左值引用 VS 右值引用

在C++98中的引用是左值引用,就是给变量取个别名。在C++11中,因为新增了右值引用的概念,所有把C++98中的引用都称为左值引用。

右值引用就是给右值取个名字
语法:

数据类型&& 变量名 = 右值;

代码示例:

#include <iostream>

using namespace std;

class A
{
public:
	int m_a = 9;
};
A getTemp()
{
	return A();
}
int main()
{
	int&& a = 3;//3是右值
	int b = 8;//b是左值
	int&& c = b + 5;//b+5 是右值
	A&& aa = getTemp();//getTemp的返回值是右值(临时变量)

	cout << "a=" << a << endl;//3
	cout << "c=" << c << endl;//13
	cout << "aa.m_a=" << aa.m_a << endl;//9
	return 0;
}

值得一提的是,右值有了名字之后,就成了普通变量,普通变量有名字就可以取地址。
所以上述代码中:
a,c,aa均可以看成是左值

我们可以用代码验证一下:
对左值进行运算:

	int&& a = 3;//3是右值
	a++;
	int b = 8;//b是左值
	int&& c = b + 5;//b+5 是右值
	c++;
	A&& aa = getTemp();//getTemp的返回值是右值(临时变量)
	aa.m_a++;
	cout << "a=" << a << endl;//4
	cout << "c=" << c << endl;//14
	cout << "aa.m_a=" << aa.m_a << endl;//10

对左值取地址

在这里插入图片描述

也可以像普通引用一样加const约束

在这里插入图片描述

1.3 谈谈C++11引入右值引用的意义

在上述代码中,getTemp()的返回值在表达式语句结束之后也就该终结了(因为是临时变量),而通过右值引用获得了新生,其生命周期将与右值引用aa的生命周期一样,只要aa还活着,该右值临时变量将会一直活下去。

引入右值引用的主要目的是实现移动语义(后面会讲)

1.4 左值引用和右值引用的一些细节问题

左值引用只能绑定(关联、指向)左值,右值引用只能绑定(关联、指向)右值,如果绑定的不对,编译失败。

错误代码示例:
把上述代码的右值引用改为左值引用

int& a = 3;//错误代码,3是右值
int& c = b + 5;错误代码,b+5 是右值
A& aa = getTemp();//,错误代码,getTemp的返回值是右值(临时变量)

在这里插入图片描述
错误代码示例:
错误使用右值引用

int&& c = b;//错误代码,b是左值,却用了右值引用

在这里插入图片描述


万能引用类型 : 常量左值引用

常量左值引用可以算是一个万能的引用类型,它可以绑定非常量左值,常量左值,右值,而且在绑定右值的时候,常量左值引用一样将右值的生命周期延长,缺点是只能读,不能改。

在笔者之前写过一篇关于常量左值引用
常量左值引用的一些经典问题

在C++引入了右值引用以后,能更方便地解决了这些问题。

代码示例:

	int a = 1;
	const int& ra = a;//a是非常量左值
	const int b = 1;
	const int& rb = b;//b是常量左值
	const int& rc = 1;//1是右值

2 移动语义

如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。
深拷贝把对象中的堆区资源复制了一份,如果源对象(被拷贝的对象)是临时对象,拷贝完就没什么用了,这样会造成没有意义的资源申请和释放操作。如果能够直接使用源对象拥有的资源,可以节省资源申请和释放的时间。C++新增的移动语义就能够做到这一点。

实现移动语义要增加两个函数:移动构造函数和移动赋值函数

移动构造函数的语法:

类名(类名&& 源对象){…}

移动赋值函数的语法:

类名&operator=(类名&& 源对象){…}

代码示例:
我们先写出不用右值引用的拷贝构造函数和赋值函数

#include <iostream>
#include <utility>
using namespace std;

class AA
{
public:

	AA(int* _m_data)
		:m_data(new int[sizeof(_m_data)])//开辟空间
	{
		*m_data = *_m_data;//赋值
	}
	AA()
	{
	}
	AA(const AA& a)//拷贝构造函数
		:m_data(nullptr)
	{
		cout << "调用了拷贝构造函数" << endl;
		AA tmp(a.m_data);
		swap(tmp);
	}
	AA& operator=(const AA& a)
	{
		cout << "调用了赋值函数" << endl;
		if (this != &a)//避免自我赋值
		{
			AA tmp(a.m_data);
			swap(tmp);
		}
		return *this;
	}
	void swap(AA& a)
	{
		std::swap(m_data, a.m_data);//库里面的交换函数
	}
	~AA()
	{
		delete m_data;
		m_data = nullptr;
	}
public:
	int* m_data = nullptr;//数据成员,指向堆区资源的指针
};

int main()
{
	int t = 3;
	AA a1(&t);//创建一个对象a1
	cout << "a1.m_data=" << *a1.m_data << endl;

	AA a2 = a1;//将调用拷贝构造函数
	cout << "a2.m_data=" << *a2.m_data << endl;

	AA a3;
	a3 = a1; //将调用赋值函数
	cout << "a3.m_data=" << *a3.m_data << endl;
	return 0;
}

在这里插入图片描述

其中我们拷贝构造函数和赋值函数采用了现代写法:先创建一个临时对象,临时对象会调用一次次有参构造,申请内存资源,然后将该临时对象与this指针交换即可完成我们的拷贝构造。赋值函数同理。

不管是传统的写法还是现代的写法,都需要再申请一份堆区的资源,这份申请的资源被拷贝完后就没什么用了,造成了资源的浪费。于是就有了我们的移动语义

AA(const AA& a)//拷贝构造函数
		:m_data(nullptr)
	{
		cout << "调用了拷贝构造函数" << endl;
		AA tmp(a.m_data);
		swap(tmp);
	}
	AA(AA&& a)  //移动构造函数
		:m_data(nullptr)       
	{
		cout << "使用了移动语义" << endl;
		swap(a);//直接交换即可,不用再申请空间
	}

注意:移动构造函数的函数参数里不能加const,因为我们要对a这个对象的值进行修改。

这是交换前:
在这里插入图片描述
交换后:
在这里插入图片描述
赋值函数也同理:

AA& operator=(const AA& a) //赋值函数
	{
		cout << "调用了赋值函数" << endl;
		if (this != &a)//避免自我赋值
		{
			AA tmp(a.m_data);
			swap(tmp);
		}
		return *this;
	}
	AA& operator=(AA&& a)//移动赋值函数
	{
		cout << "调用了移动语义" << endl;
		if (this != &a)//避免自我赋值
		{
			swap(a);
		}
		return *this;
	}

我们运行一下代码试试:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

注意事项:
1.对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?(就是我们的演示代码)C++11为了解决这个问题,提供了std::move()方法来讲左值转义为右值,从而方便使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我使用拷贝函数,而是用移动构造函数。左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值中的资源,可能会发生意想不到的错误。
2.如果没有提供移动构造/移动赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/移动赋值函数就去寻找拷贝构造/赋值函数。
3.C++11中的所有容器都实现了移动语义,避免对含有资源的对象发生无谓的拷贝。
4.移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。

3 完美转发

在讲完美转发之前,我们先来看这样一个问题。

如下的代码,我们想要的结果是让fun(a)调用fun1(int& ii) 这个函数,fun(8)调用fun1(int&& ii)这个函数,但是通过一个函数中转调用,编译器都会认为是左值。

void fun1(int& ii) //参数如果是左值,调用此函数
{
	cout << "参数是左值=" << ii << endl;
}
void fun1(int&& ii) //参数如果是右值,调用此函数
{
	cout << "参数是右值=" << ii << endl;
}

void fun(int i) //通过该函数调用fun1
{
	fun1(i);
}
int main()
{
	int a = 3;
	fun(a);//a是左值
	fun(8);//8是右值
	return 0;
}

在这里插入图片描述

那如果我们把代码改成这样:

void fun(int& i) //通过该函数调用fun1
{
	fun1(i);
}

fun(8)这行代码就会报错,因为8是右值
在这里插入图片描述
同理,把代码改成这样:

void fun(int&& i) //通过该函数调用fun1
{
	fun1(i);
}

在这里插入图片描述
无法将左值绑定到右值引用

解决方案
我们重载一份fun函数,一份左值引用,一份右值引用。
右值引用的的fun()在调用fun1的时候,把i利用move函数改成右值

void fun(int& i) //通过该函数调用fun1
{
	fun1(i);
}
void fun(int&& i) //通过该函数调用fun1
{
	fun1(move(i));
}

如果上述代码是以函数模板的形式写的,那么C++11有了更完美的解决方案,就是完美转发
原代码

template <class T> 
void fun(T& i) //通过该函数调用fun1
{
	fun1(i);
}
template <class T>
void fun(T&& i) //通过该函数调用fun1
{
	fun1(move(i));
}
int main()
{
	int a = 3;
	fun(a);//a是左值
	fun(8);//8是右值
	return 0;
}

在这里插入图片描述
上述的重载了两个函数模板,反而违背了模板的理念。所以,利用C++11完美转发优化后的代码如下:

void fun1(int& ii) //参数如果是左值,调用此函数
{
	cout << "参数是左值=" << ii << endl;
}
void fun1(int&& ii) //参数如果是右值,调用此函数
{
	cout << "参数是右值=" << ii << endl;
}

template <class T>
void fun(T&& i) //注意这里:左值为什么也能绑定到右值?
{
	fun1(forward<T>(i));//完美转发
}
int main()
{
	int a = 3;
	fun(a);//a是左值
	fun(8);//8是右值
	return 0;
}

在这里插入图片描述

在函数模板中,可以将参数“完美”的转发给其他函数。所谓完美,即不仅能准确的转发参数的值,还能保住被转发参数的左、值属性不变。
C++11标准引入了右值引用和移动语义,所以,能否实现完美转发,决定了该参数在传递过程中使用的是拷贝语义还是移动语义。
为了支持完美转发,C++11提供了以下方案:

引用折叠(模板参数才能使用)

如果模板中(包括类模板和函数模板)函数的参数书写成为T&&,那么,函数既可以接受左值引用,又可以接受右值引用。

std::forward<T> 参数

提供了函数模板 std::forward<T>(参数),用于转发参数,如果参数是一个右值,转发之后仍是右值引用;如果参数是一个左值,转发之后仍是左值引用。

4 总结

以上就是右值引用/移动语义/完美转发的解释。C++11引入引入右值引用是为了给移动语义和完美转发服务的。移动语义的移动拷贝和移动赋值为我们避免了内存资源的浪费,完美转发解决了代码的冗余。

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

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

相关文章

【JavaSE】Java基础语法(二十二):包装类

文章目录 1. 基本类型包装类2. Integer类3. 自动拆箱和自动装箱4. int和String类型的相互转换 1. 基本类型包装类 基本类型包装类的作用 将基本数据类型封装成对象的好处在于可以在对象中定义更多的功能方法操作该数据常用的操作之一&#xff1a;用于基本数据类型与字符串之间的…

Goby 漏洞更新|锐捷网络 NBR路由器 webgl.data 信息泄露漏洞

漏洞名称&#xff1a;锐捷网络 NBR路由器 webgl.data 信息泄露漏洞 English Name&#xff1a;Ruijie NBR Router webgl.data information CVSS core: 7.5 影响资产数&#xff1a;204290 漏洞描述&#xff1a; 锐捷网络NBR700G路由器是锐捷网络股份有限公司的一款无线路由设…

口碑超好的挂耳式耳机盘点,这几款蓝牙耳机值得一看!

运动已成为人们业余生活中不可缺少的组成部分&#xff0c;徒步、夜跑、骑行等运动项目受到越来越多的人的喜欢&#xff0c;运动与耳机的搭配也是当代年轻人喜爱的行为方式&#xff0c;在颠簸的运动项目中耳机的稳固性和舒适性是非常主要的&#xff0c;现在新推出的开放式耳机深…

Ps:移除工具

移除工具 Remove Tool是一款简单易用、功能强大的工具&#xff0c;可快速、轻松地清去除图片中的干扰元素或不需要的区域。 快捷键&#xff1a;J 就如同使用画笔工具一样&#xff0c;只要在不需要的对象上涂抹&#xff08;描边&#xff09;即可将其去除。 移动工具基于人工智能…

几号发工资就能看出公司的好坏?(文末附招聘岗位)

作为一名资深的职场搬砖人&#xff0c;不知道各位最近有没有跟我一样关注到这个话题 ​#发工资时间看公司#小编刚看到这个话题的第一印象&#xff0c;想的是发工资时间无非是公司实力的体现&#xff0c;工资发的越早证明这个公司的现金流越稳定强大。 打开评论区&#xff0c;不…

Linux——进程优先级

1.什么是优先级&#xff1f; 优先级和权限息息相关。权限的含义为能还是不能做这件事。而优先级则表示&#xff1a;你有权限去做&#xff0c;只不过是先去做还是后去做这件事罢了。 2.为什么会存在优先级&#xff1f; 优先级表明了狼多肉少的理念&#xff0c;举个例子&#xff…

Processing通过编程实现艺术设计_实现艺术和现实的交互---数据设计分析002

还记得这个生命的游戏,也是在这上面有 https://processing.org/ 官网是这个 使用Processing可以用编程的方式来创作艺术 Processing是一门开源编程语言,可以直接对用它来做艺术创作, 可以看一些它的作品 https://processing.org/examples/gameoflife.html 官网是这个,完…

作为C/C++程序员你可以不使用但你必须会的Linux调试器-gdb(GNU Debugger)

gdb(GNU Debugger) 是一个用于调试 Linux 系统的软件工具。在学习 Linux 的过程中&#xff0c;gdb 的重要性不言而喻。以下是 gdb 在 Linux 学习者中的重要性的详细说明: 帮助理解 Linux 系统的运作方式:gdb 是一个强大的调试工具&#xff0c;可以帮助学习者深入了解 Linux 系统…

代码随想录算法训练营day53 | 1143.最长公共子序列,1035.不相交的线,53. 最大子序和 动态规划

代码随想录算法训练营day53 | 1143.最长公共子序列&#xff0c;1035.不相交的线&#xff0c;53. 最大子序和 动态规划 1143.最长公共子序列解法一&#xff1a;动态规划 1035.不相交的线解法一&#xff1a;动态规划 53. 最大子序和 动态规划解法一&#xff1a;动态规划解法二&am…

Python学习38:凯撒密码——解密

类型&#xff1a;字符串‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‭‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬ 描述‪‬‪…

【Netty】Netty 编码器(十三)

文章目录 前言一、MessageToByteEncoder 抽象类二、MessageToMessageEncoder 抽象类总结 前言 回顾Netty系列文章&#xff1a; Netty 概述&#xff08;一&#xff09;Netty 架构设计&#xff08;二&#xff09;Netty Channel 概述&#xff08;三&#xff09;Netty ChannelHan…

notepad++查询指定内容并复制

背景说明 记录一下使用notepad进行文本内容查找以及替换的相关场景,简单记录方便后期查看,场景内容: 1.从指定的给出内容中筛选出所有的人员id集合 2.将每一行后面添加逗号 1.从指定的给出内容中筛选出所有的人员id集合 要求从指定的给出内容中筛选出所有的人员id集…

便携式明渠流量计的使用特点

便携式明渠流量计使用特点&#xff1a; 便携式明渠流量计使用特点&#xff0c;首先了解相关要求&#xff1a; 随着新标准JJG711-1990《明渠堰槽流量计试行检定规程》、HJ/T15-2019《超声波明渠污水流量计技术要求及检测方法》、HJ 354-2019《水污染源在线监测系统(CODCr、NH3-N…

Java开发 - 你不知道的JVM优化详解

前言 代码上的优化达到一定程度&#xff0c;再想提高系统的性能就很难了&#xff0c;这时候&#xff0c;优秀的程序猿往往会从JVM入手来进行系统的优化。但话说回来&#xff0c;JVM方面的优化也是比较危险的&#xff0c;如果单单从测试服务器来优化JVM是没有太大的意义的&…

制作PE工具盘

文章目录 前言一、什么是PE工具箱&#xff1f;二、制作WinPE工具箱例&#xff1a;制作ISO模式的PE工具箱 三、PE工具箱的典型应用1.清除Windows登陆密码2.调整分区大小3.系统备份4.系统恢复例&#xff1a;系统备份与恢复 四、使用U深度制作的PE工具恢复误删除的文件实验步骤注意…

springboot+vue车辆充电桩管理系统(java项目源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的车辆充电桩管理系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 &#x1f495;&#x1f495;作者&#xff1a;…

Vim的使用

1.什么是Vim Vim是Linux下的一款代码编辑器&#xff0c;vi/vim的区别简单点来说&#xff0c;它们都是多模式编辑器&#xff0c;不同的是vim是vi的升级版本&#xff0c;它不仅兼容vi的所有指令&#xff0c;而且还有一些新的特性在里面。例如语法加亮&#xff0c;可视化操作不仅…

Javascript中常用方法

1.操作元素的类&#xff1a; 元素.classList.add(类名) 元素.classList.remove(类名) 元素.classList.toggle(类名) //切换类名 2.操作表单元素属性&#xff1a; 表单.value用户名 表单.typepassworddisabled checked selected 3.自定义属性&#xff1a; 定义&#xff1a…

小程序极速注册认证免300认证费 突破管理员身份只能绑定5个小程序绿色通道

小程序注册账号管理相信大家有时候头都很大&#xff0c;一个身份证号只能绑定5个小程序主管理员。超个5个小程序就得请朋友、请亲戚身份证绑定管理员。有时还得看对方不方便&#xff0c;改个类目改个LOGO都还得管理员扫码。为了满足会员需求同是也满足自己需要&#xff0c;特别…

C语言初阶 牛客网刷题笔记(将持续更新..)

BC7 缩短二进制 printf 格式控制符 “%o” —— 八进制整数 、“%x” —— 十六进制整数 &#xff1b; 修饰符 “#” —— 控制前导显示 BC64 K形图案 错因&#xff1a;把图形分成两部分&#xff0c;下半部分打印错误 先把下半部分作为一个完整三角形&#xff1a; int n0;scan…