【C++】右值引用和移动语义(详细解析)

news2024/11/27 17:52:04

文章目录

  • 1.左值引用和右值引用
    • 左值引用
    • 右值引用
  • 2.左值引用和右值引用的比较
    • 左值引用总结
    • 右值引用总结
  • 3.右值引用的使用场景和意义
    • 知识点1
    • 知识点2
    • 知识点3
    • 知识点4
    • 总结
  • 4.完美转发
    • 万能引用
    • 见识完美转发的使用
    • 完美转发的使用场景


1.左值引用和右值引用

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

左值引用

  1. 左值是一个表示数据的表达式(如变量名或解引用的指针);
  2. 我们可以获取它的地址;
  3. 我们可以对它赋值;
  4. 左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边;
  5. 定义const修饰符后的左值,不能给它赋值,但是可以取它的地址;
  6. 左值引用就是给左值引用,即给左值取别名。

使用方法:

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

int main()
{
	//以下的p,b,c,*p都是左值
	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;

	return 0;
}

右值引用

  1. 右值也是一个表示数据的表达式(如字面常量,表达式返回值,函数返回值等等);
  2. 右值引用可以出现在赋值符号的右边,但是不能出现在赋值符号的左边
  3. 右值不能取地址。
  4. 右值就是对右值的引用,给右值取别名。
int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);
	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;
	
	return 0;
}

一定要注意:右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说,我们本来不能取字面量10的地址,但是rr1引用后,导致我们可以对rr1取地址了,也可以修改rr1,这个时候rr1就变成左值了。如果不想rr1被修改,可以用const int&& rr1去引用,为什么用const修饰之后就可以了呢?因为——x+y这个表达式的返回值是临时变量,临时变量具有常性,所以用const修饰之后就不会报错了。

int main()
{
	double x = 1.1, y = 2.2;
	x + y;
	10;
	int&& rr1 = 10;
	const double&& rr2 = x + y;

	rr1 = 20;
	//rr2 = 5.5; //报错

	return 0;
}

2.左值引用和右值引用的比较

注意两点:

  • 能不能取地址是区分左值引用和右值引用的主要区别。
  • 不能认为有分配空间的就是左值引用,没有被分配空间的就是右值引用。因为在上述的例子中,x+y表达式是右值,但是实际上它是占用空间的,表达式的返回值存储在临时变量中。

左值引用总结

  • 左值引用只能引用左值,不能引用右值
  • 但是const修饰的左值引用可以给右值取别名,也可以引用左值
int main()
{
	//左值
	int a = 10;
	//左值引用可以引用左值
	int& ra1 = a;
	//左值引用不能引用右值
	int& ra2 = 10;//编译失败,因为10常量是右值

	//const修饰的左值引用可以引用左值,也可以引用右值
	const int& ra3 = a;
	const int& ra4 = 10;
	
	return 0;
}

右值引用总结

  • 右值引用只能给右值取别名,不能引用左值
  • 但是右值引用可以给move后的左值取别名
int main()
{
	int a = 0;
	int b = 1;
	int* p = &a;

	a + b;

	//右值引用可以给右值取别名
	int&& raf1 = 10;
	int&& raf2 = a + b;

	//右值引用可以给move后的左值取别名
	int&& raf3 = a;//报错:无法将右值引用绑定到左值
	int&& raf3 = std::move(a);
}

3.右值引用的使用场景和意义

出现右值的原因之一:可以把左值和右值区分开。
我们来看以下代码,函数名都为func,但是参数不同,一个是左值引用,一个是右值引用,我们来看看能否运行成功,将左值和右值区分开。

#include<iostream>
#include<vector>
using namespace std;
void func(int& a)
{
	cout << "void func(int& a)" << endl;
}
void func(int&& a)
{
	cout << "void func(int&& a)" << endl;
}
int main()
{
	int a = 0;
	int b = 1;
	func(a);//这里a是左值
	func(a + b);//这里a+b是右值

	return 0;
}

运行结果:OK,编译通过,运行正确。所以虽然函数名相同,但是由于参数不同,调用的函数也不同。
最重要的是我们把左值和右值区分出来了。
在这里插入图片描述

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!

左值引用存在的短板:

之前我们学习过 左值引用可以直接减少拷贝。

  1. 左值引用传参;
  2. 传引用返回。

但是如果函数内是局部对象,局部对象出了函数作用域那块空间就销毁了,这种情况下是不能用引用返回的。 对引用还不太理解的宝子可以先看这篇文章:【C++】引用&详细解析

右值引用如何解决左值引用存在的短板?

对于右值,有些书上又将其分为纯右值和将亡值。纯右值一般是内置类型,将亡值一般是自定义类型。如果右值将亡了,还对它进行深拷贝代价是有点高的,所以对于右值(将亡值),我们采用的是资源转移,即不重新开辟空间。上图!
在这里插入图片描述
接下来,我们来看几个例子,这样能更加理解左值引用和右值引用。

知识点1

例一:这里采用的是自定义string类。便于调试观察每个步骤调用的是什么函数。

在这里插入图片描述

知识点2

例二:下面是一个可以将整型转换成字符串的函数——注意是传值返回。同样用的是自定义的string类,这样调试时便于观察每个步骤调用的是哪个函数。

namespace nan
{
	nan::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}
		nan::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;
	}
}
int main()
{
	// 在nan::string to_string(int value)函数中可以看到,这里
	// 只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷
	//贝构造)。
	nan::string ret1 = nan::to_string(1234);
	return 0;
}

在这里插入图片描述

在这里插入图片描述
解释例2:返回时,由于编译器做了优化,将返回值(左值)通过某种方式转变成了右值(将亡值),所以直接采用了移动构造,即先调用了移动构造函数,然后才调用了析构函数。、

知识点3

move是库里面的函数,如果右值想引用左值,用上move函数即可。但是move函数不能随便乱用哦,大家看下面这个代码,先构造了s1,然后用s1拷贝构造s2,s1是左值,第3句代码想将s1通过move函数转换成右值,进行移动构造。
但是从运行结果我们发现,s1的家竟被s3偷了,所以一定要辨别使用场景,不能乱用move函数。

在这里插入图片描述
注意:move函数不是将s1转换成右值,而是move函数的返回值是右值。
这样写就不会被偷家了:
在这里插入图片描述

知识点4

C++11以后,STL所有的容器都增加了移动构造:
在这里插入图片描述
并且STL所有的容器的插入数据接口都增加了右值引用版本。
在这里插入图片描述
那么有什么意义呢?
在这里插入图片描述
第一种尾插方式采用的是拷贝构造函数——深拷贝,所以会再拷贝出一份s1,然后插入链表中,通过下面的调试窗口,可以看出原来的字符串s1还存在。
在这里插入图片描述
在这里插入图片描述
第二种尾插方式采用的是移动构造——浅拷贝,直接转移资源,将前面深拷贝构造出来的s1直接拿来尾插,不需要再深拷贝构造一个s1,又大大提高了效率。但是,通过调试窗口发现,s1被偷家了,悬空了!所以要注意一下这点。
在这里插入图片描述

总结

问题:匿名对象属于左值还是右值?——答案:匿名对象是右值。

在这里插入图片描述
总结:

  1. C++98,只有拷贝构造;C++11之后,既有拷贝构造,也有移动构造;
  2. 左值引用减少拷贝,提高效率,右值引用也是减少拷贝,提高效率,但是它们的角度不同,左值引用是直接减少拷贝,右值引用是间接减少拷贝,编译器先识别数据是左值还是右值,如果是右值,则不再深拷贝,直接移动拷贝(直接移动资源),提高效率。

4.完美转发

万能引用

模板中的&& , 不代表右值引用,而是万能引用,其既能接收左值又能接收右值。

栗子如下:

template<class T>
void PerfectForward(T&& t)
{
	//...
}

然后我们现在通过下列代码,观察一个现象:

下面重载了四个Func函数,这四个Func函数的参数类型分别是左值引用、const左值引用、右值引用和const右值引用。在主函数中调用PerfectForward函数时分别传入左值、右值、const左值和const右值,在PerfectForward函数中再调用Func函数。如下:

#include<iostream>
using namespace std;

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;
}

运行结果:
在这里插入图片描述
由于PerfectForward函数的参数类型是万能引用,因此既可以接收左值也可以接收右值,而我们在PerfectForward函数中调用Func函数,就是希望调用PerfectForward函数时传入左值、右值、const左值、const右值,能够匹配到对应版本的Func函数。但实际调用PerfectForward函数时传入左值和右值,最终都匹配到了左值引用版本的Func函数,调用PerfectForward函数时传入const左值和const右值,最终都匹配到了const左值引用版本的Func函数。

在这里插入图片描述

造成这种结果的原因:

根本原因就是:右值被引用后会导致右值被存储到特定位置,这时这个右值可以被取到地址,并且可以被修改,所以在PerfectForward函数中调用Func函数时会将t识别成左值。因为右值默认具有常性,右值引用后属性是左值,这样才能实现资源转移。
在这里插入图片描述

也就是说,右值经过一次参数传递后其属性会退化成左值,如果想要在这个过程中保持右值的属性,就需要用到完美转发

见识完美转发的使用

保持原有属性的关键:要想在参数传递过程中保持其原有的属性,需要在传参时调用std::forward函数。

#include<iostream>
using namespace std;

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)
{
	// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
	Fun(forward<T>(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;
}

运行结果:
在这里插入图片描述

完美转发的使用场景

下面模拟实现了一个简化版的list类,类当中分别提供了左值引用版本和右值引用版本的push_back和insert函数。

namespace zxn
{
	template<class T>
	struct ListNode
	{
		T _data;
		ListNode* _next = nullptr;
		ListNode* _prev = nullptr;
	};
	template<class T>
	class list
	{
		typedef ListNode<T> node;
	public:
		//构造函数
		list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}
		//左值引用版本的push_back
		void push_back(const T& x)
		{
			insert(_head, x);
		}
		//右值引用版本的push_back
		void push_back(T&& x)
		{
			insert(_head, std::forward<T>(x)); //完美转发
		}
		//左值引用版本的insert
		void insert(node* pos, const T& x)
		{
			node* prev = pos->_prev;
			node* newnode = new node;
			newnode->_data = x;

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
		//右值引用版本的insert
		void insert(node* pos, T&& x)
		{
			node* prev = pos->_prev;
			node* newnode = new node;
			newnode->_data = std::forward<T>(x); //完美转发

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
	private:
		node* _head; //指向链表头结点的指针
	};
}

在这里插入图片描述
如果我们没有使用std::forward完美转发,会导致运行结果全是拷贝构造函数,即深拷贝。

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

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

相关文章

【C++】引用 - 基本语法,注意事项,函数参数,函数返回值,本质

文章目录 1. 引用的基本语法2. 引用的注意事项3. 引用做函数参数4. 引用做函数返回值5. 引用的本质6. 常量引用 1. 引用的基本语法 作用是&#xff1a;给变量起别名 语法&#xff1a;数据类型 &别名 原名 2. 引用的注意事项 引用必须初始化引用在初始化后&#xff0c;不…

量子计算:基本概念

选了课程 《量子计算与量子信息》&#xff0c;没学过量子力学的博主实在是听不懂啊 (ㄒoㄒ) 简略整理了下 可能大概也许 明白一二都没有 的课程最开始两节的内容&#xff0c;如有错误欢迎指出 ~ ~ ~ 文章目录 矩阵论复空间中的矩阵矩阵上的运算 量子力学量子态基本假设 量子计算…

阿里云的内容识别技术可以实现哪些场景下的智能化应用?

阿里云的内容识别技术可以实现哪些场景下的智能化应用&#xff1f; [本文由阿里云代理商[聚搜云]撰写]   随着人工智能技术的快速发展&#xff0c;阿里云借助自身的技术和资源优势&#xff0c;开发了一种名为“内容识别”的技术。这项技术能够高效、准确地识别出图片、视频、…

有个规划文档,会让软件开发更有效

有个规划文档&#xff0c;会让软件开发更有效 中小企业&#xff0c;业务部门不太清楚软件生产过程 软件生产有一定的抽象和复杂性 要形成一个共识 趣讲大白话&#xff1a;要有点整体观 【趣讲信息科技181期】 **************************** 2019年整理出了一个目录框架 用在很多…

windows的cmd命令窗口介绍

1.打开cmd 1.1.方式一 左下角搜索&#xff1a;“运行” -> 打开 输入"cmd" -> 确定 1.2.方式二 直接使用快捷键 windows r 即可打开 然后输入cmd&#xff0c;点击确认 1.3.方式三 打开文件管理器&#xff0c;输入cmd&#xff0c;回车 即可在该文件路径下…

统计软件与数据分析Lesson16----pytorch基本知识及模型构建

统计软件与数据分析Lesson16----pytorch基本知识及模型构建 0.上节回顾0.1 一元线性回归数据生成数据处理初始数据可视化 0.2 梯度下降Gradient DescentStep 0: 随机初始化 Random InitializationStep 1: 计算模型预测值 Compute Models PredictionsStep 2: 计算损失 Compute t…

让进程能够“相互沟通”的高级方式一:匿名管道

代码运行及测试环境&#xff1a;linux centos7.6 在阅读这篇文章时&#xff0c;需要掌握OS对文件管理的基础知识&#xff08;文件打开表、文件描述符、索引结点…&#xff09; 前言 我们都知道进程是具有独立性的&#xff0c;意味着进程之间无法相互通信。但在一些情况下&…

当我们谈笔记的时候,我们在谈什么

文章具体内容如图&#xff0c;感谢妙友分享好文&#x1f389; 本篇内容来源于网站Untag Minja 上传的内容《当我们谈笔记的时候&#xff0c;我们在谈什么》 如有侵权请联系删除&#xff01;

如何搭建与使用FTP服务器

一、概述 目前用作搭建FTP服务器端的软件有很多&#xff0c;比如 Vsftpd、ProFTPD、PureFTPd、Wuftpd、ServerU、Filezilla Server等&#xff0c;这里使用Vsftpd进行搭建。 vsftpd 是“very secure ftp daemon”的首字母缩写&#xff0c;它是一款在Linux发行版中最受推崇的免…

应用到vscode

随着ChatGPT的热度持续上升&#xff0c;我们也不得不深刻认识到&#xff0c;这已经势不可挡了。我们必须去接受它&#xff0c;甚至是拥抱它。 私信我可以获取最新包 今天呢&#xff0c;我们要介绍的是vscode的一款插件&#xff0c;叫做ChatGPT&#xff1a; 使用方式 安装完成…

Xshell安装使用教程

简介 Xshell 是一个强大的安全终端模拟软件&#xff0c;它支持SSH1, SSH2, 以及Microsoft Windows 平台的TELNET 协议。Xshell 通过互联网到远程主机的安全连接以及它创新性的设计和特色帮助用户在复杂的网络环境中享受他们的工作。 Xshell可以在Windows界面下用来访问远端不…

【C#图解教程】第三章 C#编程概述 笔记总结

程序实例 命名空间一组类型声明 using system&#xff1b;表示使用类型库中的system中的所有类 namespace Myspace{}则会创建一个新的命名空间&#xff0c;这个空间中可以声明该空间的类 SimpleProgram中使用了两个命名空间。先是使用了System命名空间中定义的console类&…

【Linux】项目自动化构建工具-make和Makefile 的使用和进度条的实现

文章目录 一、什么是make/makefile二、如何编写makefile三、make 的工作原理1.make的使用2.make的依赖性3.项目清理4..PHONY伪目标 四、Linux第一个小程序 -- 进度条1.\r&&\n2.行缓冲区概念3.进度条process.hprocesstest.cmakefile 一、什么是make/makefile 什么是mak…

【c语言习题】使用链表解决约瑟夫问题

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c语言系列专栏&#xff1a;c语言之路重点知识整合 &#x…

在 Shell 脚本中使用 `exec` 命令的方法和用途

在 Shell 脚本中&#xff0c;exec 是一个非常有用的命令&#xff0c;用于执行命令或替换当前进程。它允许您在脚本中启动新的进程&#xff0c;并将控制权完全转移到新进程。本文将详细介绍在 Shell 脚本中使用 exec 命令的方法和用途。 什么是 Exec 命令&#xff1f; exec 是一…

python基础----02-----字面量、变量、数据类型及其转换、标识符以及字符串、数据输入(input语句)

一 字面量 1 字面量 字面量&#xff1a;在代码中&#xff0c;被写下来的的固定的值称之为字面量。类似C/C的字符串常量。 1.1 常见的python值类型 Python中常见的值类型&#xff1a; 实际上在C/C字面量和这里的类型还是有区别的&#xff0c;体现在内存存储中&#xff0c;字…

JavaScript正则表达式

1.介绍 2.语法 3.元字符 4.修饰符 目标&#xff1a;学习正则表达式概念及语法&#xff0c;编写简单的正则表达式实现字符的查找或检测。 一、介绍 1.什么是正则表达式 正则表达式&#xff08;Regular Expression&#xff09;是用于匹配字符串中字符组合的模式。&#xff08;…

设计模式之~原型模式

定义&#xff1a;用原型实例指导创建对象的种类&#xff0c;并且通过拷贝这些原型创建新的对象。原型模式其实就是从一个对象再创建另外一个可定制的对象&#xff0c;而且不需知道任何创建的细节。 优点&#xff1a; 一般在初始化的信息不发生变化的情况下&#xff0c;克隆是最…

触摸屏驱动的问题,在C站搜文章一下午解决不了,最后ChatGpt解决了

目录 一、遇到问题二、在C站搜索文章去解决问题的收获三、用 ChatGpt 去解决的收获四、总结 一、遇到问题 现实问题&#xff1a; 有一个基于Linux4.19内核开发了&#xff0c;在海思SS528芯片运行的系统&#xff0c;用鼠标可以正常使用。 现在要求使用一块公司开发的 多点触控屏…

ES+Redis+MySQL,这个高可用架构设计

一、背景 会员系统是一种基础系统&#xff0c;跟公司所有业务线的下单主流程密切相关。如果会员系统出故障&#xff0c;会导致用户无法下单&#xff0c;影响范围是全公司所有业务线。所以&#xff0c;会员系统必须保证高性能、高可用&#xff0c;提供稳定、高效的基础服务。 …