引用、左值与右值

稀土 2018-01-01

我的理解


引用是一个左值,而常量引用是一个右值。两者最关键的地方在于,左值可以被取到地址,而右值取不到地址,这个性质就决定了右值不能在 “=” 的左侧。

从汇编角度去理解这个问题的话,就很好理解了,左值是一个内存单元里的数据,我们可以直接寻址找到它,而右值是一个立即数或者是寄存器里的值。

C与C++中的左值、右值


C 中可以通过赋值号 “=” 简单地判断,比如

int a = ; // 表达式a是左值,字面值常量42是右值
    int b = ; // 表达式b是左值,字面值常量43是右值

    a = b; // 表达式a和表达式b都是左值
    b = a; // 表达式a和表达式b都是左值
    a = a * b; // 表达式a是左值, 表达式a*b是右值

    int c = a * b; // ok,表达式c是左值,表达式a*b是右值
    a * b = ; // error,表达式a*b是右值,右值不能出现在赋值操作符的左边
                //lvalue required as left operand of assignment

但是在 C++ 中,由于自定义类引入的一些新的特性,这些就没那么简单了。

当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。

C++中常见的左右值如下:

    • 赋值运算符(=)的左侧运算对象必须是一个非常量的左值,其结果也仍然是一个左值。
    • 递增和递减运算符(++和–)必须作用于左值运算对象,前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。
    • 箭头运算符(->)作用于一个指针类型的运算对象,结果是一个左值。
    • 点运算符(.)分成两种情况:
      • 如果成员所属的对象是左值,那么结果是左值。
      • 如果成员所属的对象是右值,那么结果是右值。
    • 条件运算符(? :)的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值;否则运算的结果是右值。
    • 取地址运算符(&)作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
    • 内置解引用运算符*、迭代器解引用运算符*、内置下标运算符[]、容器下标运算符[]的求值结果都是左值。
    • 算术运算符的运算对象和求值结果都是右值。
    • 逻辑运算符(!,&&,||)的运算对象和求值结果都是右值。
    • 关系运算符(<,<=,>,>=,==,!=)的运算对象和求值结果都是右值。
    • 函数的返回类型决定函数调用是否是左值。
      • 调用一个返回引用类型的函数得到左值。
      • 调用一个返回其他类型的函数得到右值。

记忆方法


我的记忆规则是左值持久,右值短暂。

左值是持久的,而右值要么是常量,要么是表达式求值所产生的临时对象。

两个错误例子


例一:在递归时

int func(Object &a) {
    if (...) return ...;         
    Object b; 
    func(b); //错误,b是一个临时变量,是右值,而引用需要的是左值  
}

//改进方法
int func(const Object &a) { //常量引用需要的是右值
    if (...) return ...;         
    Object b; 
    func(b); 
}

例二:参数左值、右值匹配

string func1 (string &s) {
    ...
    return string;        
}
const string func2 (string &s) {
    ...
    return "1234";        
}
int func3 (const string &s)  {
   ...
}

main () {
    strings s1 = "123456"; // s1 是左值
    string &s2 = func1(s1); //引用需要左值,而 func1() 返回左值,所以正确
    func3(s1); // func2() 需要的是常量引用,是右值,而 s1 是左值,但是C++有可变性,可以将左值传给右值,所以编译器可以通过。
    string &s3 = func2(s1); // func2()返回的是右值,而 s3需要的是左值,所以出错    
}