C++11右值引用

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

左值与左值引用

        左值是一个表示数据的表达式(如变量名解引用的指针),

        1.左值可以获取它的地址。

        2.左值可以对它赋值,但定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。

        3.左值可以出现赋值符号的左边,也可以右边

        左值引用就是给左值的引用,给左值取别名。

int a = 0;
int& b = a;

右值与右值引用

        右值也是一个表示数据的表达式(如:字面常量表达式返回值函数返回值(这个不能是左值引用返回)等等)

        1.右值不能取地址。(与左值最重要的区别)

        2.右值可以对它赋值,不可以改变它

        3.右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边

        右值引用就是对右值的引用,给右值取别名。

double func()
{
	return 1.3;
}
int main()
{
	double x = 1.1;
	double y = 1.2;

	//常见的右值
	10;//字面常量
	x + y;//表达式返回值
	func();//函数返回值

	//右值引用
	int&& a1 = 10;
	double&& a2 = x + y;
	double&& a3 = func();

	return 0;
}

底层上 

总结:

语法上,引用都是取别名,不开空间,左值引用是给左值取别名,右值引用是给右值取别名。

底层上,本质上都是指针,

        1.左值引用是存当前左值的地址。

        2.右值引用是把当前右值拷贝到栈上的一个临时空间,存储这个临时空间的地址。

右值引用与左值引用特殊使用情况

        左值引用不能直接给右值取别名,但是const 左值引用可以给右值取别名。

        右值引用不能直接给左值取别名,但是move(左值)可以用右值引用进行取别名。

double func()
{
	return 1.3;
}
int main()
{
    //左值引用给右值取别名
	//int& r1 = func();错误
	//int& r2 = 10;错误
	const int& r1 = func();
	const int& r2 = 10;

    //右值引用给左值取别名
	int x = 0;
	//int&& rr1 = x;错误
	int&& rr1 = move(x);//需要move一下

	return 0;
}

右值引用所解决的问题(移动语义)

左值引用解决了什么问题?

        1.传参拷贝的问题全部解决

                传参时,代替c语言中指针的作用。

        2.传返回值的问题解决了一部分

                解决了传返回值时候产生拷贝的问题,减少了一次拷贝(当返回值是一个大的类对象时,减少拷贝能减少很大开销)。

        但是局部对象返回(出了作用域销毁)的拷贝问题未解决。

        在C++98中对传左值和右值都会被视为const类型的对象

例如:

void func(const int& i)
{
	cout << "void func(const int& i)" << endl;
}
//void func(int&& i)
//{
//	cout << "void func(int&& i)" << endl;
//}
int main()
{
	int a = 0;
	func(a);//传左值
	func(10);//传右值
	return 0;
}

        而在C++11中,有了右值引用的概念后,我们可以对函数写出右值引用的版本 ,这时传右值就会调右值引用版本的函数,传左值就会调左值引用版本的函数。

例如:

void func(const int& i)
{
	cout << "void func(const int& i)" << endl;
}
void func(int&& i)
{
	cout << "void func(int&& i)" << endl;
}
int main()
{
	int a = 0;
	func(a);//传左值
	func(10);//传右值
	return 0;
}

        当然也可以使用move将左值属性转变成右值属性,来当成右值传参。

void func(const int& i)
{
	cout << "void func(const int& i)" << endl;
}
void func(int&& i)
{
	cout << "void func(int&& i)" << endl;
}
int main()
{
	int a = 0;
	func(a);//传左值
	func(10);//传右值
    func(move(a));//使用move将左值转成右值
	return 0;
}

        本质上是对传入参数的类型(左值or右值)进行识别。

C++11对右值概念的解释可以形象地理解为以下两种:

1.纯右值(内置类型的右值)如: 1,a+b

2.将亡值(自定义类型的右值)如:匿名对象,传值返回函数/to_string(上面的例子)

场景一

string拷贝构造的场景——拷贝构造移动构造版本

拷贝构造
        //拷贝构造——左值
		string(const string& tmp)
		{
			cout << "string(const string& tmp)——深拷贝" << endl;
			_str = new char[tmp._capacity + 1];
			strcpy(_str, tmp._str);
			_size = tmp._size;
			_capacity = tmp._capacity;
		}

        如果传入的对象tmp是将亡值,深拷贝是一种浪费资源的行为,因为深拷贝后tmp对象就销毁了。

移动构造

        使用右值引用进行移动构造。

		//移动构造——右值(将亡值)
		string(string&& tmp)
		{
			cout << "string(string&& tmp)——移动拷贝" << endl;
			swap(tmp);
		}

        本质上是识别传入的值是右值(将亡值),然后函数内部对该右值(将亡值)进行特殊处理。

 注:右值被右值引用后,右值引用的属性是左值

总结:

浅拷贝的类不需要移动构造。

原因:通常是一些内置类型,直接拷贝代价不大。

深拷贝的类需要移动构造。

原因:传入的是将亡值时,重新开空间构造对于一些大对象代价过大,不如直接将 将亡值 的资源转移给要构造的对象。

 场景二

        在这个场景中,C++11有了右值引用和移动语义,大大减少了传值返回的拷贝。

        考虑这样一个场景,在string容器中有个成员函数是 to_string ,它的功能是将一个数值转换成string类型的字符串,以下是其简要模拟实现。

        string to_string(int value)
		{
			bool flag = true;
			if (value < 0)
			{
				flag = false;
				value = 0 - value;
			}
			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;
		}

        C++98中,需要两次拷贝,但被编译器优化成一次拷贝。

        C++11后加入右值引用,需要一次拷贝,但被编译器优化成无拷贝。

场景三 

拷贝赋值重载的移动赋值版本(强行去掉编译器优化)

赋值拷贝
		//赋值拷贝(现代写法)
		string& operator=(string& tmp)
		{
			cout << "string& operator=(string& tmp)——深拷贝" << endl;
			string s(tmp);
			swap(s);
			return *this;
		}
int main()
{
	string s;
	s = to_string(10);
	return 0;
}

        相比于上一个场景,先定义后赋值,可以避开编译器优化,这样我们直接可以看到拷贝过程。

        结果是两次拷贝,刚好是如下图这样的情况。 

移动赋值
		//移动拷贝
		string& operator=(string&& tmp)
		{
			cout << "string& operator=(string&& tmp)——移动赋值" << endl;
			swap(tmp);

			return *this;
		}
int main()
{
	string s;
	s = to_string(10);
	return 0;
}

        这样就是一次深拷贝,一次移动赋值不产生拷贝。

        移动将亡值的资源,并且把不要的空间给将亡值,让将亡值释放时刚好释放不要的空间,一举两得。

总结:左值引用没有解决的问题,右值引用解决了,深拷贝对象传值返回只需要移动资源,代价很低

        C++11以后,所有的容器都增加了移动构造移动赋值和插入。有值转移的地方都可以考虑右值引用版本。

场景四

        特殊细节:右值被右值引用之后,右值引用r的属性是左值。
int main()
{
	//右值被右值引用以后,右值引用r是左值。
	int&& r = 10;
	r++;//左值可以被修改
}

        以list容器中的复用insert的push_back为例。

	    iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			return newnode;
		}
		void push_back(const T& x)
		{
			insert(end(), x);
		}

        以下是右值引用版本

	    iterator insert(iterator pos, const T&& x)
		{
            //右值引用版本代码……

			return newnode;
		}
		//右值引用版本
        void push_back(T&& x)
		{
			insert(end(), x);
		}
        然而在push_back中传入右值时,再去调insert函数,结果调用的是insert左值引用版本。
        原因是右值传入时, 右值被右值引用,此时右值引用x是一个左值所以传入insert的是一个左值。

        如何解决?
        法一:强行把左值move成右值
		//右值引用版本
        void push_back(T&& x)
		{
			insert(end(), move(x));//将左值move成右值
		}

         法二:完美转发

完美转发

模版中的&&——万能引用

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

例子:

template<typename T>
void PerfectForward(T&& t)//这里是万能引用,传右值就是右值引用,传左值就是左值引用
{
	//代码
}

测试一下 

void Func(int& x)
{
	cout << "void Func(int& x)----左值" << endl;
}
void Func(int&& x)
{
	cout << "void Func(int&& x)----右值" << endl;
}
void Func(const int& x)
{
	cout << "void Func(const int& x)----const左值" << endl;
}
void Func(const int&& x)
{
	cout << "void Func(const int&& x)----const右值" << endl;
}

template<typename T>
void PerfectForward(T&& t)
{
	Func(t);
}

int main()
{
	int a = 10;
	PerfectForward(a);//传左值
	PerfectForward(10);//传右值
	return 0;
}

但是结果

        原因是传右值时,右值被右值引用,右值引用属性是左值,导致再去调Func函数时传的是左值。

        如何解决这个问题?

        前文中的一种方式是move一下,将左值move成右值,但是对于这样的场景是不适用的,这里是要测试传的是左值还是右值,而move统统把所有值变成右值,这样就失去了测试的意义(传左值结果被move成右值,导致测试结果是右值)。

        这里C++11提供了一个 std::forward 完美转发在传参的过程中保留对象原生类型属性,它的作用是保持属性:如果本身是左值就不变,如果本身是右值右值被右值引用后,右值引用是左值,转成右值,相当于move一下。

template<typename T>
void PerfectForward(T&& t)
{
	Func(std::forward<T>(t));
}

        这样也就弥补之前场景四的特殊细节的情况 

总结:move与forward的区别

一、move 左值属性——>右值属性

二、forward 保持属性

        1.如果本身是左值就不变

        2.如果本身是右值,右值被右值引用后,右值引用是左值,转成右值,相当于move一下

  • 19
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

橘子13

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

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

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

打赏作者

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

抵扣说明:

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

余额充值