本文部分内容来自C++ primer,记录一下。
左值和右值
左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体;
右值当在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。
一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。
左值右值翻译:
L-value中的L指的是Location,表示可寻址。Avalue (computer science)that has an address.
R-value中的R指的是Read,表示可读。in computer science, a value that does not have an address in a computer language.
int a=3;
const int b=5;
a=b+2; //a是左值,b+2是右值
b=a+2; //错!b是只读的左值但无写入权,不能出现在赋值符号左边
(a=4) +=2; //a=4是左值表达式,2是右值,+=为赋值操作符
34=a+2; //错!34是字面量不能做左值
引用
引用的本质还是靠指针来实现的。引用相当于变量的别名。
引用(reference)分为左值引用和右值引用,通常我们说引用,指的是左值引用。
1.左值引用
引用为对象起了另外一个名字,引用类型引用另外一种类型,通过将声明符写成&d
的形式来定义引用类型,其中d
是声明的变量名:
int ival = 1024;
int &refVal = ival;
int &refVal2;//Declaration of reference variable 'refVal2' requires an initializer
一般在初始化变量时,初始值会被拷贝到新建的对象中,然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用,一旦初始化完成,引用将和它的初始值对象一致绑定在一起,因为无法令引用重新绑定到另外一个对象,所以引用必须初始化,类似于const
定义的常量。
注意:引用即别名,引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。
定义一个引用后,对其进行的所有操作都是在与之绑定的对象上进行的:
refVal = 2;
int ii = refVal;
为引用赋值,实际上是把值赋给了与引用绑定的对象上,获取引用的值,实际上是获取了引用绑定的对象的值,以引用为初始值,实际上以引用绑定的对象为初始值:
int &refVal3 = refVal;//绑定到ival
int i = refVal;//ival的值初始化
引用的定义
int i=2, &ref=i;
int &refVal = 10 // Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
注意:非const引用类型的初始值必须是对象。
常量左值引用可以被赋右值。 因为它们是常量,不能通过引用被修改,因此修改一个右值没问题。这使得C++中接受常量引用作为函数形参成为可能,这避免了一些不必要的临时对象的拷贝和构造。
2.右值引用
一个右值表达式表示的是对象的值
int i=42;
int &r=i; //正确,r引用i
int &&rr=i //错误,不能将一个右值引用绑定到一个左值上
int &r2=i*42; //错误,i*42是一个右值
const int &r3=i*42; //正确,我们可以将一个const的引用绑定到一个右值上
int &&r2=i*42; //正确,将rr2绑定到乘法结果上
左值持久,右值短暂
左值有持久的状态,而右值要么是字面值常量,要么是表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,我们得知:
1.所引用的对象将要被销毁
2,.该对象没有其他用户
这两个特征意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
变量是左值
变量可以看作只有一个运算对象而没有运算符的表达式,虽然我们很少这样看待变量。类似于其他任何表达式,变量表达式也有左值/右值属性。变量表达式都是左值,带来的结果就是,我们不能将一个右值引用绑定到一个右值引用类型的变量上。
int &&rr1 =1; //正确,字面值常量是右值
int &&r2 =rr1; //错误,表达式rr1是左值,因为我们可以给rr1赋值 rr1=2
虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显式地将一个左值转换为对应的右值引用类型。我们可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility中。
int&& r1 = 1;
int&& r2 = std::move(r1); //OK
move调用告诉编译器:我们有一个左值,但我们希望像右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr1赋值或者销毁之外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。
注意:
1.我们可以销毁一个移后源对象,也可以赋予它新值,但是不能使用一个移后源对象的值。
2.对于move的使用应该是std:move而不是move。这样做可以避免潜在的名字冲突。
3.左值右值转换
通常来说,语言构造一个对象的值要求右值作为它的参数。例如,二元加运算符 ‘+’ 要求两个右值作为它的参数并且返回一个右值:
int a = 1; //a是一个左值
int b = 2; //b是一个左值
int c = a + b; //+需要右值,左值a和b都转换成右值,并且返回一个右值
a + 1 = 2; //错误!表达式必须是左值,而a+1为右值
左值可以转换成右值(当做右值用),而右值不能转换成左值(即右值不能当做左值用)。
但是右值可以产生左值,例如一元运算符*
(解引用)可以以一个右值为参数产生左值作为结果。
int arr[] = {1, 2};
int* p = &arr[0];
*(p + 1) = 10; //对的:p+1是一个右值,但是*(p+1)是一个左值
相反的,一元取地址符 &
拿一个左值作为参数并且生成一个右值:
int var = 10;
int* bad_addr = &(var + 1); //错误:‘&’运算符要求一个左值
int* addr = &var; //正确:var是左值
&var = 40; //错误:赋值运算符的左操作数要求一个左值
加深理解
例1
int foo() { return 2; }
int main()
{
foo() = 2;
return 0;
}
运行:
test.c: In function 'main':
test.c:8:5: error: lvalue required as left operand of assignment
原因是等号左侧要求一个左值,而函数返回一个右值
例2
int& foo() { return 2; }
int main()
{
foo() = 2;
return 0;
}
运行:
testcpp.cpp: Infunction 'int& foo()':
testcpp.cpp:5:12: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
赋值运算符的左部分是一个来自右值的左值引用,无法赋值
不是所有的对函数调用结果赋值都是无效的。比如,C++的引用(reference)让这成为可能:
int globalvar = 20;
int& foo()
{
return globalvar;
}
int main()
{
foo() = 10;
return 0;
}
这里foo返回一个引用,这是一个左值,所以它可以被赋值。实际上,C++从函数中返回左值的能力对于实现一些重载运算符时很重要的。一个普遍的例子是在类中为实现某种查找访问而重载中括号运算符 []。std::map
可以这样做。
给 mymap[10]
赋值是合法的因为非const的重载运算符 std::map::operator[]
返回一个可以被赋值的引用。
std::map<int, float> mymap;
mymap[10]=5.6;
例3
例3可以帮助理解右值引用帮助减少赋值的重复构造操作。
作为一个例子,考虑下面一个简单的动态 “整数vector” 实现。
class Intvec
{
public:
explicit Intvec(size_t num = 0)
: m_size(num), m_data(new int[m_size])
{
log("constructor");
}
~Intvec()
{
log("destructor");
if (m_data) {
delete[] m_data;
m_data = 0;
}
}
Intvec(const Intvec& other)
: m_size(other.m_size), m_data(new int[m_size])
{
log("copy constructor");
for (size_t i = 0; i < m_size; ++i)
m_data[i] = other.m_data[i];
}
Intvec& operator=(const Intvec& other)
{
log("copy assignment operator");
Intvec tmp(other);
std::swap(m_size, tmp.m_size);
std::swap(m_data, tmp.m_data);
return *this;
}
private:
void log(const char* msg)
{
cout << "[" << this << "] " << msg << "\n";
}
size_t m_size;
int* m_data;
};
[000000C5888FFC90] constructor //Intvec v1(20)
[000000C5888FFC70] constructor //Intvec v2 默认构造
assigning lvalue...
[000000C5888FFC70] copy assignment operator //Intvec& operator=(const Intvec& other)
[000000C5888FFC80] copy constructor //Intvec(const Intvec& other)
[000000C5888FFC80] destructor //尾号80 调用析构函数
ended assigning lvalue...
[000000C5888FFC70] destructor
[000000C5888FFC90] destructor
虽然这里我只是赋一个刚刚构造的vector,但是这只是真是证明一个非常普遍的例子,一些临时的右值被构造然后被赋值给 v2(比如,这可能发生在函数中返回一个vector)。输出:
[0000007CAFDEF840] constructor
assigning rvalue...
[0000007CAFDEF820] constructor // 右值 构造
[0000007CAFDEF840] copy assignment operator
[0000007CAFDEF830] copy constructor // 再次构造
[0000007CAFDEF830] destructor
[0000007CAFDEF820] destructor
ended assigning rvalue...
[0000007CAFDEF840] destructor
可以发现,多了一对额外的构造/析构调用。不幸的是,这是个额外工作,没有任何用,因为在拷贝赋值运算符的内部,另一个临时拷贝的对象在被创建和析构。
C++11给我们右值引用可以实现“移动语义”,我们来添加另一个 operator= 到 Intvec :
Intvec& operator=(Intvec&& other)
{
log("move assignment operator");
std::swap(m_size, other.m_size);
std::swap(m_data, other.m_data);
return *this;
}
&& 语法是新的右值引用。掏空右值对象的值。输出为:
[0000004164BAFE30] constructor
assigning rvalue...
[0000004164BAFE20] constructor
[0000004164BAFE30] move assignment operator
[0000004164BAFE20] destructor
ended assigning rvalue...
[0000004164BAFE30] destructor