C++11右值引用

文章目录

  • 左值
    • 左值引用
  • 右值
  • 右值引用
  • 左值引用和右值引用
    • 左值引用和右值引用总结
  • 右值引用使用场景和意义
    • 左值引用的使用场景
    • 左值引用的缺点
    • 右值引用
      • 移动构造
      • 移动赋值
    • 右值引用的其他使用场景
  • 万能引用
    • 完美转发
    • 完美转发的实际应用场景

C++11之前就有了引用的语法,而C++11中新增了的右值引用语法特性,所以在C++11之前的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
首先认识一下左值和右值,在来认识左值引用和右值引用。

左值

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,也可以出现在赋值符号的右边。

比如下面a,p,*p,b变量都是左值:

int a = 0;
int* p = new int(1);
const int b = 0;

左值引用

引用就是给变量取别名,左值引用就是给左值取别名。
左值引用符号&
比如下面refa,refp,pvalue,refb都是左值引用

//a,p,b都是左值
int a = 0;
int* p = new int(1);
const int b = 0;

//refa,refp,pvalue,refb都是左值引用
int& refa = a;//左值a的引用
int*& refp = p;//左值p的引用
int& pvalue = *p;//左值*p的引用
const int& refb = b;//左值b的引用

更多关于引用的知识在之前C++入门的博客中有详细的介绍C++入门。
这篇文章主要介绍C++11新引入的右值引用

右值

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(临时对象)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址
比如下面10,a+b,fun(a+b)都是右值。

int fun(int a)
{
	return a;
}
int main()
{
	//a,p,b都是左值
	int a = 0;
	int* p = new int(1);
	const int b = 0;

	//10,a+b,fun(a+b)都是右值
	10;
	a + b;
	fun(a + b);
	return 0;
}

注意右值不能出现在=左边,
比如上面的右值出现在=左边时:

a + b = 1;//error编译错误,"="左边的操作数必须是左值

右值引用

右值引用就是对右值的引用,给右值取别名。
右值引用符号&&
比如下面ref1,ref2,ref3都是右值引用

//10,a+b,fun(a+b)都是右值
10;
a + b;
fun(a + b);

//ref1,ref2,ref3都是右值引用
int&& ref1 = 10;//右值10的引用
int&& ref2 = a + b;//右值a+b的引用
int&& ref3 = fun(a + b);//右值fun(a+b)的引用

左值引用和右值引用

左值引用可以引用右值吗?右值引用可以引用左值吗?

先说结论:可以。
右值是一些字面量和一些表达式和函数返回值的临时对象,而临时对象具有常性。所以const左值引用也可引用右值。
比如下面ref就是左值引用对右值的引用:

int fun(int a)
{
	return a;
}
int main()
{
	int a = 1, b = 1;
	//右值 这里的fun(a+b)是临时变量,生命周期只有自己所在的这一行
	fun(a + b);
	//左值引用引用右值
	const int& ref = fun(a + b);//这里因为临时变量具有常性,所以要加const才可以。
}

右值引用也可以引用左值。
比如下面ref就是右值引用对左值的引用:

int main()
{
	//左值a
	int a = 0;
	//右值引用引用左值,必须使用move函数来完成
	int&& ref = move(a);
	return 0;
}

关于move函数简单的认为就是右值引用引用左值必须要使用的。

左值引用和右值引用总结

  • 左值引用

    • 左值引用只能引用左值,不能引用右值
    • const左值既可以引用左值,也可以引用右值
  • 右值引用

    • 右值引用只能引用右值,不能引用左值
    • 但是右值引用可以move以后的左值

右值引用使用场景和意义

正片开始,上面主要介绍了左值引用和右值引用,通过前面可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?下面就来看看左值引用的短板,右值引用是如何补齐这个短板的!
更好的观察到现象,这里提供一个简易版的stirng类,通过控制台输出信息更好的观察程序运行情况和结果。

namespace ding
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		// 默认构造
		string(const char* str = "")
			:_size(strlen(str))
			,_capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		//拷贝构造
		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);
		}
		// 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动语义" << endl;
			swap(s);
			return *this;
		}
		string operator=(const string& s)
		{
			string tmp(s); 
			swap(tmp); 
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}
		void swap( string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

左值引用的使用场景

介绍左值引用的短板的时候,先看一下左值引用的使用场景。

  • 做函数参数和返回值都可以提高效率。
    比如:
    下面代码全部会调用上面的string类,可以打印信息,方便观察结果。
void fun1(ding::string str)
{}
void fun2(ding::string& str)//左值引用做函数参数
{}
int main()
{
	ding::string s1("hello world");
	fun1(s1);

	fun2(s1);
	return 0;
}

运行结果:

image.png

  • 对于fun1函数来说,是一个左值做函数参数,main函数中的s1传给fun1函数参数str时,会自动调用拷贝构造函数,生成一份s1传给str。mian函数中的s1对象和fun1函数参数str是两块不同的地址空间。在fun1函数中修改str不会影响到main函数中的s1对象。
  • 对于fun2函数来说,是一个左值引用左函数参数,fun2函数中的str就是对main函数中的s1取别名,他俩是同一块地址空间。在fun2函数中修改str对象会影响到mian函数中的s1对象。
  • 可以看出,左值引用做函数参数时,会减少拷贝,如果一个函数中相对外部对象做修改,那么传引用效率会更高,比传指针还高。因为指针还要占用4个字节大小空间。当然4个字节的空间不是很多,但是引用总比指针操作简单。

左值引用的缺点

左值引用做返回值,并不能完全避免函数返回对象时的拷贝。
当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回。这就是左值引用的短板。
下面用一个例子来说明:

ding::string fun()
{
	ding::string str;//函数局部对象
	return str;
}

int main()
{
	ding::string s1 = fun();
	return 0;
}

主函数调用fun函数,fun函数里面的局部变量做返回值,这里不能使用传引用返回,因为str是一个局部变量,出了这个函数后就被销毁了。只能使用传值返回,传值返回一定会在str析构之前调用拷贝构造函数来生成一份临时对象,然后临时对象在调用拷贝构造赋值给主函数的s1对象。

image.png
临时对象具有常性,cosnt左值引用可以引用,而拷贝构造函数的参数就是const左值引用类型。这里就会调用拷贝构造来完成。而拷贝构造函数又是一次深拷贝。这里编译器会优化,本来两次的拷贝构造编译器直接优化成了一次拷贝构造来完成。

这里还不能使用左值引用左返回值,只能使用值传递,值传递又会导致一次拷贝构造,C++11引入了右值引用来解决这一问题。

运行结果如下:

(这里我用的是vs2017专业版的,如果是新一点的编译器会优化,比如22版,会优化,一次调用也没有。如果是老一点的编译器,可能会调用两次。)
image.png
在Linux平台下使用g++(4.8.5)编译也是会优化,一次都不会调用。
下面的测试环境都在vs2017专业版下面进行测试了。

右值引用

移动构造

右值引用解决上面的问题就是给string类提供一个移动构造。
函数如下:

//移动构造(右值引用做为函数参数)
string(string&& s)
        :_str(nullptr)
{
        cout << "string(const string&& s) --- 移动拷贝" << endl;
        swap(s);
}

移动构造函数不再调用构造函数取初始化,不涉及资源申请,直接交换两个对象即可。效率会比拷贝构造更高。
此时同样的代码,运行结果如下:

image.png

此时编译器会调用移动构造,来完成资源的移动,而不是像深拷贝一样,释放资源之前先拷贝一份临时资源在进行释放。效率更高。移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不 用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

移动赋值

不仅仅有移动构造,还有移动赋值,他们的本质都是一样的,不再申请新的资源,直接将之前的资源窃取过来,不用在做深拷贝,提高了效率。移动赋值函数如下:

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

比如下面程序就会调用移动赋值

ding::string fun()
{
	ding::string str;
	return str;
}

int main()
{
	
	ding::string s1;
	s1 = fun();//调用移动赋值
	return 0;
}

运行结果:
image.png
如果不提供移动赋值,就会去调用赋值运算完成深拷贝。大大的提高了效率。

在C++11之后,STL库中的容器都支持了移动构造和移动赋值,
比如string类

image.png

image.png

总结:
通过上面的例子可以看出,cosnt左值引用去引用右值也是有价值的,比如没有移动构造的时候,右值析构之前会调用拷贝构造函数来完成资源的保存,但是会重新申请空间保存资源。在C++11之后,有了右值引用之后,右值直接转移资源,不再涉及资源申请的问题。提高了效率。这也是左值引用无法解决的问题。

右值引用的其他使用场景

C++11更新了右值引用之后,STL库中除了增加移动赋值和移动构造,有些插入函数还新增了右值引用版本。比如
list的push_back接口

image.png
下面就研究一下 右值引用做为插入函数接口参数的意义。
当一个链表中存放的是上面自己模拟实现简易的sting类时,向list中push_back元素:如下

int main()
{
	ding::string s1("Hello World");
	list<ding::string> l1;
	//调用string的拷贝构造(深拷贝)
	l1.push_back(s1);
	//调用string的移动构造
	l1.push_back("xxx");
	//调用string的移动构造
	l1.push_back(ding::string("xxxx"));
	//调用string的移动构造
	l1.push_back(move(s1));
	return 0;
}

上面代码中,s1是左值,会调用左值引用版本的push_back();val就是左值引用,push_back时就会调用拷贝构造来构造string对象。此时val就是深拷贝。
后面的三个push_back传的都是右值,会去调用右值版本的push_back。val就是右值引用。调用移动构造来完成结点的插入。这样效率就会提高很多。

万能引用

模板中的万能引用。
在模板中&&符号是万能引用,即可以当做右值引用,也可以当做左值引用。
比如:

template<class T>
void fun(T&& data)
{
	//....
}

函数参数data既不是左值引用也不是右值引用,而是万能引用。模板的万能引用只提供了同时接收左值和右值的能力,但是引用类型唯一作用就是限定了接收的类型,后续使用中都退化成了左值。比如上面fun函数中的data参数,万能引用只提供了同时接收左值和右值的功能,但是在函数体内后续使用data,data都只是左值。如果想让data继续保持原有属性,就要用到完美转发。

完美转发

完美转发主要解决的是模板中万能引用后退化成左值的问题。
比如下面代码

template<class T>
void Func(T& data)
{
	cout << "左值引用" << endl;
}
template<class T>
void Func(const T& data)
{
	cout << "const 左值引用" << endl;
}
template<class T>
void Func(T&& data)
{
	cout << "右值引用" << endl;
}
template<class T>
void Func(const T&& data)
{
	cout << "const 右值引用" << endl;
}
template<class T>
void PerfectForward(T&& data)
{
	Func(data);
}

int main()
{
	double a = 0;
	PerfectForward(a);//左值

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

	PerfectForward(10);//右值

	PerfectForward(move(b));//const 右值

	return 0;
}

运行结果:

image.png
可以发现,全部调用左值和const左值的函数了,而右值也调用左值版本的Func函数了。
这个原因是因为先调用PerfectForward函数,在PerfectForward函数中在调用Func函数。
而在PerfectForward函数中,向data传的不论是左值还是右值,data在后续使用过程中都是左值,所以上面程序运行的结果全是左值版本的Func函数。
如果想让data继续保持原有的属性,解决方式如下

template<class T>
void PerfectForward(T&& data)
{
	Func(forward<T>(data));
}

在PerfectForward函数体内将data完美转发,保持其原有的属性
此时运行结果如下:
image.png

完美转发的实际应用场景

简易实现一个STL库中的list,提供push_back的右值引用版本

namespace ding
{
	template<class T>
	struct ListNode
	{
		
		ListNode(const T& data = T()) 
			:_next(nullptr)
			,_prev(nullptr) 
			,_data(data)
		{}
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;
	};
	template<class T>
	class list
	{
		typedef ListNode<T> Node;
	public:
		list()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}
		~list()
		{
			Node* cur = _head->_next;
			while (cur != _head)
			{
				Node* next = cur->_next;
				delete cur;
				cur = next;
			}
			delete _head;
			_head = nullptr;
		}
		//左值引用版本insert
		void Insert(Node* pos,const T& data)
		{
			cout << "void Insert(Node* pos,const T& data)" << endl;
			Node* newnode = new Node(data);
			Node* prev = pos->_prev;
			prev->_next = newnode;
			newnode->_next = pos;
			newnode->_prev = prev;
			pos->_prev = newnode;
		}
		//右值引用版本insert
		void Insert(Node* pos, const T&& data)
		{
			cout << "void Insert(Node* pos, const T&& data)" << endl;
			Node* newnode = new Node(data);
			Node* prev = pos->_prev;
			prev->_next = newnode;
			newnode->_next = pos;
			newnode->_prev = prev;
			pos->_prev = newnode;
		}
		//左值版尾插
		void Push_back(T& data)
		{
			Insert(_head,data);
		}
		//右值版尾插
		void Push_back(T&& data)
		{
			Insert(_head, forward<T>(data));
		}
	private:
		Node* _head;
	};
}

list简单实现了一个尾插功能,并且提供了左值版和右值版的尾插,尾插调用insert函数复用。

int main()
{
	ding::list<int> ls;
	int a = 0;
	//左值
	ls.Push_back(a);
	//右值
	ls.Push_back(1);
	ls.Push_back(2);
	ls.Push_back(3);
	ls.Push_back(4);
	return 0;
}

对于上面代码,除了第6行会调用左值版的push_back,其余的按理来说会调用右值版的push_back。然后右值版的push_back再调用右值版的insert。但是运行结果如下:

image.png
结果是右值版的也调用了左值版的insert。原因上面已经说过了。解决方式就是用完美转发。右值版的push_back修改如下:

void Push_back(T&& data)
{
    //Insert(_head, data);
    Insert(_head, forward<T>(data));
}

右值版的insert不用修改,如果修改完后,insert中的new就无法调用ListNode的构造函数了。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Rust升级慢,使用国内镜像进行加速

背景 rustup 是 Rust 官方的跨平台 Rust 安装工具&#xff0c;国内用户使用rustup update的时候&#xff0c;网速非常慢&#xff0c;可以使用国内的阿里云镜像源来进行加速 0x01 配置方法 1. Linux与Mac OS用户配置环境变量 修改~/.bash_profile文件添加如下内容&#xff1…

Three.js-04轨道控制器

1.导入 说明&#xff1a;相机围绕目标进行轨道运动。也就是可以通过鼠标拖拽进行移动视角。 import { OrbitControls } from three/addons/controls/OrbitControls.js; 2.使用 说明&#xff1a;构造controls对象&#xff0c;再调用update方法&#xff1b;为了使效果更为明显…

【数据结构与算法】常用算法 前缀和

&#x1f389;&#x1f389;欢迎光临&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;特别推荐给大家我的最新专栏《数据结构与算法&#xff1a;初学者入门指南》&#x1f4d8;&am…

刘知远LLM——Transformer与预训练模型

文章目录 注意力机制原理介绍注意力机制的各种变式注意力机制的特点 Transformer结构概述Transformer整体结构 输入层byte pair encodingpositional encoding Transformer BlockEncoder BlockMulti-Head Attention Decoder Block其他tricks总结 预训练语言模型语言建模概述预训…

DP读书:《半导体物理学(第八版)》(一)绪论 3min速通

DP读书&#xff1a;《半导体物理学&#xff08;第八版&#xff09;》刘恩科 3min速通半导体物理之绪论 DP读书&#xff1a;《半导体物理学&#xff08;第八版&#xff09;》刘恩科绪论第一章 半导体中的电子状态1.1 半导体的晶格结构和结合性质1.1.1 金刚石型结构和共价键1.1.2…

详解三种网络适配器:HBA、NIC 和 CNA

目录 前言&#xff1a; 一、主机总线适配器 (HBA) HBA的特点 二、网络接口卡 (NIC) NIC的特点 三、并发网络适配器 (CNA) CNA的特点 四、HBA、NIC 与 CNA的区别 五、结论 前言&#xff1a; 网络中的主机总线适配器 (HBA)、网络接口卡 (NIC) 和并发网络适配器 (CNA) 是…

视频号视频下载教程:如何把微信视频号的视频下载下来

视频号下载相信不少人都多少有一些了解&#xff0c;但今天我们就来细说一下关于视频号视频下载的相关疑问&#xff0c;以及大家经常会问到底如何把微信视频号的视频下载下来&#xff1f; 视频号视频下载教程 视频号链接提取器详细使用指南&#xff0c;教你轻松下载号视频&…

关于 cocos creator 如何打包抖音字节小游戏步骤一

1、cocos creator打开引擎&#xff0c;在顶部选择构建之后&#xff0c;在选择点击构建(ps:具体看项目组的大小&#xff0c;如果是一个简单的不多资源一般不到一分钟&#xff0c;如果项目很大&#xff0c;就至少半个小时以上)&#xff0c;之后 成功构建之后如下所示&#xff1a;…

欢迎免费申报讯方技术HarmonyOS人才训练营!

在今年1月备受瞩目的鸿蒙生态千帆启航仪式上&#xff0c;华为宣布&#xff1a;HarmonyOS NEXT星河预览版正式面向开发者开放申请&#xff0c;意味着鸿蒙将建立更广泛的生态系统&#xff0c;迎来更多的应用和软硬件产品&#xff0c;加速自我技术迭代&#xff0c;同时推动华为全场…

python 进程笔记二(通讯) (概念+示例代码)

1、为什么要掌握进程间通信 Python代码效率由于受制于GIL全局锁限制&#xff0c;多线程不能利用多核CPU来加速&#xff0c;而多进程方式却可以绕过GIL限制, 发挥多CPU加速的优势&#xff0c;达到提高程序的性能的目的。 然而进程间通信却是不得不考虑的问题。 进程不同于线程&a…

投资生涯的核心密码:构建交易逻辑体系

首先&#xff0c;我们需要明确一点&#xff0c;交易中究竟有没有确定性&#xff1f; 确定性是指在某一种形式、或有若干条件时&#xff0c;价格必然会上涨或下跌&#xff0c;也可以决定上涨或下跌的程度。 我认为&#xff0c;没有。迄今为止还没有一个理论能发现即使确定的东西…

金融知识分享系列之:五日线

金融知识分享系列之&#xff1a;五日线 一、股票均线二、五日线三、五日线加量能三、五日线案例四、五日线案例五、五日线案例六、五日线案例七、五日线案例八、五日线案例 一、股票均线 股票均线是一种用于平滑股票价格的指标。它是根据一段时间内的股票价格计算得出的平均值…

PureFlash v1.9.1特性介绍

PureFlashv1.9.1版本特性主要有3个&#xff1a; 1. 支持RDMA网络 使用RDMA协议可以大大减少对CPU的消耗&#xff0c;性能提升30%以上。 PureFlash的网络配置分为存储节点间网络&#xff08;存储后端网&#xff09;和客户端网络&#xff08;前端网&#xff09;。都支持使用RD…

C++:STL(标准模板库)

STL&#xff1a;主要是一些“容器”的集合&#xff1b;“容器”有&#xff1a;vector(数组)、list(双向链表)、deque(双向队列)、set(集合)、map(图&#xff1a;内部结构红黑树) STL也是算法和其他一些组件的集合&#xff0c;是泛型编程的一个经典范例。 STL的目的是标准化组…

数据分析-Pandas数据如何图示规律

数据分析-Pandas数据如何图示规律 数据分析和处理中&#xff0c;难免会遇到各种数据&#xff0c;那么数据呈现怎样的规律呢&#xff1f;不管金融数据&#xff0c;风控数据&#xff0c;营销数据等等&#xff0c;莫不如此。如何通过图示展示数据的规律&#xff1f; 数据表&…

一文读懂!ERP是什么?ERP和进销存有哪些区别?

ERP是什么&#xff1f;ERP和进销存有哪些区别&#xff1f; ERP和进销存可不止是叫法上的区别&#xff0c;这其中的门道还大着呢&#xff0c;今天就来跟大家详细讲解一下这些问题&#xff01;全文字数4000&#xff0c;干货满满&#xff0c;建议收藏&#xff01; 本文你将了解&…

无人驾驶-室内外循迹运行

1. 前言 好多初创公司公布出来的视频明显都是循迹的效果&#xff0c;不是说循迹不好&#xff0c;相反可以证明&#xff0c;循迹是自动技术开始的第一步。 自动驾驶循迹&#xff1a;一种能够自动按照给定的路线&#xff08;通常是采用不同颜色或者其他信号标记来引导&#xff…

✅技术社区项目—JWT身份验证

通用的JWT鉴权方案 JWT鉴权流程 基本流程分三步: ● 用户登录成功之后&#xff0c;后端将生成的jwt返回给前端&#xff0c;然后前端将其保存在本地缓存; ● 之后前端与后端的交互时&#xff0c;都将iwt放在请求头中&#xff0c;比如可以将其放在Http的身份认证的请求头 Author…

跨境外贸自动评论脚本开发常用代码!

随着跨境电商的兴起&#xff0c;自动化评论成为了提升销售和客户满意度的重要工具&#xff0c;通过编写自动评论脚本&#xff0c;商家可以快速地在各个平台留下正面评价&#xff0c;提高产品的曝光率和信誉度。 本文将介绍跨境外贸自动评论脚本开发的一些常用代码&#xff0c;…

STM32控制数码管从0显示到99

首先 先画电路图吧&#xff01;打开proteus&#xff0c;导入相关器件&#xff0c;绘制电路图。如下&#xff1a;&#xff08;记得要保存啊&#xff01;发现模拟一遍程序就自动退出了&#xff0c;有bug&#xff0c;我是解决不了&#xff0c;所以就是要及时保存&#xff0c;自己重…