当前位置:   article > 正文

谈谈C++中的左值,右值,右值引用,std::move()的问题_std 左值 右值引用

std 左值 右值引用

前言

在C++中,我们一直都听说过一种左值右值的概念,甚至有左值引用,右值引用,左值表达式,右值表达式等的东西;一般大家都对他们模棱两可,甚至有人听过std::move()的用法,但是不知其真正的含义。本文也就是帮助大家扫扫盲,顺便给自己的c++知识库做一个总结。

左值和右值

我们知道,变量的本质是内存中的一段存储空间;而左值就是使用表达式的存储空间,表示这个表达是就是使用存储空间,即使用的是内存,表示的是内存一段空间。

从字面上理解左值:就是能赋值在等号 = 左边的值,就叫左值(使用的是内存空间);

而右值,就是左值的对立面,即 不能作为左值的值就是右值;

对于右值来说,不能放到等号 = 的左边,因为一旦放到做好的左边的值,就是左值,而右值又是左值的对立面的,所以右值是一定不能赋值在等号 = 的左边;

既然是左值右值是对立的,所以就有一个结论:
对于一条表达式来说(变量也是表达式,最简单的表达式),要么是左值,要么是右值,不可能同时是左值,同时是右值,即一条表达式不能有两个身份。

对右值的第一层理解:所以我们就有判断右值的一个办法:
只要表达式不能在等号 = 的左边赋值,那么我们就可以断定这个表达式一定是右值。

举个例子:

int i = 1;
//int i = 1; 在等号 = 左边的表达式 i 就是左值,使用的是内存中的一段存储空间
//而对于等号右边 = 的 1是 右值,如何判断的呢?
//因为 1 不能放在等号 = 左边赋值,所以可以判断为右值;

另一种判断方式:常量都为右值。
 1 是常量,所以为右值。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

右值的第二层理解:判断右值的办法:只要是常量我们都可叫为右值。


看下个例子:

int i = 1;
i = i+1; 
  • 1
  • 2

你是否发现这里的 i 既能做左值(使用的是 i 的内存空间),又能做右值(使用的是 i 的内存空间内容),是真的如此嘛?
其实不然,因为左值和右值是不能够同时存在的。那为什么么 i = i + 1;中 i 又是左值,又可以做右值呢?原因就是这里 左值是有左值属性和右值属性,在这里, i 是左值,所以 i 不可能是右值,所以 i 是左值,自然能够在 = 等号左边出现,i 在等号 = 右边出现,使用的是 i 的内容,这里表现得是i 的右值属性,即 i 是左值但是它有右值属性。

对左值的理解:左值有左值属性和右值属性,所以左值可以即放到等号 = 左边,又可以放到等号 = 右边。


引用的分类

引用大致可以分为三种引用:

  1. 第 一种是左值引用;
  2. 第二种是常量左值引用;//不希望修改该引用
  3. 第二种是右值引用;

其中左值引用和常量左值引用都是我们之前学过的引用,即有一个 & 和 const & 的;

现在重新谈谈对他们的理解:

还记我们之前学得引用嘛?引用不能引用常量。

为什么呢?今天我就告诉你,因为这些都是左值引用,左值引用是什么?左值引用就是要引用左值,所以才叫左值引用,而常量是右值,一个右值怎么能被左值引用去引用呢。

int i = 1; //i 是左值, 1是右值

int& ri = i;//正确,因为 ri 是左值引用,而 i也是左值,所以对

int& ri1 = 1;//错,因为 ri1是左值引用,只能引用左值,而 1 是右值,所以不能引用右值。

ri = 2; //正确,ri代表 i ,ri = 2,i也是 = 2;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

那么什么是右值引用?

右值引用时c++11引入的新标准,它赋予了右值一种生命价值的体现,因为右值引用的出现,右值得以被很好的使用。在c++中规定,右值引用是 && ,两个符号表示,而左值是 & 一个符号。

右值引用的作用:右值引用就是为了绑定右值。

要理解右值和右值引用不是一个概念。右值就是不是左值的值,而右值引用是用来引用右值的一个变量/对象。

举例子:

int i = 1;
int&& ri = i; //错误,i是左值,ri是右值引用,两者不对应
int&& ri1 = 2; //正确:ri1是右值引用,2是右值。

int& ri2 = i*10;//错误 i*10是右值,ri2是左值引用

int&& ri3 = i*10;//正确

const int& ri4 = 1; //正确,对于const & 可以绑定右值;

const int& ri3 = i* 10;//正确

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

右值引用虽然是引用右值,但是右值引用的变量本身就是左值!
右值引用虽然是引用右值,但是右值引用的变量本身就是左值!
右值引用虽然是引用右值,但是右值引用的变量本身就是左值!

要理解这句话很重要,对于上面的例子: &&ri1,这里 ri1虽然是右值引用,但是ri1是左值。

原因很简单,一个表达式要么是左值,要么就是右值。而在这里 ri1 到底是哪个呢?那肯定是左值
第一:ri1 在等号 = 左边;而右值是不可能出现在等号 = 右边的。

第二:ri1可以被作为左值引用,引用左值,你没看错,ri1虽然是右值引用,但是ri1就是左值。

int& ri1 = i;//正确。ri1是上面的右值引用,但是对于左值引用也可以使用,说明它本身就是左值。
  • 1

不要被绕晕了。左值引用,右值引用 和左值,右值,不是同一个概念。一个是引用层面,一个是值层面。

所以说,当把右值引用作为参数时候,我们要清楚,它本身就是左值。

void fun(int&& ri) //这里ri是右值引用,但是它本身是左值
{
	//逻辑实现体
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

即将销毁的/临时的对象都是右值

我们前面已经对右值引用达到了两层的理解:

  1. 只要表达式不能在等号 = 的左边赋值,那么我们就可以断定这个表达式一定是右值。
  2. 只要是常量我们都可叫为右值。

现在多了一层: 只要是即将销毁的或者临时的对象都是右值。而右值引用的作用是引用右值,更具体的就是右值引用的作用就是为了绑定(引用)那些即将销毁的/临时的对象。这个才是右值引用出现的主要作用。

那么c++引入右值引用它到底是什么目的?目的就是为了提升程序运行的效率,而提升运行效率的方式就是:把复制对象变成移动对象,从而提升效率。

复制对象和移动对象浅谈

现在要理解的就是什么是复制对象,什么是移动对象?

复制对象应该大家都清楚,假如有一个对象A,要使用对象B的内容,然后对象B 就把里面的成员完完全全的一个一个复制给对象A,而这个对象A,并不是凭空而来的,是需要先创建号自己的空间,才可以让对象B的成员都复制给对象A。其实这就是C++中的拷贝构造函数和赋值运算符函数的调用。

对于复制对象,关注两个点:
第一要创建新对象,就是开辟新空间,
第二要复制对象的内容,很明显,当对象的内容很多时候,这个复制效率及其低下,
第三这是调用了拷贝和赋值运算符导致的。


而移动对象呢?

关注点应该在移动两个字,很明显和复制时不一样的,那怎么移动呢?假设有一个对象B,这个对象里面的new出来的数据可以直接交给另一个对象A来接管,也就是所谓的移动到对象A来接管,而对象B原来new出来的数据的使用权,从对象B变到了对象A,很明显B new来的使用权不属于B,那么自然而然就不要使用B new的数据,一个很好的方式就是把对象B 指向该 new的空间指针置为 nullptr;

在这里理解移动对象两个点:
第一:对象B要移动给对象A接管的原因时对象B是即将销毁的/临时的对象,即右值。
第二:对象A并不需要新的空间去接收对象B的内容,因为对象A是直接接管B对象的内容。


那么移动对象是如何创建出来的?

我们知道复制对象,就是调用了拷贝构造函数和赋值运算符;

而移动对象,就是调用了移动构造函数和移动复制运算符;

移动构造和移动复制和拷贝构造和拷贝复制在形式上就是形参上有点不同;

复制对象和复制运算符的第一个形参是左值引用,而我们移动构造和移动复制第一个形参是右值引用,接收的是右值。

所以我们就清楚啦。右值引用在移动构造移动赋值运算符的形参作用,只要调用触发了一定条件,那么就会调用移动构造移动复制运算符函数,这两个函数比拷贝构造和拷贝复制好很多。

至于什么条件,等到下一章我讲讲移动构造和移动复制时候再说吧,现在先理解到这里。


std::move( )

std::move()是c++11新标准退出的一个新的函数,这个函数的功能是:把左值强制转换为右值. 我么们都知道 右值引用是不能引用左值的,但是一旦通过std::move( ) 这个函数处理过的左值,它就变成了右值,那么右值引用就可以引用它啦。此时的右值引用就是和左值可以对应上了,那么右值引用就代表左值,左值就代表右值引用。

举个例子

void fun(int&& ri) //形参为右值引用,引用的是右值,所以说传参只能传右值,
				   //但是对应ri本身来说它是左值
{
	//逻辑函数体实现
}

int i = 1;
int&& ri1 = i; //错误,i是左值,ri1是右值引用
int&& ri2 = std::move(i);//正确,std::move()强制把i左值转换为了右值,
						//所以可以用右值引用ri2去接收它
						//此时 ri2就代表 i,i就代表ri2
//所以下面的修改也是正确的
//右值引用就是和左值可以对应上了,那么右值引用就代表左值,左值就代表右值引用。
ri2 = 2; //正确 ri2 = 2,与此同时 i = 2;
i = 3; //正确 i = 3,与此同时 ri2 = 3;

int j  = 1;

fun(j)//错误调用,fun函数接收的是右值
fun(std::move(j));//正确调用,此时fun里的形参ri就代表 j这个变量
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

再看看其他案例:对于string这个类的(要知道标准库的string里面是有移动构造和移动复制,具体实现细节不展开说,知道就好)

string str = "hello world";

string&& rStr = str; //错误,str是左值,rStr是右值引用

string str1 = std::move(str);//正确
要看清楚,这里str1不是右值引用啊,这里std::move(str)会触发string这个类本身里面的移动构造函数,
而这个string类的移动构造函数处理的结果是:把str里面的数据,移动交付给了str1,而str的内容变空,
也就是说str原来 = hello world, 通过 std::move(str)的操作调用了移动构造函数,这个移动构造函数
把 hello world 交付给 str1去使用,于此同时把str的内容置为空。

所以朋友,不要误解string str1  = std::move(str);是把 str里面的内容移动到了 str1的内容,
我们要清楚知道 std::move()函数作用,是将左值变为右值,而再这里string类有移动构造函数,所以当
转换为右值的时候,系统觉得触发移动构造函数的调用条件,即有一个右值,所以就调用了移动构造函数。


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

进一步验证std::move()是简单的将左值转换为右值,不是移动数据给另一个对象。

string str = "abcdef";

std::move(str);//这里单纯调用std::move()函数,并没有用对象接收它的值

cout<<str<<endl;//值依旧是abcdef

string&& rStr = std::move(str);
//在这里并没有触发string类的移动构造函数哦,只是一个简单的引用罢了,rStr代表str,str表示rStr;

所以下面修改也成立:
rStr = "ghi";//rStr = "ghi" 与此同时 str = "ghi";
str = "jkl";//str = "jkl",与此同时 rStr = "jkl";
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

右值的精髓所在是配合移动构造和移动赋值运算使用,而std::move()很好的把左值转换右值,把本身是左值用来调用拷贝和复制运算符的函数,变为调用移动构造和移动复制运算,这就是提升效率的地方。

至于移动复制和移动构造的篇章下一篇再说。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/640041
推荐阅读
相关标签
  

闽ICP备14008679号