左值右值是C++和C中共有的基础概念,包括课本和多数的C++辅导书都不会对其进行细致的讲解,我们日常在使用C++时也不会注意到。但是理解它的意义能让我们更深层次的理解为什么一些我们认为正确的语法却会被无情的报错,而且在C++11中,更新了一系列与左值右值概念相关的新语法,理解这一概念也能让我们与时俱进。
简单来说,左值(lvalue)指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式);
右值(rvalue)指的则是只能出现在等号右边的变量(或表达式)。左值可以被修改,右值则不可以。
int f()
{
return 5;
}
int main()
{
int a;
a = 5;
5 = a; //报错 表达式必须是可修改的左值
a = f();
f() = 5; //报错 表达式必须是可修改的左值
}
如果让一个没有学习过编程的人看这两句奇怪的语句,他可能不会觉得有任何问题(一个事物和另外一个事物相等,两者必然是互等的啊),虽然这里的等于并不是相等的意思…
而让有编程经验的人看,就会一眼看出来问题,但是他不一定能说出其中的道理——可能只会说是老师和经验让他不要这样写…
为了更深刻地理解这一性质,我查阅了C++11中对左值和右值的描述。
在C++11中,可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值);
右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value);
这和之前我们的描述有所不同,即真正决定左值右值的不是它们在等式的左边还是右边,而是它们是否可以取地址,是否有名字。在等式的左右两边只是一种判断方法。
int a =10; //a是左值 是可以被取地址
f(); //f()是右值 没有被分配内存 自然也没有对应的地址
有一点是很明显的,代码中计算得到的数据的存储位置是不一样的。
定义好了的变量会被分配一定的内存,我们可以在这个内存中存放变量的数据,也可以通过这个内存的地址找到这个数据。
int a = 10; //定义一个变量 分配给它对应的内存
int* p = &a; //取得这个变量被分配的地址
*p = 20; //通过地址对这个变量进行操作
而以下的代码则会报错
int* var = &10;//报错 表达式必须为左值
这是因为10只是一个立即数(数电的概念),它的生命周期和函数栈帧是一致的,在函数运行结束以后就会被清空,一般它只会被存放在寄存器中,而不会占用内存空间,自然就找不到它的地址,无法找到它并进行操作。
将亡值是C++11新增的概念,添加这个概念是因为同期添加一个重要的概念:右值引用;
将亡值通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值,具体在下面的右值引用中讲述,此处不再赘述。
左值和右值的概念是在汇编阶段就存在的,而具体运用左值和右值的概念,则是在C++11添加了右值引用这个概念之后。
先看一下传统的左值引用。
int a = 10;
int &b = a; // 定义一个左值引用变量
b = 20; // 通过左值引用修改引用内存的值
引用就是一个别名,这里面的b就是a的一个别名,对b操作就是对a进行操作;
int &var = 10;
上述代码是无法编译通过的,因为10是一个右值,没有地址,就无法对其进行引用;
const int &b = 10;
使用常引用来引用常量数字10,因为此刻内存上产生了临时变量保存了10,这个临时变量是可以进行取地址操作的,因此b引用的其实是这个临时变量,相当于下面的操作:
const int a = 10;
const int &b = a;
左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用;
但使用常引用后,我们只能通过引用来读取数据,无法去修改数据,因为其被const修饰成常量引用了。
右值引用是C++11新增的特性。它可以将被引用的右值表达式的生存期延长到引用的生存期;
C++11定义右值引用的格式如下:
类型 && 引用名 = 右值表达式;
举一个例子
#include
using namespace std;
int f(int *a,int i)
{
return a[i];
}
int main()
{
int a[2]{ 1,2 };
int&& var = f(a,1);
var = 5; //正确 右值引用可以进行读写操作
const int& bar = 10;
bar = 100; //报错 常引用无法进行写操作 只能进行读操作
}
我们可以清楚的看到,右值引用的最大特性就是可以进行读写操作。
可是目前来看 它的作用还不是很明确,我们根本没必要花这么多力气去把一个没有存入内存的值进行引用来实现这个引用的读写,这看起来真的很傻,那右值引用的作用到底是什么呢?
C++有很容易操作的拷贝构造函数和析构函数,如果你需要将一个对象中的数据从一个对象中传递给另外一个对象,你只需要执行一个拷贝,再将原来的对象析构,这个流程看起来没有任何问题,但是如果从代码中走出,观察内存的调用,你就会发现这一操作是很蠢的。我们只需要数据的移动,C++却必须 拷贝->析构。
为了将“移动”这个概念在C++中实现,移动语义就出现了。右值虽然没有内存,在使用中看起来比左值有很多缺点,但是它不占用内存的这一点在移动这一概念中却有着巨大的作用。只需要一个右值引用,我们就能让痴迷于性能的C++开发者欣喜若狂。
#include
#include
#include
#include
using namespace std;
int main()
{
string str = "Hello";
vector<string> v;
//调用常规的拷贝构造函数,新建字符数组,拷贝数据
v.push_back(str);
cout << "After copy, str is \"" << str << "\"\n";
//调用移动函数 将str变为右值引用
v.push_back(std::move(str));
cout << "After move, str is \"" << str << "\"\n";
cout << "The contents of the vector are \"" << v[0]<< "\", \"" << v[1] << "\"\n";
}
move:放弃左值对资源的占用,而将左值变为右值引用;
在使用右值引用后,就可以让移动这一操作变得如此优雅——对性能痴迷者而言。
也许对我而言,我很难会用到右值引用,用到移动函数,但是对一些写C++库的程序员,右值引用带来的效率提高是为他们所重视的。而且,更加深刻地理解了变量常量的实质,也让我受益良多。
《深入理解C++11:C++11新特性解析与应用》
https://stackoverflow.com/questions/4986673/c11-rvalues-and-move-semantics-confusion-return-statement
《C++ Prime》
简书:C++11 std::move和std::forward
https://www.jianshu.com/p/b90d1091a4ff