C Programming Language

Expertise #03: Lvalue and Rvalue

# Basic Tutorial,  # Programming Language,  # C Programming Language

Last Updated: 2020-07-01


前置知识

在阅读本文档前,请确保你已经掌握类型的基础知识,详见 Basics #03: Types


前言

左值和右值的概念是 C 语言标准中有明确规定的,它们为语法中的一些常见的、基础的、易于解释的简单现象或结论提供了理论上的规范和解释。不知道是什么原因,国内的课程或教材极少会介绍左值和右值的相关概念,它们的概念并不复杂,却有着较为重要的意义,完全没有将其省略的必要。

对于 C 语言开发者而言,左值和右值的概念其实不是必要的,哪怕你对此一无所知,也并不影响平时的编程,但是对于帮助理解编译信息和深入学习 C 语言来说,了解这些概念是很有意义的。

(不过,在 C++ 语言中,左值和右值的概念对于学习 C++11 及以上的一部分新特性(例如右值引用)来说是必要的前置知识。另外,左值和右值的概念在 C 语言和 C++ 语言中有微妙的差异,还请注意。)


左值

基本概念

左值 (l-value, left value) 是指标识了对象的内存位置的表达式,它有时也被称为 locator value 。左值可以出现在赋值运算符 = 的左侧或右侧,通常表示为标识符。

左值所表示的内容一定是在内存中具有确切可识别的地址的,例如你通常定义的各类变量的名称几乎都是左值,而例如一些字面量整数值就不是左值。

如果左值所标识的内存位置是可修改的,则称其为 可修改的左值 (modifiable l-value) 。例如数组类型和被 const 修饰的类型就不是可修改的左值,因为你不能直接修改一个数组,也不能直接修改一个 const 变量的值。

“左值”这一名称最初来自赋值表达式,例如对于 a = b ,其中 a 必须是一个(可修改的)左值。

基于左值的概念,我们可以规范地解释大量的基础语法规则。例如:

int a, b;
const int c = 0;
a = 1;
a = b;
3 = a;
a + 1 = b;
c = b;

3, 43,\ 4 行的代码是合法的;第 5, 65,\ 6 行的代码会编译错误,因为 3a + 1 都不是一个左值;第 77 行的代码也会编译错误,因为 c 不是一个可修改的左值。

如果一个标识符引用一个内存位置并且其类型是算术、结构、联合或指针,则它是可修改的左值。例如对于指向了某个内存区域的指针 p 而言, *p 是可修改的左值,用于指定 p 所指向的内存。例如:

int a[5];
int * p = &a[0];
int * q;
*p = 1;
q = p + 2;
*(p + 1) = 3;
p + 1 = q;

类似地,第 77 行的代码会编译错误,因为 p + 1 都不是一个左值;而第 66 行则是正确的,因为 *(p + 1) 是合法的左值。

详细定义

在 C 语言中,左值必须是下列表达式之一:

  • 任何类型的变量的名称,例如整型、指针类型、结构体类型的标识符。

  • 下标表达式 [] 的结果,如果结果不是数组类型。

  • 对指针解引用的结果,如果结果不引用数组类型或函数类型。

  • 括号 () 中的左值。

  • const 修饰的表达式。

  • 通过 .-> 访问结构体或共用体类型的成员的结果。

除了括号的规则以外应该都很好理解,就不再列举代码示例了。

现在轻松一下——下面这段看起来很奇怪的代码其实是合法的,因为括号不会改变左值的性质:

int a;
(a) = 1;

下面是 C99 标准中对左值的原文定义:

6.3.2.1 Lvalues, arrays, and function designators 1 An lvalue is an expression with an object type or an incomplete type other than void; if an lvalue does not designate an object when it is evaluated, the behavior is undefined. When an object is said to have a particular type, the type is specified by the lvalue used to designate the object. A modifiable lvalue is an lvalue that does not have array type, does not have an incomplete type, does not have a const-qualified type, and if it is a structure or union, does not have any member (including, recursively, any member or element of all contained aggregates or unions) with a const-qualified type.


右值

右值 (r-value, right value) 通常是指存储在内存中某个地址的数据值,是无法为其分配值的表达式,它有时也被称为 value of the expression 。右值只能出现在赋值运算符 = 的右侧。

从概念上讲,右值只是一个数据值,在 C 语言中右值不像左值那样引用某个对象(但在 C++ 中右值并非一定不能引用对象)。

右值的描述看起来和左值很像,但实际上完全不同。左值强调的是标识出指定对象的内存位置,而右值所标识的内存地址通常是匿名的或临时的,所指的通常是表达式计算的中间结果,例如:

int a = 1;
int b = a + 1;

在 C 程序被转换为汇编语言后,第 22 行的代码会被分解为若干个步骤,其中 a + 1 会在过程中被存储在临时的位置(例如可能在寄存器中),这也就是右值的定义中所述的“存储在内存中某个地址”。但这个地址通常是无法由上层程序直接访问的,你不能去修改 a + 1 的值,例如令 a + 1 = 2

在 C 语言中右值的概念不太重要,事实上 C 标准甚至没有给出“右值”这一概念的明确定义,只将右值简单地称为 value of the expression 。所以在 C 语言中我们一般只需重点关注左值的相关问题。


实践作用

许多 C/C++ 编译器的编译信息中都引用了左值和右值的概念,了解左值和右值有利于我们阅读理解这些信息。例如:

int a = 1, b = 2;
a + 1 = b;

上述代码会报告如下编译错误:

|2| error: lvalue required as left operand of assignment

其字面意思为“需要左值作为赋值的左操作数”。如果你不理解左值是什么,看到这个报错很可能会一头雾水。而现在事情就简单多了,显然编译错误是因为你在赋值运算符的左侧误用了一个右值表达式 a + 1

当然,这种简单的问题不需要看懂报错也能解决,但在复杂的程序中,如果你理解左值和右值的概念,就可以非常高效地判明错误的来源,否则看到 lvalue 啥的你可能就要陷入迷惑了。

此外,因为 C/C++ 标准中事实上大量引用了左值和右值的概念,所以了解左值和右值有利于我们阅读相关文献和资料。一些复杂的问题可能很难用通俗的语言去描述,但是用左值和右值的理论就很容易描述清楚。