C++11右值引用

本文详细介绍了C++11中新增的右值引用及其与左值引用的区别,探讨了右值引用在移动构造、移动赋值中的优势以及如何通过完美转发解决模板中万能引用的退化问题。文章还以实际案例展示了右值引用在STL库中的list插入函数中的应用。
摘要由CSDN通过智能技术生成


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的构造函数了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

C++下等马

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值