c++右值引用

c++左值引用与右值引用

先来看一下经典的左值引用

int main()
{
	int a = 0;
	int &b = a;
	b = 20;
	return 0;
}

反汇编代码

	int a = 10;
	// 这条mov指令把10放到a的内存中
0112436E  mov         dword ptr [a],0Ah  
	int &b = a;
	/* 下面的lea指令把a的地址放入eax寄存器
	   mov指令把eax的内容放入b内存里面*/
01124375  lea         eax,[a]  
01124378  mov         dword ptr [b],eax  
	b = 20;
	/* 下面的mov指令把b内存的值放入eax寄存器(就是a的地址)
	   mov指令再把20放入eax记录的地址的内存里面(就是把20赋值给a)*/
0112437B  mov         eax,dword ptr [b]  
0112437E  mov         dword ptr [eax],14h  

从上面的指令可以看出,定义一个左值引用在汇编指令上和定义一个指针是没有任何区别的,定义一个引用变量int &b=a,是必须初始化的,因为指令上需要把右边a的地址放入一个b的内存里面(相当于定义了一个指针的内存),当给引用变量b赋值时,指令从b里面取出a的地址,并把20写入该地址,也就是a的内存中(相当于给指针解引用赋值),所以也说,使用引用变量时,汇编指令会做一个指针自动解引用的操作。

所以在汇编指令层面,引用和指针的操作没有任何区别!
再思考下面的代码:

int &b = 20;

上面的代码是无法编译通过的,现在你应该知道原因,因为定义引用变量,需要取右边20的地址进行存储,但是20是立即数字,没有在内存上存储,因此是无法取地址的,但是解决这个问题还是有办法的,如下:

const int &b = 20;

用常引用可以引用20这个常量数字,难道此时20就能取地址了吗?当然不是,因为现在在内存上产生了一个临时量保存了20,b现在引用的是这个临时量,相当于下面的操作:

/*
这里temp是在内存上产生的临时量
const int temp = 20; 
const int &b = temp;
*/
const int &b = 20;

如果你还不明白,可以通过查看汇编指令进行确认,如下:

const int &b = 20;
010517C8  mov         dword ptr [ebp-14h],14h    《= ebp-14h就是内存栈上产生的临时量的内存地址
010517CF  lea         eax,[ebp-14h]= 取临时量的内存地址放入寄存器eax
010517D2  mov         dword ptr [b],eax  《= 再把eax寄存器的值(放的是临时量地址)存入b中

好了,到现在我们可以得到这样一个结论,上面的C++引用就是我们常用的左值引用,
左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用,
如const int &b = 20;但是这样一来,我们只能通过b来读取数据,
无法修改数据,因为b被const修饰成常量引用了,怎么办?

解决办法当然就是使用右值引用了,先看下面的这段代码分析:

int &&b = 20;  // 通过指令可以看到,原来const int &b=20和int &&b=20一模一样!!! 这里mov指令相当于是产生了临时量,起始地址ebp-14h
00CA18B8  mov         dword ptr [ebp-14h],14h  
		 //把临时量的地址放入eax寄存器当中
00CA18BF  lea         eax,[ebp-14h]  
         //再把eax的值(临时量的地址)放入b内存中(一个指针大小的内存)
00CA18C2  mov         dword ptr [b],eax  
	b = 40;
00CA18C5  mov         eax,dword ptr [b]  
00CA18C8  mov         dword ptr [eax],28h 

看上面代码,定义一个右值引用变量是这样的int &&b=20,从汇编指令来看,依然要产生临时量,然后保存临时量的地址,也就是说const int &b=20和int &&b=20在底层指令上是一模一样的,没有任何区别,不同的是,通过右值引用变量,可以进行读操作,也可以进行写操作

所以,可以给一个这样的结论,有地址的用左值引用,没有地址的用右值引用;有变量名字的用左值引用,没有变量名字的(比如临时量没有名字)用右值引用

从C++98和C++0x标准一路走来,一直在用左值引用解决问题;那么从C++11开始支持右值引用后,除了上面的好处,在实际的面向对象编程上,对我们还有什么帮助呢?请继续看下面的内容!

面向对象的效率问题

class Stack
{
public:
	// size表示栈初始的内存大小
	Stack(int size = 1000) 
		:msize(size), mtop(0)
	{
		cout << "Stack(int)" << endl;
		mpstack = new int[size];
	}
	// 栈的析构函数
	~Stack()
	{
		cout << "~Stack()" << endl;
		delete[]mpstack;
		mpstack = nullptr;
	}
	// 栈的拷贝构造函数
	Stack(const Stack &src)
		:msize(src.msize), mtop(src.mtop)
	{
		cout << "Stack(const Stack&)" << endl;
		mpstack = new int[src.msize];
		memcpy(mpstack, src.mpstack, sizeof(int)*mtop);
	}
	// 栈的赋值重载函数
	Stack& operator=(const Stack &src)
	{
		cout << "operator=" << endl;
		if (this == &src)
			return *this;

		delete[]mpstack;

		msize = src.msize;
		mtop = src.mtop;
		mpstack = new int[src.msize];
		memcpy(mpstack, src.mpstack, sizeof(int)*mtop);
		return *this;
	}
	// 返回栈的长度
	int getSize()const { return msize; }
private:
	int *mpstack;
	int mtop;
	int msize;
};
Stack GetStack(Stack &stack)
{
	// 这里构造新的局部对象tmp
	Stack tmp(stack.getSize());
	/*
	因为tmp是函数的局部对象,不能出函数作用域,
	所以这里tmp需要拷贝构造生成在main函数栈帧上
	的临时对象,因此这里会调用拷贝构造函数,完成
	后进行tmp局部对象的析构操作
	*/
	return tmp;
}
int main()
{
	Stack s;
	/*
	GetStack返回的临时对象给s赋值,该语句结束,临时对象
	析构,所以此处调用operator=赋值重载函数,然后调用
	析构函数
	*/
	s = GetStack(s);
	return 0;
}

上面的代码运行结果如下所示:
Stack(int) =》 对应Stack s;
Stack(int) =》 对应 Stack tmp(stack.getSize());
Stack(const Stack&) =》 对应return tmp;
~Stack() =》 对应tmp的析构
operator= =》 s = GetStack(s);
~Stack() =》 对应s = GetStack(s);语句完成,临时对象的析构
~Stack() =》 对应main函数中s局部对象的析构

上面的这段代码是我们编写C++类经常会遇到的一类问题,Stack对象由于成员变量是一个指针int *mpstack,构造时指向了堆内存,因此这样的对象做默认的浅拷贝和赋值操作是有问题的,导致两个对象的成员指针指向同一个资源,析构时同一个资源被delete两次,代码运行崩溃,因此我们需要给Stack提供自定义的拷贝构造函数和operator=赋值重载函数,如上面的代码所示。

上面的代码虽然解决了对象的浅拷贝问题,但是效率却非常的低下,主要在这两句代码上:

  1. return tmp;
    这句代码中,tmp是函数的局部对象,因此不能出函数作用域,所以这里由tmp拷贝构造生成main函数栈帧上的临时对象。请仔细查看上面的拷贝构造函数的实现:
// 栈的拷贝构造函数
Stack(const Stack &src)
	:msize(src.msize), mtop(src.mtop)
{
	cout << "Stack(const Stack&)" << endl;
	mpstack = new int[src.msize];
	memcpy(mpstack, src.mpstack, sizeof(int)*mtop);
}

上面代码中,src引用的是tmp对象,this指针指向的是main函数栈帧上的临时对象,它的实现是根据tmp临时对象的内存大小给临时对象底层开辟内存,然后把tmp的数据再通过memcpy拷贝过来,关键是tmp马上就析构了!!!

上面为什么不能把tmp持有的内存资源直接给临时对象呢?非得给临时对象重新开辟内存拷贝一份数据,然后tmp的资源又没有什么用处,而且马上就要析构,这样只能造成代码运行效率低下。

  1. s = GetStack(s);
    这里先通过临时量对象给s赋值,然后再析构临时对象,看看上面的operator=赋值函数的代码实现,先释放s占用的内存,又根据临时量的大小给s重新分配内存,拷贝数据:
// 栈的赋值重载函数
Stack& operator=(const Stack &src)
{
	cout << "operator=" << endl;
	if (this == &src)
		return *this;

	delete[]mpstack;

	msize = src.msize;
	mtop = src.mtop;
	mpstack = new int[src.msize];
	memcpy(mpstack, src.mpstack, sizeof(int)*mtop);
	return *this;
}

同样的问题,临时量对象给s赋值完成后,马上就析构了,为什么不能把临时对象的资源直接给s呢?如果这样做的话,效率就很高了,省了内存的开辟和大量数据的拷贝时间了。

上面提到的两个问题,在C++11中的解决方式是提供带右值引用参数的拷贝构造函数和operator=赋值重载函数。

右值引用的拷贝构造和operator=赋值函数
给上面的Stack类添加带右值引用参数的拷贝构造函数和operator=赋值重载函数,新添加代码如下:

// 带右值引用参数的拷贝构造函数
Stack(Stack &&src)
	:msize(src.msize), mtop(src.mtop)
{
	cout << "Stack(Stack&&)" << endl;
	/*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
	mpstack = src.mpstack;  
	src.mpstack = nullptr;
}

// 带右值引用参数的赋值重载函数
Stack& operator=(Stack &&src)
{
	cout << "operator=(Stack&&)" << endl;
	if(this == &src)
	    return *this;
	    
	delete[]mpstack;

	msize = src.msize;
	mtop = src.mtop;
	/*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
	mpstack = src.mpstack;
	src.mpstack = nullptr;
	return *this;
}

重新运行所有代码,打印如下:
Stack(int)
Stack(int)
Stack(Stack&&) =》对应return tmp; 自动调用带右值引用参数版本的拷贝构造
~Stack()
operator=(Stack&&) =》 s = GetStack(s); 自动调用带右值引用参数的赋值重载函数
~Stack()
~Stack()

从上面的打印可以清晰的看到,上面两处的拷贝构造函数和赋值重载函数的调用,自动使用了带右值引用参数的版本,效率大大提升,因为没有涉及任何的内存开辟和数据拷贝。因为临时对象马上就要析构了,直接把临时对象持有的资源拿过来就行了。

所以,临时量都会自动匹配右值引用版本的成员方法,旨在提高内存资源使用效率。带右值引用参数的拷贝构造和赋值重载函数,又叫移动构造函数和移动赋值函数,这里的移动指的是把临时量的资源移动给了当前对象,临时对象就不持有资源,为nullptr了,实际上没有进行任何的数据移动,没发生任何的内存开辟和数据拷贝。

一道笔试题

2018年校招中遇见过这样一道笔试题,如下:

#include <iostream>
#include <vector>
using namespace std;
class A
{
public:
	A(int data=10):ptr(new int(data)) {}
	~A() { delete ptr; ptr = nullptr; }
	A(const A &src)
	{
		cout << "A(const A&)" << endl;
		ptr = new int(*src.ptr);
	}
	A(A &&src)
	{
		cout << "A(A&&)" << endl;
		ptr = src.ptr;
		src.ptr = nullptr;
	}
private:
	int *ptr;
};
int main()
{
	vector<A> vec;
	vec.reserve(2);
	A a;
	vec.push_back(a);  // 调用哪个构造函数?
	vec.push_back(A(20)); // 调用哪个构造函数?
	return 0;
}

很明显,vec.push_back(a)调用的是左值引用参数的拷贝构造函数。vec.push_back(A(20))实参传入的是临时量对象,调用的是右值引用参数的拷贝构造函数,效率较高。上面程序打印如下:
A(const A&)
A(A&&)

函数接口返回容器

以上面笔试题类A的代码举例如下:

vector<A> getVector()
{
	vector<A> vec;
	vec.reserve(3);
	vec.push_back(A(20));
	vec.push_back(A(30));
	vec.push_back(A(40));
	cout << "————————" << endl;
	/*
		这里返回vec时,会调用vector容器的带右值引用参数的拷贝构造函数,
		类似vector(vector &&src),直接把这里vec的资源移动给main函数
		中的v,效率很高,也就是说函数在返回容器的过程中,没有做任何的内存和
		数据开销
	*/
	return vec;
}
int main()
{
	vector<A> v = getVector();
	return 0;
}

代码打印如下:
A(A&&)
A(A&&)
A(A&&)
————————

可以看到,vector< A > v = getVector()没有做任何的容器数据拷贝,调用带右值引用参数的成员方法,大大提高了对象的使用效率。

引用折叠

先看下面的一段代码解释:

int main()
{
	int a = 10;
	int &b = a;
	//int &&c = a; // 错误,无法将左值a绑定到右值引用c
	//int &&d = b; // 错误,无法将左值b绑定到右值引用d
	int &&e = 20;  // 正确,20是一个右值(没地址没名字),可以绑定到右值引用e上
	//int &&f = e; // 错误,无法将左值e绑定到右值引用f,因为e有名字,有地址,本身也是左值
	int &g = e;    // 正确,e本身有名字,有地址,是一个左值,可以被g引用
	return 0;
}

从上面这段代码大家可以对左值和右值有更清楚的了解,尤其是右值引用变量e本身是一个左值,这个需要做正确的理解。

引用折叠的概念主要用在函数模板类型参数的推导中,如下面的函数模板:

template<typename T>
void func(T&& val)
{
	cout << "01 val:" << val << endl;
	T tmp = val;
	tmp++;
	cout << "02 val:" << val << " tmp:" << tmp << endl;
}
int main()
{
	int a = 10;
	int &b = a;
	int &&c = 10;

	cout << "func(10):" << endl;
	func(10);// 10是右值,引用类型是int&&,T&&推导过程是int&&+&&折叠成int&&,所以T是int,下同
	cout << "func(a):" << endl;
	func(a);// a是左值,不可能用右值引用来引用,所以func推导T为int&,那么T&&->int&+&&折叠成int&
	cout << "func(std::move(a)):" << endl;
	func(std::move(a)); // std::move(a)是把a转成右值类型,右值引用类型是int&&,所以func推导T为int
	cout << "func(b):" << endl;
	func(b);// b是左值,不可能用右值引用来引用,所以func推导T为int&,那么T&&->int&+&&折叠成int&
	cout << "func(c):" << endl;
	func(c);// c是左值,不可能用右值引用来引用,所以func推导T为int&,那么T&&->int&+&&折叠成int&
	
	return 0;
}

代码运行打印如下:

func(10): //T tmp = val; T是int
01 val:10
02 val:10 tmp:11
func(a): //T tmp = val; T是int&
01 val:10
02 val:11 tmp:11
func(std::move(a)): //T tmp = val; T是int
01 val:11
02 val:11 tmp:12
func(b): //T tmp = val; T是int&
01 val:11
02 val:12 tmp:12
func©: //T tmp = val; T是int&
01 val:10
02 val:11 tmp:11

从上面的代码可以看出,func这个函数模板的T类型有时候推导出来是int类型,有时候是int&类型,这个通过上面的代码测试,希望大家能够掌握。再说引用折叠,就是int && + &&折叠成int&&,除此之外,都折叠成int&,如int& + &&折叠成int&,知道这个就可以,具体的应用再看下面对move和forward函数的讲解。

std::move移动语义

看如下代码示例:

class A
{
public:
	A(int data=10):ptr(new int(data)) {}
	~A() { delete ptr; ptr = nullptr; }
	A(const A &src)
	{
		cout << "A(const A&)" << endl;
		ptr = new int(*src.ptr);
	}
	A(A &&src)
	{
		cout << "A(A&&)" << endl;
		ptr = src.ptr;
		src.ptr = nullptr;
	}
private:
	int *ptr;
};
int main()
{
	vector<A> vec;
	vec.reserve(10);
	for (int i = 0; i < 10; ++i)
	{
		A a(i);
		/*
		这里a是一个左值,因此vec.push_back(a)会调用左值的
		拷贝构造函数,用a拷贝构造vector底层数组中的对象
		*/
		vec.push_back(a);
	}
	return 0;
}

代码运行打印如下:
A(const A&)
A(const A&)
A(const A&)
A(const A&)
A(const A&)
A(const A&)
A(const A&)
A(const A&)
A(const A&)
A(const A&)

调用了10次左值引用的拷贝构造函数,看上面的代码,A a(i)在for循环中其实算是局部对象,在vec.push_back(a)完成后,a对象也就该析构了,所以在vec.push_back(a)时,应该把对象a的资源直接移动给vector容器底层的对象,也就是调用右值引用参数的拷贝构造函数,怎么做到呢?这时候就用到了带移动语义的std::move函数,main函数代码修改如下:

int main()
{
	vector<A> vec;
	vec.reserve(10);
	for (int i = 0; i < 10; ++i)
	{
		A a(i);
		/*
		由于a马上就会销毁,因此这里应该用右值引用参数
		的拷贝构造函数,效率会更高
		*/
		vec.push_back(std::move(a));
	}
	return 0;
}

代码运行打印如下:
A(A&&)
A(A&&)
A(A&&)
A(A&&)
A(A&&)
A(A&&)
A(A&&)
A(A&&)
A(A&&)
A(A&&)

此时在vec.push_back(std::move(a))这段代码中会调用到a对象的右值引用参数的拷贝构造函数。可以看move函数的源码,其实move就是返回传入的实参的右值引用类型,做了一个类型强转,move代码:

template<class _Ty>
	_NODISCARD constexpr remove_reference_t<_Ty>&&
		move(_Ty&& _Arg) noexcept   
	{	// forward _Arg as movable
	return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
	}

std::forward完美转发

先给一个代码示例,实现一个简单的vector,来描述forward的应用场景,示例代码如下:

// 容器里面元素的类型
class A
{
public:
	A(){}
	// 带左值引用参数的赋值函数
	A& operator=(const A &src)
	{
		cout << "operator=" << endl;
		return *this;
	}
	// 带右值引用参数的赋值函数
	A& operator=(A &&src)
	{
		cout << "operator=(A&&)" << endl;
		return *this;
	}
};
// 容器的类型
template<typename _Ty>
class Vector
{
public:
	// 引用左值的push_back函数
	void push_back(const _Ty &val)
	{
		addBack(val);
	}
	// 引用右值的push_back函数
	void push_back(_Ty &&val)
	{
		// 这里传递val时,要用move转换成右值引用类型,
		// 因为val本身是左值,有名字有地址,见前面引用折叠部分的说明
		addBack(std::move(val)); 
	}
private:
	enum { VEC_SIZE = 10 };
	_Ty mvec[VEC_SIZE];
	int mcur;

	template<typename _Ty>
	void addBack(_Ty &&val)
	{
		/*
		这里val本身永远是左值,所以不可能调用
		容器内部对象的右值引用参数的operator=赋值函数
		*/
		mvec[mcur++] = val;
	}
};

int main()
{
	Vector<A> vec;
	A a;
	vec.push_back(a); // 调用A的左值引用的赋值函数
	vec.push_back(A()); // 理应调用A的右值引用参数的赋值函数,却调用了左值引用的赋值函数
	return 0;
}

代码运行打印如下:
operator=
operator=
vec.push_back(A())这句代码传入的是临时对象,最终却没有调用A对象的右值引用参数的赋值函数operator=,主要原因就是在Vector中addBack函数里面,val永远被当作左值了,无法保持它接收的实参的引用类型,是左引用还是右引用,此时std::forward就要起作用了,它称作“类型完美转发”,也就是说可以保持实参数据的左引用或者右引用类型,上面的addBack函数修改如下:

template<typename _Ty>
void addBack(_Ty &&val)
{
	/*
	这里使用std::forward,可以获取val引用的实参的引用类型,
	是左引用,还是右引用,原理就是根据“引用折叠规则”
	int&+&&->int&     int&&+&&->int&&
	*/
	mvec[mcur++] = std::forward<_Ty>(val);
}

修改完addBack函数,重新运行上面代码,打印如下:

operator=
operator=(A&&)

完美!vec.push_back(A())这句代码,最终调用了A对象的右值引用参数的赋值函数operator=,符合预期。因为在addBack中使用了std::forward类型完美转发机制,它的源码实现如下:

template<class _Ty>
	_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
	{	// forward an lvalue as either an lvalue or an rvalue
	return (static_cast<_Ty&&>(_Arg));
	}

template<class _Ty>
	_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept
	{	// forward an rvalue as an rvalue
	static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
	return (static_cast<_Ty&&>(_Arg));
	}

上面是C++库里面提供的两个forward重载函数,分别接收左值和右值引用类型,进行一个类型强转,(static_cast<_Ty&&>(_Arg)), 如果实参类型是int& + && -> int&就保持了实参的左值引用类型,如果实参类型是int&& + && -> int&&就保持了实参的右值引用类型。

【总结】:std::move是获取实参的右值引用类型;std::forward是在代码实现过程中,保持实参的原有的引用类型(左引用或者右引用类型)。


作者:大秦坑王
来源:CSDN
原文:https://blog.csdn.net/QIANGWEIYUAN/article/details/88653747
版权声明:本文为博主原创文章,转载请附上博文链接!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值