C++学习笔记:左值、右值和左值引用

admin 2023年2月26日14:51:19评论17 views字数 2874阅读9分34秒阅读模式

一直以来对左值和右值的理解局限于等号的左边是左值,等号的右边是右值;而最近在读一些开源代码时,时常遇到&,&&,std::move,std::forward等,简单bing一下,发现这是C++11 引入的新概念。

左值&右值

左值是指向一个指定内存的东西。另一方面,右值就是不指向任何地方的东西;左值则活的很久,因为他们以变量的形式存在,右值表示一个本应没有名称的临时对象。我们来看些例子:

int a = 123;

上面表达式中a是一个变量,它有具体的内存位置存放着,所以它是一个左值;而右边的123是一个数字,它没有特定的内存,只是程序运行时的一个临时值被赋给a变量。C++中声明一个赋值(assignment)需要一个左值作为它的左操作数(left operand):这完全合法。对于上面的左值a,还可以进行其他操作:

int *b = &a;int c = a + 1;

上面表达式1通过取地址操作&符将a的内存地址赋值给了变量b(也是一个左值,有内存空间);取地址操作&需要一个左值,并且产生了一个右值(临时值,无内存空间);这也是另一个完全合法的操作:在赋值操作符的左边我们有一个左值(一个变量),在右边我们使用取地址操作符产生的右值。表达式2也类似,c是一个左值,有内存空间,而a+1是一个右值,无内存空间,是程序运行过程中才产生的。而像下面这样的操作则是不允许的:

int x = 0;123 = x;  // error

上面的结论是显而易见的,从技术上来说是因为123是一个字面常量也就是一个右值,它没有一个具体的内存位置,所以这会把x分配到一个不存在的地方导致错误。下面的操作也是不允许的:

int *x = &123;

我们都知道取地址操作符&需要一个左值作为操作数,因为只有一个左值才拥有内存地址。一个右值常量并没有内存地址,所以取地址失败。

返回左值右值的函数

先看下面的一段程序:

int getValue() { return 123;}
getValue() = 456; // error

我们都知道赋值的左操作数必须是左值,因此,上面的错误原因很明显:getValue()返回了一个右值,一个临时值123,他不能作为一个赋值的左操作数。再看下面这段程序:

int global = 123;
int& getGlobal() { return global;}
getGlobal() = 456; // ok

该程序可以运行,因为在这里 getGlobal()返回一个引用(reference),跟之前的 getValue()不同。一个引用是指向一个已经存在的内存位置(global变量)的东西,因此它是一个左值,所以它能被赋值。注意这里的&:它不是取地址操作符,他定义了返回的类型(一个引用)。

左右值转换

左值转换为右值,这是合法且经常发生的,看如下程序:

int x = 1;int y = 2;
int z = x + y;

上面代码中x和y都是左值,但是经过加法操作后,x和y经历了一个隐式的左值到右值的转换,很多其他的操作符也有同样的转换——减法,加法,乘法等等。

左值引用

我们日常说的引用,通常指的是左值引用。引用是C++语法做的优化,引用的本质还是靠指针来实现的。引用相当于变量的别名。声明引用的时候必须初始化,且一旦绑定,不可把引用绑定到其他对象;即引用必须初始化,不能对引用重定义;左值引用的基本语法:type &引用名 = 左值表达式;我们直接看下面的代码:

int y = 1;int& yref = x;yref++;

这里将yref声明为类型int&:一个对y的引用,它被称作左值引用。现在你可以开心地通过该引用改变y的值了。我们知道,一个引用必须指向一个具体的内存位置中的一个已经存在的对象,即一个左值。这里y确实存在,所以代码运行完美。现在,如果我缩短整个过程,尝试将10直接赋值给我的引用,并且没有任何对象持有该引用,将会发生什么?

int& yref = 10;

在右边我们有一个临时值,一个需要被存储在一个左值中的右值。在左边我们有一个引用(一个左值),他应该指向一个已经存在的对象。但是10 是一个数字常量,没有内存地址,属于一个右值,这与引用的精神不符。因此上面这表达式是错误的。

如果你仔细想想,这就是从右值到左值的转换,是不被允许的。比如,一个volitile的数字常量(右值)如果想要被引用,需要先变成一个左值。如果那被允许,你就可以通过它的引用来改变数字常量的值。常量都能被改变,世界要大乱了啊。

下面的代码片段同样会发生错误,原因跟刚才的一样:

void fnc(int& x){}
int main(){ fnc(10); // error! // This works instead: // int x = 10; // fnc(x);}

常量左值引用

右值并不能取到地址,所以左值引用不能绑定到右值,但是常量左值引用为什么就可以呢?其中的数据在内存中的存储状况是怎么样的一个情况?

先看看GCC对于之前两个代码片段给出的错误提示:

error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'

GCC认为引用不是const的,即不是一个常量引用。根据C++规范,你可以将一个const的左值引用绑定到一个右值上,所以下面的代码可以成功运行:

const int& ref = 10;  // OK!

当然,下面的也是:

void fnc(const int& x){}
int main(){ fnc(10); // OK!}

背后的道理是相当直接的,字面常量10volatile的并且会很快失效,所以给他一个引用是没什么意义的。如果我们让引用本身变成常量引用,那样的话该引用指向的值就不能被改变了。现在右值被修改的问题被很好地解决了。同样,这不是一个技术限制,而是C++人员为避免愚蠢麻烦所作的选择。

应用:C++中经常通过常量引用来将值传入函数中,这避免了不必要的临时对象的创建和拷贝。

编译器会为你创建一个隐藏的变量(即一个左值)来存储初始的字面常量,然后将隐藏的变量绑定到你的引用上去。那跟我之前的一组代码片段中手动完成的是一码事,例如:

// the following...const int& ref = 10;
// ... would translate to:int __internal_unique_name = 10;const int& ref = __internal_unique_name;

理解左值和右值的含义让我弄清楚了几个C++内在的工作方式。C++11进一步推动了右值的限定,引入了右值引用(rvalue reference)和移动(move semantics)的概念。

参考

understanding-meaning-lvalues-and-rvalues-c


作者:Codemaxi
链接:https://juejin.cn/post/7050747444958593054
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


原文始发于微信公众号(汇编语言):C++学习笔记:左值、右值和左值引用

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年2月26日14:51:19
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   C++学习笔记:左值、右值和左值引用https://cn-sec.com/archives/1575549.html

发表评论

匿名网友 填写信息