在介绍首个复合类型(左值引用)之前,我们先绕一个小弯,讨论什么是“左值”。
回顾
《表达式简介》中,我们把表达式定义为“由字面值、变量、运算符和函数调用组合而成,并可求出单一值的式子”。例如:
#include <iostream>
int main()
{
std::cout << 2 + 3 << '\n'; // 表达式 2 + 3 产生值 5
return 0;
}
2 + 3 被求值为 5,随后输出到控制台。
6.4 课又指出,表达式还可能产生“副作用”,其影响在表达式结束后依然存在:
int main()
{
int x{ 5 };
++x; // 副作用:x 递增
std::cout << x; // 输出 6
}
此外,表达式还可以求值为“对象”或“函数”,下文将进一步说明。
表达式的两大属性
为决定表达式如何求值及其可用位置,C++ 给每个表达式赋予两个属性:
- 类型(type)
- 值类别(value category)
表达式的类型
表达式的类型等同于它求值后所得值、对象或函数的类型,例如:
auto v1{ 12 / 4 }; // int / int ⇒ int
auto v2{ 12.0 / 4 }; // double / int ⇒ double(整型提升后做浮点除)
编译器在编译期即可确定类型,并据此做类型检查;而表达式的值可在编译期(constexpr)或运行期决定。
表达式的值类别
观察:
int main()
{
int x{};
x = 5; // 合法
5 = x; // 非法
}
为何 x = 5
合法,而 5 = x
不合法?答案在于值类别。
(注:本节沿用 C++11 之前的二分法:lvalue / rvalue;C++11 新增 glvalue、prvalue、xvalue,将于后续章节讲解。)
lvalue(左值)
- 发音:/ˈɛl væl juː/
- 定义:求值结果为可标识的对象或函数的表达式。
‑ 该实体可被标识符、引用或指针访问,生命周期通常跨越多个表达式。
示例:
int x{ 5 };
int y{ x }; // x 是 lvalue
- 可修改 lvalue:值可改(非 const)。
- 不可修改 lvalue:值不可改(const / constexpr)。
int a{};
const double b{};
int c{ a }; // a 为可修改 lvalue
const double d{ b }; // b 为不可修改 lvalue
rvalue(右值)
- 发音:/ˈɑːr væl juː/
- 定义:不是 lvalue 的所有表达式,通常求值为临时值。
‑ 包括字面值(除 C 风格字符串字面值)、返回临时对象的函数/运算符结果等。
‑ 无持久身份,用完即销毁。
int return5() { return 5; }
int main()
{
int x{ 5 }; // 5 是 rvalue
const double d{ 1.2 }; // 1.2 是 rvalue
int y{ x }; // x 是可修改 lvalue
int z{ return5() }; // return5() 是 rvalue(返回值)
int w{ x + 1 }; // x + 1 是 rvalue
}
运算符与值类别
二元 +
等运算符期望操作数为 rvalue;若提供 lvalue,则发生左值到右值转换(lvalue-to-rvalue conversion),即取其值。
赋值运算符要求左操作数为可修改 lvalue,右操作数为 rvalue,因此:
x = 5; // OK:x 可修改 lvalue,5 是 rvalue
5 = x; // 错误:5 是 rvalue
如何区分 lvalue / rvalue?
口诀:
- lvalue 求值结果为有持久身份的对象或函数;
- rvalue 求值结果为临时值。
编译器验证法(C++11 起利用引用重载):
template <typename T> constexpr bool is_lvalue(T&) { return true; }
template <typename T> constexpr bool is_lvalue(T&&) { return false; }
#define PRINTVCAT(expr) \
std::cout << #expr << " is an " << (is_lvalue(expr) ? "lvalue\n" : "rvalue\n");
int getint() { return 5; }
int main()
{
PRINTVCAT(5); // rvalue
PRINTVCAT(getint()); // rvalue
int x{ 5 };
PRINTVCAT(x); // lvalue
PRINTVCAT(std::string{"Hello"}); // rvalue
PRINTVCAT("Hello"); // lvalue(C 风格字符串字面值特殊)
PRINTVCAT(++x); // lvalue(前置 ++)
PRINTVCAT(x++); // rvalue(后置 ++)
}
输出:
5 is an rvalue
getint() is an rvalue
x is an lvalue
std::string {"Hello"} is an rvalue
"Hello" is an lvalue
++x is an lvalue
x++ is an rvalue
(高级提示)
C 风格字符串字面值是 lvalue,因其数组衰变为指针需取地址。
小结
- lvalue:可标识对象,可出现在赋值左侧(需可修改)。
- rvalue:临时值,只能出现在赋值右侧。
- lvalue 可隐式转为 rvalue,反之不可。
掌握 lvalue 概念后,我们便可引入首个复合类型:左值引用。