在 C++ 中,值的类别(value categories) 是语言语义的核心:它决定了表达式能否被赋值、能否绑定到引用、是否是临时对象、是否可以“被移动”。理解左值/右值及其演化(C++11 引入的 xvalue、prvalue、glvalue 等)对写出正确、高效的现代 C++ 非常重要。本文把概念、规则、常见例子、陷阱与最佳实践都讲清楚,方便你在工程中直接使用。
一、先看直观解释(直觉版)
-
左值(lvalue):表示“有地址、能被命名或能被取地址”的对象。典型是变量、解引用的指针、类成员访问等。你可以对左值取地址、可以赋值给它(
a = 5)。 -
右值(rvalue):通常是临时值、字面量或表达式的中间结果(例如
1,a + b,std::string("s"))。它们通常不能取地址(除非被绑定到引用),是短暂存在的。 -
xvalue(expiring value,过期值):既有对象身份又“即将被搬走”的值(例如
std::move(x)的结果、返回T&&的函数调用)。xvalue 能被移动(move),std::move 生成的正是 xvalue。 -
在 C++11 以后,值类别细分更细,这是为移动语义和完美转发提供语义基础。
二、正式分类(简要但精确)
C++11 及以后把表达式的类别分为以下几类(常见术语):
-
glvalue(generalized lvalue) =
lvalue或xvalue -
prvalue(pure rvalue) = 纯右值(如字面量、返回 by-value 的临时(在 C++17 前会物化为临时))
-
xvalue(expiring value) = 过期值(可视为“可移动的左值”)
日常使用中,你可以理解为:
-
lvalue—— 持久、可命名、有地址; -
prvalue—— 临时、短暂、没有可取地址的身份(直到被物化); -
xvalue—— 既有地址(是对象的一部分),又快要被“窃取”/移动。
三、为什么这些区分重要?
这些类别影响:
-
能否绑定到某种引用:
T&、const T&、T&&的绑定规则不同。 -
函数重载选择:
f(T&)与f(T&&)在重载解析时会根据表达式类别分派不同版本。 -
移动语义(move constructor / move assignment)什么时候被调用。
-
完美转发:模板
T&&参数需要判别实参的值类别以正确转发(std::forward)。 -
临时对象的生存期、拷贝消除(copy elision / NRVO / guaranteed elision in C++17)等。
四、绑定规则(常用的几条)
-
非 const 左值引用 (
T&) 只能绑定到左值(不能绑定到临时)。
int x = 1;
int& r = x; // OK
int& r2 = 1; // 错误:不能把临时绑定到 T&
2.const 左值引用 (const T&) 可以绑定到左值也可以绑定到临时(并会延长临时的寿命到引用的作用域)。
const int& r = 1; // OK,临时的生命周期延长到 r 的作用域结束
3.右值引用 (T&&) 可以绑定到右值(包括 xvalue、prvalue)。
int&& rr = 1; // OK
int a = 1;
int&& rr2 = std::move(a); // OK,std::move(a) 是 xvalue
4.绑定到 reference 的声明(在声明处直接初始化引用)会延长临时对象的生命周期(适用于 const T& 和绑定到右值引用 T&& 的情形),但把临时传给函数参数并不会延长到函数外。
五、典型例子(演示行为)
#include <iostream>
#include <utility>
#include <string>
void f_lref(std::string& s) { std::cout << "lref\n"; }
void f_clref(const std::string& s) { std::cout << "const lref\n"; }
void f_rref(std::string&& s) { std::cout << "rref\n"; }
int main() {
std::string a = "hello";
f_lref(a); // 匹配 lvalue 版本
// f_lref("tmp"); // 错误:不能把临时传给 T&
f_clref("tmp"); // const lvalue ref 可以绑定临时
f_rref(std::move(a)); // std::move(a) 是 xvalue,调用 rvalue 版本
// f_rref(a); // 错误:a 是 lvalue
// 绑定延长
const std::string& r = std::string("world"); // 临时延长到 r 的作用域
std::cout << r << "\n";
}
输出(示意):
lref
const lref
rref
world
六、std::move 与 std::forward 的含义
-
std::move(x):把x转换为对应的右值引用类型(T&&),从而允许移动语义。但std::move不做实际移动,它只是一个类型转换(cast)。实际是否移动取决于后续调用的函数(例如是否调用移动构造函数)。 -
std::forward<T>(x):在模板中用于完美转发——当你写template<typename T> void g(T&& t),std::forward<T>(t)会在T是U&时作为左值转发,在T是U时作为右值转发。只有在转发引用(forwarding/universal reference)情形下使用std::forward。
注意:错误地频繁对对象 std::move 会导致“move 后对象处于未指定但有效状态”,多数情况下后续不可再当作原先的值使用。
七、移动语义(move constructor / move assignment)示例
#include <iostream>
#include <vector>
#include <utility>
#include <string>
struct S {
std::string data;
S(const char* s) : data(s) { std::cout << "ctor\n"; }
S(const S& other) : data(other.data) { std::cout << "copy ctor\n"; }
S(S&& other) noexcept : data(std::move(other.data)) { std::cout << "move ctor\n"; }
};
int main() {
S s1("hello");
S s2 = s1; // copy ctor
S s3 = std::move(s1); // move ctor
}
输出可能是:
ctor
copy ctor
move ctor
提示:给类型实现 noexcept 的移动构造会让标准容器(如 std::vector)在重新分配时优先使用移动而不是拷贝(有利于性能)。
八、重载分派(lvalue vs rvalue 函数重载)
你可以重载两个版本,一个接受 T&、一个接受 T&&,以对 lvalue 和 rvalue 做不同处理:
#include <iostream>
void g(int& x) { std::cout << "lvalue overload\n"; }
void g(int&& x) { std::cout << "rvalue overload\n"; }
int main() {
int a = 0;
g(a); // lvalue overload
g(0); // rvalue overload
g(std::move(a)); // rvalue overload
}
这就是实现“按值类别选择不同操作”的基础(例如移动而不是拷贝)。
九、完美转发示例(模板里保留传入值类别)
#include <utility>
#include <iostream>
void do_work(std::string& s) { std::cout << "do_work lvalue\n"; }
void do_work(std::string&& s) { std::cout << "do_work rvalue\n"; }
template <typename F, typename T>
void wrapper(F&& f, T&& param) {
// 若希望把传入 param 的值类别传给 f,使用 std::forward<T>(param)
std::forward<F>(f)(std::forward<T>(param));
}
int main() {
std::string s = "hi";
wrapper(do_work, s); // do_work lvalue
wrapper(do_work, std::string("x"));// do_work rvalue
}
std::forward 的作用是在模板中把值类别“透传”给被调用函数。
十、临时对象与生命周期延长
-
将临时绑定到
const T&(或在声明处直接绑定到T&&)可以延长临时对象的生命周期到引用的作用域结束:
const std::string& r = std::string("temp"); // 临时直到 r 退出作用域才销毁
std::string&& rr = std::string("temp2"); // 同样延长到 rr 的作用域
但把临时作为函数参数传递(不是在声明处直接绑定到引用)通常不会延长到函数外;不要以为函数内部的引用会让临时在外部继续存在。
十一、C++17 中关于 prvalue 的语义变化(简述)
C++17 对 prvalue(纯右值)的求值模型做了调整:
-
保证了在某些情境(如
return T{...};)下的拷贝/移动消除(guaranteed copy elision):编译器不再被允许产生额外的拷贝构造或移动构造——对象直接在调用者的空间构造。 -
prvalue 不再会立即产生一个可寻址的临时对象,而是在需要时“物化(materialize)”对象。这影响了取地址、绑定等边界情形,但在大多数普通代码中你只要知道 C++17 后返回 by-value 更高效即可。
简言之:C++17 让返回临时对象更高效、语义更直观。
十二、常见错误与陷阱(务必记住)
把局部对象的引用返回(悬空引用):
const std::string& bad() {
std::string s = "oops";
return s; // 悬空引用,s 已被销毁
}
-
误用
std::move:std::move只是把对象标记为“可移动”,不要对之后还打算使用的对象调用std::move(它会把其状态置为“可但未指定”)。 -
对
const对象std::move没用:std::move(const_obj)生成const T&&,而移动构造通常需要T&&(非 const),因此会退化为拷贝。 -
在模板中错误使用
std::forward:std::forward只能在完美转发模板中按T模板参数使用,误用会导致未定义行为或值类别丢失。 -
以为移动后对象为空白可任意使用:移动后对象保证是有效但未指定状态(只要满足类型的不变性约束),但不是可以忽略的。
-
过度使用右值引用作为函数参数:
T&&在非模板上下文表示“只接受右值”,这会限制函数灵活性。通常为方针函数使用const T&,为需窃取资源使用T&&(或提供两种重载版本)。
十三、最佳实践(总结与建议)
-
输入参数:使用
const T&(或对小型标量直接用T)。 -
需要接收任意左/右值并转发:在模板中使用
T&&(转发引用)并用std::forward<T>转发。 -
实现移动语义:为资源类型实现移动构造函数和移动赋值,标注
noexcept(以便容器优先移动)。 -
避免返回局部引用;对临时要么返回值(C++17 已高效),要么返回智能指针。
-
使用
std::move明确意图,但只在你确定不再需要源对象时使用。 -
Prefer
std::forwardonly in forwarding contexts(即模板T&&参数处)。
十四、速查表

结束语
左值与右值(以及 xvalue/prvalue/glvalue)是理解 C++ 现代特性的钥匙:移动语义、完美转发、零拷贝返回等都基于这些概念。掌握这些可以帮你写出既安全又高效的代码,并能避免常见的悬空引用、误用 std::move、错误的模板转发等陷阱。
1291



被折叠的 条评论
为什么被折叠?



