什么是左值、右值呢?一种极不严谨的理解为:在赋值的时候,能够被放到等号左边的值为左值,放在右边的值为右值。例如:
int sum(int x, int y){return x + y;}
int a = 1; //a为左值,常数1为右值
int b = a + a; //b为左值,表达式a+a为右值
int c = sum(a, a);//c为左值,但函数sum(a, a)返回值为右值
通过上面的例子,常数a
、表达式(a+a)
和函数sum(a+a)
返回值他们都是临时值,这些值都保存在寄存器中,无法取到他们的地址;而对于a
、b
和c
为具体的变量名,存储在内存中,可以取到其地址。因此一般情况下可以根据能否取到地址,来区分左值和右值。
在了解左值和右值之前,我们首先要知道表达式的概念:由运算符和运算对象构成的计算式(类似数学中的算术表达式)。表达式是可以求值的,因此根据表达式值的类别,可以对其进行分类(准确的来说,是表达式的结果的值类别,但我们一般不刻意区分表达式和表达式的求值结果,所以这里称“表达式的值类别”。),C++11之后将表达式定义了五种类型:
它们之间的关系如下图所示:
C++11中将表达式按值类别可以分为左值、将亡值和纯右值。其中,左值和将亡值合称为泛左值,纯右值和将亡值合称为右值。
随着移动语义(后面我们会详细介绍)引入到 C++11 之中,值类别被重新进行了定义,C++之父Bjarne Stroustrup在《“New” Value Terminology》中给出以区别表达式的两种独立的性质:
C++11 中:
一般情况下,左值我们可以简单地理解理解为:能够使用&
取地址的表达式。
常见的左值有:
++i
/--i
)a=b
,a+=1
)【注:字符串是可以取地址的,因此字符串常量也属于左值】
纯右值:表达式本身就是纯粹的字面值(如1
,ture
,1.0
);或者,该表达式求值结果相当于一个字面值或一个不具名的临时对象。
常见的纯右值有:
i++
/i--
)a+b
,a&&b
,a==b
)&a
)将亡值是在C++11中引进来的,顾名思义,就即将销毁的东西。将亡值的产生与右值引用的产生而引起的,对于将亡值我们常用到的有:
std::move(x)
)static<int&&>(a)
)左值引用就是对左值的引用。它的形式如:T&
,根据const属性可以分为两种:
例如:
int a = 1;
int& la = a;//la为a的左值引用(非const左值引用)
la = 2;//la为非const左值引用,可以修改它的值
const int& c_la = a;//c_la为a的左值引用(const左值引用)
c_la = 2;//该语法错误,c_la为const左值引用,不可以修改它的值
右值引用就是对右值的引用,通过T&&
来表示。右值的引用只能绑定到右值上。
在未出现右值引用之前,我们在函数调用传参的时候,在某些时候可以使用按引用传递参数,减少参数多的拷贝对资源的消耗,提高程序的运行效率。当我们在处理包含大量数据的对象时,移动语义显的尤为重要。
如何将一个左值转换为一个右值呢?C++11在头文件utility中声明了std::move()
函数,该函数的作用就是类型转换,通过它,我们可以 把一个左值,将其标记为右值。move()
不做任何资源转移的操作,只是产生一个将亡值表达式来标识参数x
,其完全等同于static_cast<T&&>(x)
。例如:
int a = 1;
int&& r_a = a; //错误,右值引用只能绑定到右值上,而a是一个左值
int&& r_a = std::move(b); //正确, std::move(a) 是一个右值,可以用右值引用绑定
一个类 T
的首个形参是 T&&、const T&&
、volatile T&&
或 const volatile T&&
,且没有其他形参,或剩余形参都有默认值。
具体的形式如下:
T (T &&) //移动构造函数的典型声明形式
T (T &&) = default; //强制编译器生成移动构造函数。
T (T &&) = delete; //避免隐式生成移动构造函数。
示例:
#include <string>
#include <iostream>
#include <utility>
class A
{
private:
std::string s;
public:
A(std::string str = "A()") : s(str) {std::cout<<s<<"的构造函数\n";}
A(A&& o) : s(std::move(o.s)) {std::cout<<s<<"的移动构造函数\n";}
~A(){std::cout<<s<<"的析构函数\n";}
};
A f(A a) { return a; }
int main()
{
A a1(f(A("a")));// 按值返回时,从函数形参移动构造它的目标
A a2(std::move(a1));// 从亡值移动构造
}
一个类 T
的移动赋值运算符是名为 operator=的非模板非静态成员函数,它接受恰好一个 T&&
、const T&&
、volatile T&&
或 const volatile T&&
类型的形参。
具体的形式如下:
T & T ::operator= (T &&) //移动赋值运算符的典型声明
T & T ::operator= (T &&) = default; //强制编译器生成移动赋值运算符
T & T ::operator= (T &&) = delete; //避免隐式移动赋值
示例:
#include <string>
#include <iostream>
#include <utility>
class A
{
private:
std::string s;
public:
A(std::string str = "A()") : s(str) {std::cout<<s<<"的构造函数\n";}
~A(){std::cout<<s<<"的析构函数\n";}
A& operator=(const A& other)
{
s = other.s;
std::cout << "复制赋值\n";
return *this;
}
A& operator=(A&& other)
{
s = std::move(other.s);
std::cout << "移动赋值\n";
return *this;
}
};
A f(A a) { return a; }
int main()
{
A a1("a1"), a2("a2");
std::cout << "尝试从右值临时量移动赋值 A\n";
a1 = f(A("a")); // 从右值临时量移动赋值
std::cout << "尝试从亡值移动赋值 A\n";
a2 = std::move(a1); // 从将亡值移动赋值
}
参考文献 C++ Primer Plus(第六版) - 第18章 探讨C++新标准 C++ 参考手册