C++ 值类别 (value categories)概述
在 C++中值类别并不是语言特性, 他们更多的是表达式的语法属性. 理解它们有助于理解:
- 内置类型和用户定义的操作符
- 引用类型
- 移动语义
值类别也是不断演进的:
- 在早期的 C 语言中, 只有两种值类别: 左值(lvalue)和右值(rvalue). 与之相关的概念也很简单.
- 早期的 C++扩增了类(class),
const
修饰符, 和引用类型. 这使得值类别更加丰富. - 现代 C++ 引入了右值引用(rvalue references). 为此不得不引入更多的值类别来描述相关的行为.
左值和右值的定义
在 C Programming Language 这本书中, Kernighan 和 Ritchie 给出的定义是: 出现在赋值表达式=
号左边的操作数(operand)是左值.
E1 = E2;
-
一个左值(
lvalue
)是指向一个对象(Object)的表达式. 一个对象则指一块存储区域.int n; // 定义了一个名为`n`的int类型对象 n = 1; // 一个赋值表达式
上面代码中:
n
表示一个int
对象, 它是左值.1
表示一个整型常量, 它是右值.
-
右值(rvalue)被定义为: 不是左值的表达式.
下面的语句尝试修改一个整型常量
1
. 当然, C++会拒绝它.1 = n; // 明显不合法
因为一个赋值语句会给一个对象赋值, 赋值语句的左边操作数必须是左值, 但是
1
不是左值, 它是右值.
为什么要区分左值和右值
- 这样的话编译器可以假设右值(rvalue)不会占用存储空间.
- 这给编译器机器码生成提供了更多的自由.
右值的存储
我们再次看这个例子:
int n;
n = 1;
-
一个编译器或许会把字面量
1
存放在数据区, 把1
看做是一个左值. 这样的情况下它会生成下面的汇编代码:one: ; 一个标签, 指代下面对象的地址 .word 1 ; 为整数1分配一个存储位置
那么编译器将会从
one
复制到n
move n, one ; 复制`one`地址的值到`n`变量地址
-
实际上, 现代 CPU 上, 有源操作数是立即数(immediate operand)的指令. 源操作数是立即数意味着数值是指令的一部分.
在汇编语言中, 会这样写:
mov n, #1 ; 将数值 #1 拷贝到地址`n`
这种情况下: 右值
1
将会作为指令的一部分在代码区.以 x86-64 为例, 我们假定
n
已经被加载到寄存器RAX
中, 那么n=1
就可以被写为:MOV RAX, 1
, 翻译为机器码就是:B8 01 00 00 00
其中:
B8
是操作码, 表示向RAX
寄存器赋值一个 32 位的立即数.01 00 00 00
是立即数1
的小端序表示(最低有效字节位于最低地址).
字面量
很多的字面量是右值, 他们不一定占用数据存储. 包括:
- 数字字面量, 如 3, 3.14
- 字符字面量, 如 ‘a’, ‘b’
然而, 字符串字面量是左值. 它们占用存储空间. 比如"Hello"
这种字符串字面量.
左值和右值的区别
-
一个左值可以出现在赋值表达式的任意一边, 比如:
int m, n; m = n; // OK, m和n均是左值
这个表达式将左值(
n
)当做右值来使用. 从 C++术语讲, 这进行了一次左值到右值的转换. -
表达式的操作数可以是左值或右值.
比如, '+'操作符必须是两个表达式.
int x; x + 2; // OK, lvalue + rvalue 2 + x; // OK, rvalue + lvalue
-
运算结果是右值: 对于内置的二元操作符(不包含赋值
=
操作符), 比如’+', 它的操作数可能是左值或右值, 它的结果是一个右值.- 表达式
m+n
的结果将放在一个编译器生成的临时变量中, 通常是一个 CPU 寄存器. 这样的临时变量通常被称为右值(rvalue
). - 表达式
m + 1 = n
是错误的, 因为m+1
的结果是一个右值, 不能出现在赋值表达式的左边.
- 表达式
-
解引用操作符
*
是左值: 解引用操作符*
的结果是一个左值. 一个指针p
可以指向一个对象, 所以*p
是一个左值.int arr