95、左值与右值:什么是左值?什么是右值?
说实话,左值和右值这个概念,我见过太多人栽跟头了。
刚入行那会儿,我自己也迷糊过。明明代码编译过了,跑起来却莫名其妙报错。后来一查,原来是赋值号左边放了个不能放的东西。嗯,这就是左值右值没搞明白。
一、先看个例子,感受一下
int a = 10; // 这行没问题
10 = a; // 这行编译报错
为什么第一行可以,第二行不行?
因为 a 是左值,10 是右值。赋值号左边必须放左值,右边可以放左值或右值。
核心定义:
- 左值:能取地址、有内存位置、可以放在赋值号左边的表达式
- 右值:不能取地址、没有固定内存位置、只能放在赋值号右边的表达式
二、左值到底是什么?
左值,说白了就是「有地址的东西」。
你想想看,变量 int x 在内存里占了一块空间,我能用 &x 拿到它的地址。那 x 就是左值。
int x = 5;
int *p = &x; // 合法,x是左值
x = 10; // 合法,x是左值
我在项目中遇到过一个问题:有人写了个函数返回结构体,然后直接给返回值赋值。编译过了,运行却崩溃。为什么?因为函数返回值是右值,不能放在赋值号左边。
判断左值的小技巧:
能不能对它取地址?能 → 左值。不能 → 右值。
三、右值又是什么?
右值就是「没有固定地址的临时值」。
比如 5、a + b 的结果、函数返回值。这些东西用完就扔,没有长期住的地方。
int a = 3, b = 4;
int c = a + b; // a+b的结果是右值,临时存在寄存器里
int *p = &(a+b); // 编译报错,不能对右值取地址
我曾经在调试一个嵌入式程序时,看到同事写了这样的代码:
void func(void) {
return 100;
}
int main() {
func() = 200; // 编译报错!func()是右值
return 0;
}
他一脸困惑:「函数明明返回了 int,为什么不能赋值?」
我告诉他:函数返回值是右值,没有内存地址,不能当左值用。
四、左值和右值的转换
有些情况下,左值可以变成右值。比如:
int x = 10;
int y = x; // 这里的x作为右值使用,取的是x的值
但反过来,右值不能变成左值。除非你用指针或者引用去「接住」它。
注意:
C语言里,数组名是左值,但数组名不能放在赋值号左边。因为数组名是常量指针,不能修改。
int arr[5];
arr = NULL; // 编译报错,arr是左值但不能赋值
五、常见陷阱汇总
| 表达式 | 左值/右值 | 能否赋值 |
|---|---|---|
int a |
左值 | 能 |
a + 1 |
右值 | 不能 |
++a |
左值 | 能 |
a++ |
右值 | 不能 |
func() |
右值 | 不能 |
*p |
左值 | 能 |
这里有个细节:++a 返回的是 a 本身,所以是左值。而 a++ 返回的是 a 的副本,所以是右值。我刚开始学的时候也搞混过,后来记住一句话:「前置返回自己,后置返回副本」。
六、知识体系图
七、避坑指南
我曾经在写一个通信协议栈时,遇到过一个隐蔽的bug。代码大概是这样的:
int get_status(void) {
return 0;
}
void process(void) {
int *p = &get_status(); // 编译报错
}
我当时想:「函数返回一个int,我取它的地址有什么问题?」
问题大了。函数返回值是右值,它可能存在寄存器里,也可能在栈上被立即销毁。你取它的地址,拿到的是一个已经失效的地址。这就是典型的「悬空指针」隐患。
我的建议:
写代码时养成一个习惯:看到赋值号左边的东西,先问自己一句「这东西能取地址吗?」。如果不能,就别往左边放。
还有一个常见坑:字符串字面量。
char *s = "hello";
s[0] = 'H'; // 运行时崩溃!
字符串字面量 "hello" 是右值,存在只读区。虽然 s 是左值,但 s[0] 试图修改只读内存,直接崩掉。嗯,这个坑我踩过不止一次。
八、总结
左值和右值,说白了就是「有家的」和「没家的」。
- 左值有固定地址,能取地址,能放赋值号左边
- 右值没有固定地址,不能取地址,只能放赋值号右边
- 数组名是左值但不能赋值
- 函数返回值是右值
- 前置++是左值,后置++是右值
搞懂这个,很多编译错误和运行时崩溃你一眼就能看出来。别问我怎么知道的——都是血泪换来的经验。
公众号:蓝海资料掘金营,微信deep3321