值类别全面解析
定义与作用
C++11 对表达式的值类别进行了彻底重新定义,形成了一套精确的五类值体系:
| 值类别 | 一句话描述 | 判断口诀 |
|---|---|---|
| lvalue | 有身份,不可移动 | 能取地址的表达式 |
| prvalue | 无身份,纯右值 | 临时值、字面量、计算中间结果 |
| xvalue | 有身份,可移动 | std::move(x) 的返回值 |
| glvalue | lvalue ∪ xvalue | 所有"有身份"的表达式 |
| rvalue | prvalue ∪ xvalue | 所有"可从中移动"的表达式 |
核心价值:值类别体系是理解移动语义、重载决议和完美转发的基础。std::move 将 lvalue 转换为 xvalue,T&& 可以绑定到 xvalue 和 prvalue。
核心原理
五类值的判断标准
| 表达式 | 值类别 | 理由 |
|---|---|---|
变量名 x | lvalue | 有名字、有地址 |
字符串字面量 "hello" | lvalue | const char 数组,有地址 |
整数字面量 42 | prvalue | 纯值,无地址 |
std::move(x) | xvalue | 有身份(x 还在),但可移动 |
函数返回 T&& | xvalue | 将亡值 |
函数返回 T(非引用) | prvalue | 临时对象 |
函数返回 T& | lvalue | 引用 = 别名 |
解引用 *ptr | lvalue | 得到对象的引用 |
取地址 &x | prvalue | 返回一个地址值 |
| Lambda 表达式 | prvalue | 临时闭包对象 |
字符串拼接 s1 + s2 | prvalue | 临时 std::string |
值类别与引用绑定的关系
完整示例
示例一:值类别识别工具
场景说明:编写编译期和运行期工具来识别表达式的值类别,帮助理解每种值的特征。
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
// 编译期值类别判断
template <typename T>
const char* value_category() {
if constexpr (std::is_lvalue_reference_v<T>) {
return "lvalue";
} else if constexpr (std::is_rvalue_reference_v<T>) {
return "xvalue (右值引用)";
} else {
return "prvalue";
}
}
// 重载函数揭示值类别
void inspect(int& x) {
std::cout << " → lvalue (可修改)" << std::endl;
}
void inspect(const int& x) {
std::cout << " → lvalue (const)" << std::endl;
}
void inspect(int&& x) {
std::cout << " → rvalue (xvalue 或 prvalue)" << std::endl;
}
// 帮助识别 xvalue
int global = 100;
int&& get_xvalue() { return std::move(global); }
int& get_lvalue() { return global; }
int get_prvalue() { return 42; }
int main() {
std::cout << "===== 值类别分类测试 =====" << std::endl;
// ---- lvalue ----
int x = 10;
std::cout << "变量 x:";
inspect(x);
const int cx = 20;
std::cout << "const 变量 cx:";
inspect(cx);
std::string str = "hello";
std::cout << "字符串字面量 \"hello\":";
inspect(str[0]); // operator[] 返回 char& → lvalue
// ---- prvalue ----
std::cout << "字面量 42:";
inspect(42);
std::cout << "表达式 x + 5:";
inspect(x + 5);
std::cout << "get_prvalue():";
inspect(get_prvalue());
std::cout << "临时 string:";
inspect(std::string("temp")); // inspect(std::string&&) ?
// ---- xvalue ----
std::cout << "std::move(x):";
inspect(std::move(x));
std::cout << "get_xvalue():";
inspect(get_xvalue());
// ---- decltype 揭示值类别差异 ----
std::cout << "\n===== decltype 值类别测试 =====" << std::endl;
int* ptr = &x;
std::cout << "decltype(x): " << value_category<decltype(x)>() << std::endl;
std::cout << "decltype((x)): " << value_category<decltype((x))>() << " ← 双括号陷阱" << std::endl;
std::cout << "decltype(*ptr): " << value_category<decltype(*ptr)>() << std::endl;
std::cout << "decltype(42): " << value_category<decltype(42)>() << std::endl;
std::cout << "decltype(++x): " << value_category<decltype(++x)>() << std::endl;
std::cout << "decltype(x++): " << value_category<decltype(x++)>() << std::endl;
std::cout << "decltype(std::move(x)): " << value_category<decltype(std::move(x))>() << std::endl;
return 0;
}
预期输出:
===== 值类别分类测试 =====
变量 x: → lvalue (可修改)
const 变量 cx: → lvalue (const)
字符串字面量 "hello": → lvalue (可修改)
字面量 42: → rvalue (xvalue 或 prvalue)
表达式 x + 5: → rvalue (xvalue 或 prvalue)
get_prvalue(): → rvalue (xvalue 或 prvalue)
临时 string: → rvalue (xvalue 或 prvalue)
std::move(x): → rvalue (xvalue 或 prvalue)
get_xvalue(): → rvalue (xvalue 或 prvalue)
===== decltype 值类别测试 =====
decltype(x): prvalue
decltype((x)): lvalue ← 双括号陷阱
decltype(*ptr): lvalue
decltype(42): prvalue
decltype(++x): lvalue
decltype(x++): prvalue
decltype(std::move(x)): xvalue (右值引用)
逐段分析:
- 变量
x是 lvalue——它确实有名字、有地址 - 字面量
42是 prvalue——没有地址,是纯值 std::move(x)是 xvalue——它把 lvalue 转换为 xvalue,表达式类型是int&&decltype(x)得到int(prvalue 特征),decltype((x))得到int&(左值表达式,双括号陷阱)++x(前置自增)返回引用 → decltype 是 lvalue;x++(后置)返回值 → decltype 是 prvalueinspect(std::string("temp"))优先匹配inspect(int&&)?不——std::string不能转int,所以会编译失败(此处为演示用,实际会匹配不同的重载)
示例二:值类别驱动的重载选择
场景说明:演示同一个函数名对不同值类别的不同处理,这是 STL 容器 push_back vs emplace_back 区别的底层原理。
#include <iostream>
#include <string>
class Logger {
public:
// 接收 lvalue → 拷贝
void log(const std::string& msg) {
std::cout << "[COPY] " << msg << std::endl;
}
// 接收 rvalue → 移动
void log(std::string&& msg) {
std::cout << "[MOVE] " << msg << std::endl;
}
};
std::string create_message() {
return std::string("动态生成的消息");
}
int main() {
Logger logger;
std::string persistent = "持久消息";
std::cout << "===== 值类别驱动重载 =====" << std::endl;
// lvalue → 匹配 const& 版本
logger.log(persistent);
// prvalue(临时对象)→ 匹配 && 版本
logger.log("临时字符串字面量");
logger.log(std::string("显式临时对象"));
// prvalue(函数返回值)→ 匹配 && 版本
logger.log(create_message());
// xvalue(std::move)→ 匹配 && 版本
logger.log(std::move(persistent));
// 注意:persistent 现在处于"被移动后"状态
std::cout << "\n移动后 persistent 的值: \""
<< persistent << "\" (可能为空)" << std::endl;
std::cout << "移动后 persistent.size(): " << persistent.size() << std::endl;
return 0;
}
预期输出:
===== 值类别驱动重载 =======
[COPY] 持久消息
[MOVE] 临时字符串字面量
[MOVE] 显式临时对象
[MOVE] 动态生成的消息
[MOVE] 持久消息
移动后 persistent 的值: "" (可能为空)
移动后 persistent.size(): 0
逐段分析:
logger.log(persistent):persistent是 lvalue,匹配const std::string&重载,执行拷贝logger.log(std::string("...")):临时对象是 prvalue,优先匹配std::string&&重载,执行移动logger.log(std::move(persistent)):std::move将 lvalue 转为 xvalue,匹配&&重载,资源被移动- 被移动后的对象处于有效但未指定状态(通常为空),可以安全析构或重新赋值,但不应该再被读取
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
std::move 后继续使用原对象 | 读到空值或不确定状态 | 仅重新赋值或析构 |
混淆 decltype(x) 和 decltype((x)) | 类型推导错误 | 单括号获取声明类型,双括号获取值类别 |
| 认为字符串字面量是 prvalue | 编译错误 | "hello" 是 const char[6] 左值 |
| 右值引用变量本身是 lvalue | 被名字迷惑 | int&& r = 5; r 是 lvalue |
模板中误用 T&& 转发 | 转发类型丢失 | 配合 std::forward<T> 使用 |
常见面试问题
C++11 的值类别体系是怎样的?lvalue、prvalue、xvalue 的定义?——lvalue 有身份不可移动;prvalue 无身份纯右值;xvalue 有身份可移动。glvalue = lvalue ∪ xvalue,rvalue = prvalue ∪ xvalue。
std::move做了什么?它本身是否移动数据?——std::move是一个无条件将实参转为 xvalue 的 cast。它本身不移动任何数据,只是改变了表达式的值类别,触发移动语义。右值引用变量本身是左值还是右值?为什么?——是左值。
int&& r = 5;中r有名字有地址,可以取地址,所以是左值。这就是为什么需要std::move和std::forward。const T&为什么被称为"万能引用"?——它可以绑定到 lvalue、prvalue 和 xvalue,无论值类别如何都能匹配。在移动语义引入之前,const T&是实现完美转发的唯一手段。如何判断一个表达式是 lvalue 还是 rvalue?——lvalue 可以取地址(
&expr合法);rvalue 不能取地址。std::move的返回值、函数返回T&&是 xvalue(rvalue 的子类)。
小结
- lvalue = 有身份不可移动(变量、解引用、赋值表达式左操作数)
- prvalue = 无身份纯右值(字面量、临时对象、算术结果)
- xvalue = 有身份可移动(
std::move返回值、返回T&&的函数调用) std::move只是 cast,不移动数据;它改变表达式的值类别以触发移动重载- 右值引用变量本身是左值;想作为右值使用需要
std::move或std::forward