一、概述
左值是一个很让人困惑的概念,通常一条赋值表达式,例如x = y; 左边的操作数一定要是一个左值才能够被赋值,否则编译器就会报错:
error: lvalue required as left operand of assignment
要搞清楚左值的含义,首先要理解C语言的“对象”这一概念:
在C语言中,对象(object)指的是在内存中的一个位置,其内容可以用来表示某个值。
左值,指的就是内存中有具体位置的对象。
对象能出现在赋值表达式的左边进行赋值操作,所以它是一个左值。
有些表达式,它只产生一个值,却没有指示一个对象,这种表达式就是右值。
左值可以出现在赋值表达式的任意一边,而右值就只能出现在右边。
左值一定可以被解析出对应对象的地址,除非此对象是位字段,或者被类型限定符定义为const了。
左值的运算符包括下标运算符[]和间接运算符*。
C语言规定函数的返回值始终不是左值(C++会有例外情况)。
二、示例
1.比方说声明一个变量int x = 6;
x就是左值,它在内存中的地址是:&x,指针类型是int*。它是一个有位置的对象。
(x+1)则不是一个左值,这个表达式是x中保存的一个int类型数据(即6)加上1的结果,它代表一个值,它并不是内存中有具体位置的对象。
这意味着你不能这样为它赋值:(x+1) = 8;
2.上面是一个很简单的示例,但通常事情会显得相对复杂一点:
例如数组int arr[3] = {1, 2, 3};
arr+1得出的是一个新的指针,按照惯性思维,你可能会觉得它是一个左值,毕竟指针代表着内存地址(请参考指针运算)。
实际上它不是一个左值,因为地址值也只是一个数字罢了,0xff和127没有区别。
但是把这个地址值加上间接运算符*后,它的含义就变了,变成了“以int类型访问这个内存空间",这样它就变成了有空间的对象,现在它是一个左值了:*(arr+1)。
#include <stdio.h> int arr[3] = {1,2,3}; void main(void) { printf("%d ", *(arr+1));//输出2 *(arr+1) = 20; printf("%d ", arr[1]);//输出20 }
3. 再看看这个例子:
#include <stdio.h> int arr[3] = {5,9,12}; void main(void) { printf("%d ", *arr+1); }
由于运算符优先级的问题(间接运算符比算术运算符优先级高),所以这里的表达式*arr+1也只是产生一个值而已(*arr的值5+1=6)。
4.再来看一个相对更加复杂一点的例子:
#include <stdio.h> int arr[3] = {6, 7, 8}; int main(int argc, char const *argv[]) { printf("%d ", *++arr); return 0; }
根据运算符优先级的特性(请参考运算符优先级一文),表达式(*++arr)的运行顺序是先执行对arr的递增,然后再进行解参考运算;
理论上如果arr是一个指针类型的变量,那么这个表达式是没有任何问题的,arr执行的是对指针的偏移操作(参考指针运算);
但是,这里的arr只是一个指针类型的值,而不是一个变量!换而言之,它不是一个左值,而递增递减操作符要求操作数一定要是一个左值,
于是编译器会报错:
1.c:7:18: error: lvalue required as increment operand printf("%d ", *++arr); ^
倘若需要进行类似操作,你必须确保操作数是一个左值,像这样是理想的:
printf("%d ", *(arr+1));
因为表达式(arr + 1)运算只是产生了一个类型为int指针的值,并不需要给任何对象赋值,接着用*为该指针类型的值解参考;
又或者可以这样:
int* ptr = arr; printf("%d ", *++ptr);
ptr是可以运行递增操作的,因为ptr是一个对象,这个对象保存的是一个int类型指针,递增操作改变了ptr,使它编程了指向下一个元素的指针;
两者结果都是正常的输出元素7;
5.通常地,函数的返回值都不是一个左值,无论返回值是什么类型。
例如,返回值是一个指针,那么它仅仅是一个代表内存地址的数字罢了,要访问它指向的对象,必须加上间接运算符*;
又例如,返回值是一个整型,在赋值给一个空间之前,这个整型并不具备任何可操作空间,想象一下你如何运用地址操作符&拿到函数返回结果的地址值?答案是不能的;
所以函数返回的结果,都是数据,不是左值。
struct Article getArticle(int id); printf("%s ", getArticle(3).content);
以上代码函数getArticle()返回一个Article的结构(假设该结构包含成员content),所以点运算符在这里是合法的,但是getArticle()的返回结果不是一个左值,
你无法对它进行类似这样的赋值操作:
getArticle(3).content = "some text";//illegal
6.结合表达式和运算符优先级的概念,再来看看一个有趣的例子:
int main(int argc, char const *argv[]) { int x = 1; ++x++; return 0; }
这段代码会抛出操作数不是左值的错误信息:
1.c: In function ‘main’: 1.c:7:2: error: lvalue required as increment operand ++x++; ^
原因是由于运算符优先级的关系,x++比++x具有更高的优先级,所以x++先运行了。
其实无论哪个表达式先运行,它运行的结果都是产生一个值,而接下来运行的表达式将会基于这个值进行运算。
x++优先运行,它产生了一个值,这个值等于x的本身,其实关注点不在这个值是多少,而是,x++运行后,后面的表达式只是基于它运行后的值接着运算。
而这个时候它已经不是一个左值,但是前序++运算符需要一个左值作为操作数,所以它报错了。
即使把表达式改为:(++x)++,也无济于事,报错依旧,只不过这次轮到了后序++运算符报错:
1.c:7:7: error: lvalue required as increment operand (++x)++; ^
那么,
x+++x++
可不可以运行呢,答案是可以的,因为运算符优先级的原型,以上式子实际上是按照这个顺序运行的:
(x++) + (x++)
虽然这个表达式可以运行,但是是不推荐的,尽量不要在两个序列点直接改变同一个变量超1次;