【c++11】看完立马就懂--右值引用!!!

一、什么是右值?什么是左值?

首先,当我们看到右值的时候,我们很自然的就会产生疑问?
什么的右边呢?
等号的右边吗?
那么如果是按赋值=符号的右边来定义的话,那么,左值是不是就是=符号的左边的值呢?
但是,看到下面的这段代码,我们又感觉,上面的说法,貌似不太对!

#include<iostream>

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

仔细推敲,我们发现,10是个字面常量,和我们的a,在栈上创建的变量,貌似不是一个东西?这样来看,好像和我们上面的定义挺符合的啊?两个不同的东西,刚好用左值和右值这么个名称来进行区分。好像挺对的哦。
但是,b不也是在栈上开辟的变量吗?b在赋值符号=的左边,但是a也是栈上的变量,a在赋值符号=的右边呀!那这样子来看,左值和右值不就没什么区别了吗?????
————————————————————————————————
所以,上面的这种结论,肯定是不正确的!那么究竟什么是左值?什么是右值???
左值和右值,它肯定会存在区别!不然为什么要出现这样子的命名。通过作者的不懈努力的查阅资料!!!
左值,我们可以理解我们平常开辟在栈上的变量,例如上面的a,b这些变量,都可以叫做左值。
右值,通常我们认为,右值是那么字面常量,如10;表达式的返回值(a + b返回一个临时变量);函数的返回值(这个返回值不是引用);我们将这些统称为右值。
其中,我们最佳的区分方式就是,看看这个变量能不能取地址。
在这里插入图片描述
注意,字符串,我们将它认为是左值,它可以取地址,地址是首元素的地址。
在这里插入图片描述
在这里插入图片描述
————————————————————————————————

二、右值引用

引用我们都很熟悉,也就是

#include <iostream>

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

这种引用我们叫做左值引用,右值引用是长啥样子的?

#include <iostream>
int main()
{
	int&& ra = 10;
	return 0;
}

这样子的引用方式我们叫做右值引用。
那么我们就会产生以下的疑问?

  • 左值引用能不能引用右值?
  • 右值引用能不能引用左值?

左值能不能引用右值?
在这里插入图片描述
撕~~~~,好像不可以诶?仔细想了想,是不是和引用的权限升级有关呢?
因为10是个字面常量呀,它又不能修改,我用一个左值引用它,不就发生权限放大吗?那么我们给它加上一个const,来限制它的权限,是不是就能够左值引用右值了呢?
在这里插入图片描述
可以看到,编译器这次没有报错了,说明这样子的方式是可以的,也就说明了,const的左值引用可以引用右值,所以,这就解释了,为什么STL容器里面的拷贝构造,构造函数,为什么要对参数加上const的原因了,这样既能够将我们的左值传参过来,也可以将右值传参传过来。

右值引用能不能引用左值?
我们来尝试一下就知道了
在这里插入图片描述
编译器报错了,说明是不可以的?肯定不可以吗?想想原因,貌似想不出来什么原因了??????
是不是真的不可以?
通过我查阅C++11,发现了,c++11里面给我们提供了一个方法。也就是将左值move一下。
在这里插入图片描述
好像可以了,但是为什么move一下,a就可以被右值引用了呢?这样子写可以吗?
在这里插入图片描述
好像又不行了,所以我推断,move应该是根据左值a,来产生一个临时的右值作为返回值,然后右值引用就可以引用了。

三、右值引用的好处

右值引用说了半天,那么右值引用的出现到底有什么用啊?它难道就是为了能够替换const的左值引用,给右值也能够名正言顺的加上一个右值引用的名称吗?仅仅只是为了给我们的右值也有了地位,恩宠一下右值是吧?????
那肯定是不可能的啊,怎么可能会耗费那么大的力气搞出来一个右值引用,就为了恩宠一下右值???
对于它的作用我也很好奇,所以我也去看别人的博客和上网查资料,我看到一些博客上面介绍:右值引用可以延长生命周期???
我看到的时候,我满脸问号
在这里插入图片描述
什么玩意???延长生命周期???
延长啥的生命周期啊???
为什么要延长生命周期???
延长生命周期有啥用啊???
我表示很疑问和好奇,在这种好奇和疑问的驱动下,我就去看了《C++ Primer》这本书,我仿佛领悟到了一些!!!
那篇博客的作者可能想表达的是,延长资源的生命周期,右值引用可以说是一个非常非常强大牛逼的东西。
场景一:

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
using namespace std;

string func()
{
	string s("aaaaaaaaaa");
	return s;
}

int main()
{
	string s1 = func();
	return 0;
}

按照右值引用没有出现之前,我们的代码逻辑应该是这样子的
在这里插入图片描述
然后这里由于出现的s要拷贝构造一次,s1也要拷贝构造,编译器就会优化,直接跳过中间的那个临时变量,直接s1拷贝构造s(编译器优化就是我们的编译器其实是很智能的,如果发生那种创建一个变量,需要连续的构造几个中间变量的时候,编译器就会直接将中间的变量优化掉)。

这是右值引用没有出现的时候的分析,如果这个函数里面的string非常的大,那么在构造的时候,string肯定是深拷贝,那么这样子的代价真的太大了,如果返回的是vector< vector < int >>这样的类型,那么拷贝的代价真的太恐怖了,所以这里在右值引用没有出现之前,我们是通过输出型参数的方式来获取结果,也就是这样形式的代码风格

#include <iostream>

void func(string& s1)
{
	.......
}

那么右值引用的出现,就可以解决这样的问题,如何解决?

string func()
{
	string s("aaaaaaaaaa");
	return s;
}

int main()
{
	string s1 = func();
	return 0;
}

我们看到,func里面的s,它会随着函数调用的结束销毁栈帧,那么在里面的s,它的生命周期也就在那一个函数体里,函数销毁,那么s自然而然也要销毁,但是我们要返回s的内容,那么我们为什么不把s的资源夺过来,也就是将s夺舍!!!把它的资源占为己有,然后我的s1,它都已经要被赋值,把原先的内容给丢掉了,构造新的内容出来了,那么我为什么不把s的资源给拿过来,将我s1不要的资源丢给你s,你s都要销毁了,你就顺路把我那些垃圾,不要的资源也带走吧!!!这样的思路,就是右值引用的意义。
它将我们的将亡值,也就是即将销毁的s的资源的生命周期延续了下去,其实本质就是一种交换资源的方式。
那么这种实现也很简单,只需要创建一些指针变量,来交换我们的变量所指向在堆上创建的资源,获取到对方指向的内存块,这样就完成拷贝,这种拷贝我们称为移动拷贝
那么根据上面的思路,我们可以这样子玩

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
using namespace std;

class A
{
public:
	A(int size = 0)
		:_a(new int[size]),
		_size(size)
	{}

	A(const A& it)
	{
		cout << "A(const A& it) -- 左值引用" << endl;
		_size = it._size;
		_a = new int[_size];
		for (int i = 0; i < _size; i++)
		{
			_a[i] = it._a[i];
		}
	}

	A(A&& it)
	{
		cout << "A(A&& it) -- 右值引用" << endl;
		int* tmp = it._a;
		it._a = _a;
		_a = tmp;
	}

private:
	int* _a = nullptr;
	int _size = 0;
};

A func()
{
	A a(10);
	return a;
}

int main()
{
	A a1 = func();
	return 0;
}

当我们运行到箭头位置的时候
在这里插入图片描述
我们看到a的地址的后四位是0510,然后接着运行
在这里插入图片描述
我们可以观察到,a的地址转移到了a1上面,而我们实现的方式,仅仅只是创建了一个临时的指针对象,交换双方指向的内容,而深拷贝要开辟空间,要把对方的值拷贝给我开辟的空间上,要调佣循环,这样的代价相比,移动构造的代价小的非常非常的多。

我们运行的结果
在这里插入图片描述

这样子,我们只需要很小的代价,就将资源获取到了,创造出右值引用的人实在是太牛了。所以右值引用我只能说,真香!
————————————————————————————————

四、万能引用

我们来看这段代码

template<class T>
void func(T&& x)
{
	cout << x << endl;
}

int main()
{
	func(10);        //右值

	int a = 9;
	func(a);         //左值

	const int b = 8;
	func(b);         //const 左值

	func(move(a));   //move 左值 -> 右值
	func(move(b));   //move const 左值 -> const 右值

	return 0;
}

按理来说,我们的func函数里面看着是个右值,所以理所应当的应该要传一个右值过去,但是我们运行发现,
在这里插入图片描述
咦???怎么全部都可以编译通过并且运行呢???
这里需要科普一个知识,
首先,这是一个模版,它是通过具体的函数调用的值来实例化的,然后这里涉及到了一个叫做引用折叠的知识,如果传参传过去的是个左值,那么这里的T&&就会发生引用折叠,变成T&,而右值传过去依然是T&&。
那么这样的语法,它被称为万能引用,就是字面意思,模版的加入给予了它根据参数来实例化出不同的具体函数,这种方式让我们c++变得更加的灵活强大。

五、完美转发

我们也是一样,思考一下这段代码的结果


void test(int& x)
{
	cout << "void test(int& x)        -- 左值引用" << endl;
}

void test(const int& x)
{
	cout << "void test1(const int& x) -- const 左值引用" << endl;
}

void test(int&& x)
{
	cout << "void test(int&& x)       -- 右值引用" << endl;
}
void test(const int&& x)
{
	cout << "void test(const int&& x) -- const 右值引用" << endl;
}

template<class T>
void func(T&& x)
{
	test(x);
}

int main()
{
	func(10);        //右值

	int a = 9;
	func(a);         //左值

	const int b = 8;
	func(b);         //const 左值

	func(move(a));   //move 左值 -> 右值
	func(move(b));   //move const 左值 -> const 右值

	return 0;
}

我们来分析一下,根据上面学的万能引用,这里的输出结果应该是
右值
左值
const 左值
右值
const 右值
我们来查看编译器运行的结果
在这里插入图片描述
诶???这里就要产生疑问了,怎么全是左值了???很奇怪是不是??
我们之前说过,判断一个变量,是左值还是右值,关键在于能不能取地址,那么我们就怀疑,右值引用这个变量本身,能不能取地址呢?

int&& x = 10;
	cout << &x << endl;

在这里插入图片描述
我在vs2019下测试发现,右值引用本身竟然是一个左值,我们再来看它的汇编语言
在这里插入图片描述
我们发现,它好像是将10放在了栈上的某个位置,然后将这个位置用寄存器保存起来了,然后x仿佛被当成了一个指针, 然后将10存放在栈的位置存到x里面。
所以在我的编译器上,右值引用被当成是一个左值了,所以这就解释了为什么上面的代码执行结果全是左值!
那么我们如何解决这个问题?
c++11提供了forward来实现完美转发,即在传参过程中保证了参数的属性不会发生改变,也就是右值引用去当参数传递时,调用的是右值引用的函数,


void test(int& x)
{
	cout << "void test(int& x)        -- 左值引用" << endl;
}

void test(const int& x)
{
	cout << "void test1(const int& x) -- const 左值引用" << endl;
}

void test(int&& x)
{
	cout << "void test(int&& x)       -- 右值引用" << endl;
}
void test(const int&& x)
{
	cout << "void test(const int&& x) -- const 右值引用" << endl;
}

template<class T>
void func(T&& x)
{
	test(forward<T>(x));
}

int main()
{
	func(10);        //右值

	int a = 9;
	func(a);         //左值

	const int b = 8;
	func(b);         //const 左值

	func(move(a));   //move 左值 -> 右值
	func(move(b));   //move const 左值 -> const 右值

	return 0;
}

运行结果
在这里插入图片描述
这样就没有发生问题了!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ck837

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

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

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

打赏作者

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

抵扣说明:

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

余额充值