左值与右值

左值与右值,很早就知道了,但是一致没具体的搞清楚到底怎么回事儿,这次深入探究一下。

起源

据说左值和右值是从 C 语言带来的概念,原本的用途是用来帮助记忆:赋值语句左边的就是左值,右边的就是右值😂。但在 C++ 中,就完全不是这么一回事了。
尽管大家都是在 C/C++ 中议论这俩概念,但个人看来,这个概念应该也可以从其他高级程序语言中体现出来。那为什么有些其他语言基本没有强调这个内容呢?显然,语言本身肯定是越简单越好的,用户只需要学习他需要的、且能使用的概念就行了。
至于在 C/C++ 中,为什么要学习那么多的概念、语法。显然,是为了避免写出 BUG😑。
好了,闲话少说,我们一个一个来。

分类

首先应该明确的是,在 C++11 中,每个表达式(运算符及其操作数、字面量、变量名等)都有两个独立(意味着二者无关)的属性描述:类型(type)值类别(value category)。注意这句话,类型和值类别无关,且都是表达式的属性描述。也就是说,类型和值类别是在不同层面上对表达式进行属性描述。而且,我们同时还限定了是在 C++11 中,这意味着,在 C++11 之前,C++ 没有明确清晰的左右值概念。

类型(type)

类型,我们很容易理解,比如我们常说的这个变量是int类型的等等。而一般来讲,C++ 类型系统可以分成两类:

  1. 基本类型,诸如intdoublechar等。
  2. 复合类型,诸如指针、引用、数组、各种容器、用户自定义类等。

这块内容都是稍微熟悉一点的内容,就不继续介绍了。

值类别(value category)

在 C++11 之前,值类别这个概念就已经出现了,直到 C++11 才正式引入。值类别将表达式的属性分为三类:

  1. 纯右值(prvalue)
  2. 将亡值(xvalue)
  3. 左值(lvalue)

下面我们分别来看一下。

左值

左值一般指的是指向内存位置的表达式(有明确地址),且能长期存在(生命周期超过单个表达式)。
PS:虽然有内存位置,但不一定能通过&得到,比如下面会提到的右值引用。
我们可以举一些常见的例子:

  1. 变量、函数或数据成员的名称,即便变量的类型是右值引用,由其名称组成的表达式也是左值表达式,比如:

    1
    2
    3
    int a; // a 是左值
    void func() {} // func 是左值,因为 &func 可以得到函数的地址
    int&& rra = a; // rra 是右值引用,但也是左值
  2. 函数调用或重载运算符表达式,其返回类型是左值引用,比如:

    1
    std::cout << 1; // std::cout << 1 这个表达式返回的值也是左值
  3. 所有内置类型的赋值和符合赋值表达式,比如:

    1
    2
    3
    4
    a = b
    a += b
    a %= b
    // PS:因为 a = b 表达式式左值,所以它支持 a = b = c = ... 类似这样的表达式
  4. 内置类型和迭代器的前置自增、前置自减运算符的求值结果也依然是左值,比如:

    1
    2
    3
    int a;
    ++a
    --a
  5. 解引用运算符*、下标运算符[]、迭代器解引用运算符的求值结果都是左值,比如:*parr[]等。

  6. 内置的指针成员、对象成员表达式,比如:p->a.xxp->*mp等。
  7. 逗号表达式和三目表达式则以最后结果的值类别作为整个表达式的值类别,比如:

    1
    2
    a, b // b 是左值,则结果为左值
    a ? b : c // b 和 c 是左值,则结果一定为左值
  8. 特别指出:字符串字面量是左值,比如:

    1
    std::string s = "hello"; // "hello" 就是左值,注意说的是 hello 不是 std::string 类型的变量 s
  9. 还有其他情况…

情况很多,不推荐死记,遇到了,查阅资料,结合具体代码分析即可。

纯右值

纯右值一般是指不可修改、无持久内存地址的临时值
我们也举一些常见的例子:

  1. 字面量(除字符串外),比如:

    1
    2
    3
    int a = 42; // 42 是纯右值
    bool flag = false; // false 是纯右值
    int* p = nullptr; // nullptr 是纯右值
  2. 函数调用或重载运算符表达式,其返回类型是非引用,比如:

    1
    2
    3
    std::string s, ss;
    s.substr(1, 3)
    s + ss
  3. 内置类型的后缀递增、递减表达式,比如:a++a--

  4. 内置类型的算术表达式、逻辑表达式、比较表达式、取地址表达式,比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    int a, b;
    // 算术表达式
    a + b
    a - b
    a % b
    a & b
    // 逻辑表达式
    a && b
    a || b
    !b
    // 比较表达式
    a > b
    a < b
    a <= b
    // 取地址表达式
    &a
  5. 对象成员表达式中成员为成员枚举数或非静态成员函数,比如:

    1
    2
    3
    // m 为成员枚举数或非静态成员函数
    a.m
    p->m
  6. 对象成员指针表达式中成员为指向成员函数的指针,比如:

    1
    2
    3
    // m 指向成员函数的指针
    a.*m
    p->*m
  7. 上面提到过的逗号表达式和三目表达式、this指针。

  8. 还有很多其他情况...

一样,不要死记,遇到了,查阅资料,结合具体代码分析即可。

将亡值

将亡值一般是指即将被销毁但可重用其资源的对象。这个含义有两层意思:即将被销毁(说明也是临时值)资源可重用
我们还是举一些常见的例子:

  1. 对象成员表达式和对象成员指针表达式,比如:

    1
    2
    a.m // a 是右值,m 是对象类型的非静态数据成员
    a.*mp // a 是右值,mp 是指向数据成员的指针
  2. 逗号表达式和三目表达式,比如:

    1
    2
    a, b // b 是将亡值
    a ? b : c // 最终表达式的结果取决于 b c
  3. return语句,比如:

    1
    2
    3
    type func() {
    return expression; // expression 的值会被隐式转换为函数返回值类型,最终得到一个将亡值
    }
  4. 还有其他情况...

同样,还是不推荐记忆,遇到问题,查文档,结合具体代码分析即可。

总结

最后,我们总结下文章的内容。
在这篇文章中,我们讨论了 C++ 中左值右值的来历,知道了从 C++11 版本开始引入值类别的概念,且它并不是类型的补充,二者都是表达式属性的独立描述。
但我们在文章中介绍的是左值、纯右值和将亡值,并没有介绍右值,那右值具体是什么呢?
实际上,将亡值可以是左值,也可以是右值。何时为左值,何时为右值取决于具体的情况,需要结合具体代码分析。

另外,还想说的是:千万不要太过于纠结这些令人困惑的东西,不要陷入语言的语法陷阱中,至少从个人的角度来看,这纯纯是设计的问题。有些地方其实也不需要去理解,把它当作语言特性就可以了,就像英语里面的固定搭配一样😑。
最后,附上CPPReference 中文参考手册😴。


Buy me a coffee ? :)
0%