关于“左值”和“右值”
[!attention] 注意
本文讨论的上下文仅适用于 C 语言,不适用于 C++。C++ 关于左值和右值的理解略有不同,尤其是在--i
和i--
这些表达式的左右值分类上面存在差异。对于这部分的粗浅讨论参见最后一部分 与 C++ 的区别。
定义
左值和右值的核心都是值,也就是数据。而具体的“左”和“右”是按照它们在运算,尤其是赋值运算 (使用赋值运算符 =
) 中这些值的位置来确定的。后面会谈到关于这个位置定义的局限性。
习惯上我们讨论左值和右值,实际上是在讨论左值和右值表达式。因为对于一个变量,我们可以很容易区分其左值和右值属性。关于这一点,可以从后文 从存储类别的角度看左值与右值 得出结论。并且,在大部分资料中,左值/右值 与 左值表达式/右值表达式 的概念常被混用,或者不作具体区分。我们可以认为它们的含义是等价对应的。
左值表达式
前文提到按照在复制表达式的左右位置来判定是存在历史性的。因为最早 CPL 编程语言中就是将复制表达式左边的表达式定义为左值表达式。但随着高级语言的发展,这个定义已经不再完备,所以我们可以按照以下的情况来判定一个表达式是否为左值表达式:
- 标识符 (包括代表了存储对象的函数形参,关于对象的内容参见另一篇讨论存储类别的文章);
- 字符串字面量;
- 复合字面量 (compound literal);
- 对左值表达式的外部加括号;
- 左操作数是左值的成员访问 (
.
) 表达式; - 成员访问指针 (
->
) 表达式; - 对左值指针 (即指向对象的指针) 使用一元解引用 (
*
) 表达式; - 下标运算的结果 (
[]
);
可修改的左值表达式
可修改的左值表达式能够用于自增和自减运算符的操作数、赋值和复合赋值表达式的左操作数。其含义就是对应的内存中的数值可以被修改的表达式。以下左值表达式都属于可修改的左值表达式:
- 属于非数组且完全的类型[1],并且没有使用
const
修饰的左值表达式; - 所有成员 (包括嵌套成员) 都没有使用
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
;