关于“左值”和“右值”

[!attention] 注意
本文讨论的上下文仅适用于 C 语言,不适用于 C++。C++ 关于左值和右值的理解略有不同,尤其是在 --ii-- 这些表达式的左右值分类上面存在差异。对于这部分的粗浅讨论参见最后一部分 与 C++ 的区别

定义

左值和右值的核心都是值,也就是数据。而具体的“左”和“右”是按照它们在运算,尤其是赋值运算 (使用赋值运算符 =) 中这些值的位置来确定的。后面会谈到关于这个位置定义的局限性。

习惯上我们讨论左值和右值,实际上是在讨论左值和右值表达式。因为对于一个变量,我们可以很容易区分其左值和右值属性。关于这一点,可以从后文 从存储类别的角度看左值与右值 得出结论。并且,在大部分资料中,左值/右值左值表达式/右值表达式 的概念常被混用,或者不作具体区分。我们可以认为它们的含义是等价对应的。

左值表达式

前文提到按照在复制表达式的左右位置来判定是存在历史性的。因为最早 CPL 编程语言中就是将复制表达式左边的表达式定义为左值表达式。但随着高级语言的发展,这个定义已经不再完备,所以我们可以按照以下的情况来判定一个表达式是否为左值表达式:

  • 标识符 (包括代表了存储对象的函数形参,关于对象的内容参见另一篇讨论存储类别的文章);
  • 字符串字面量;
  • 复合字面量 (compound literal);
  • 对左值表达式的外部加括号;
  • 左操作数是左值的成员访问 (.) 表达式;
  • 成员访问指针 (->) 表达式;
  • 对左值指针 (即指向对象的指针) 使用一元解引用 (*) 表达式;
  • 下标运算的结果 ([]);

可修改的左值表达式

可修改的左值表达式能够用于自增和自减运算符的操作数、赋值和复合赋值表达式的左操作数。其含义就是对应的内存中的数值可以被修改的表达式。以下左值表达式都属于可修改的左值表达式:

  1. 属于非数组且完全的类型[1],并且没有使用 const 修饰的左值表达式;
  2. 所有成员 (包括嵌套成员) 都没有使用 const 修饰的结构体和联合;

右值表达式

Microsoft 的一篇博文 (L-Value and R-Value Expressions,中文翻译版 左值和右值表达式),其中有一句话:

所有的左值都是右值,而并非所有的右值都是左值。

这句话其实是有歧义的。因为右值的定义本身就是根据左值来判定的。也就是在 C 语言标准中,通过 非左值 (non-lvalue) 来定义右值 (rvalue)。个人理解 Microsoft 博文中这句话想要表达的是,所有的左值都可以 作为右值使用,但不是所有右值都可以 当做左值使用

按照 C 语言标准定义,以下的所有表达式都属于 非左值表达式右值表达式

  • 所有函数调用表达式,如 int val = foo();
  • 所有类型转换表达式 (注意区分复合字面量 (compound literal),它们属于左值表达式);
  • 对非左值的结构体、联合的成员访问表达式,如 foo().mem1(struct1, struct2).mem1;
  • 所有算数运算、关系运算、逻辑云算、位运算的结果;
  • 所有自增和自减表达式的结果;
  • 赋值运算的结果;
  • 条件运算的结果,如 a == b ? 1 : 2;
  • 逗号运算的结果,如 a, b;
  • 取址运算的结果 (即使是对一维解引用表达式取值也不行),如 &(*ptr_a) = 0x844FDAC&ptr_a = 0x844FDAC;

此外,如果一个表达式的类型是 void,那么它也是一个非左值表达式。因为我们认为这个表达式产生的值既没有相应的呈现方式 (representation) 也不需要分配存储空间。

从存储类别的角度看左值与右值

关于存储类别我们另外用一篇文章来讲解,存储类别与左值和右值相关的地方仅仅在于它提供的一种观察 (定义) 值类别的视角——即值的存储和取用。

在 C 语言中,存储了某个或某些值的一块物理内存空间被称为 对象 (object)。如果一个标识符指定了一个 对象的内容,那么这个标识符就一定是左值。因为它代表了具有实际存储空间的值。回看上面的定义,如果一个表达式的运算结果是一个分配了内存地址和空间的值,那么它就是一个左值。

至于是否允许修改,这需要看是否满足 可修改的左值 相关的判定条件。即 是否可以通过这个左值修改对象中的值

我们以字符串字面量为例:

char * str = "Hello World!";

str 是一个标识符,并且是一个自动变量,需要注意变量的存储期和值的存储期是不同的[2]。这个标识符 str 本身代表了静态存储的一个内存区域的低地址,即它的值是 'H' 在内存中的地址。同时,从内存分布来看,字符串 "Hello World!" 的每个字符 (包括结束符 '\0') 也是具有单独地址的。因此不论是字符串字面量整体,还是组成它的每个个体都是对象,即对象是可以被嵌套旳。按照这个原理:

char * str = "Hello World!";
char str1 = str[1];

标识符 str1 也指代了一个对象,即字符 'e'

与 C++ 的区别

以下 C 语言中为右值的表达式在 C++ 中被认为是左值:

  • 前导形式的自增和自减,如 --i++i;
  • 复制表达式的结果;
  • 第二和第三操作数是 同一类型的两个左值 的条件运算的结果,如 a == b ? a : b;
  • 第二操作数是左值的逗号表达式,如 1, b;

参考资料

  1. 值类别 (C 语言)
  2. 值类别 (C++ 语言)
  3. 字符串字面量 (C 语言)

  1. 除了没有完整定义的结构体、联合,以及没有指定维度的数组之外的,都是完全类型。 ↩︎

  2. 字符串字面量的存储期是静态的 (static storage duration),因为 C 语言标准规定了字符串字面量都必须是不可修改的,视编译器的实现,通常会被放置在 .rodata.text 段中,都是全局可用且不可修改的数据。 ↩︎

posted @ 2023-11-25 19:00  那阵东风  阅读(19)  评论(0编辑  收藏  举报