引用:左值、右值与陷阱
探索左值引用和右值引用,理解 C++ 值类别,学习引用折叠规则和转发引用,并避免悬空引用的 bug。
左值引用
左值引用(T&)是一个别名 ── 已有对象的另一个名字。与指针不同,引用必须被初始化,不能为空,并且不能被重新绑定到另一个对象。
左值引用绑定到左值 ── 具有标识的表达式(你可以取其地址)。变量、数组元素和解引用的指针都是左值。非 const 左值引用(int&)不能绑定到临时对象(右值),因为修改临时对象几乎总是一个 bug。
左值引用基础
左值引用是 C++ 的主力 ── 用于按引用传递、运算符重载和避免拷贝。
#include <iostream>
#include <string>
void append_greeting(std::string& s) {
s += ", welcome!"; // modifies the caller's string
}
int main() {
int x = 10;
int& ref = x; // ref is an alias for x
ref = 20;
std::cout << x << '\n'; // 20 — x was modified through ref
// const reference can bind to a temporary (rvalue)
const std::string& greeting = std::string("Hello");
// The temporary's lifetime is extended to match greeting's scope
std::cout << greeting << '\n'; // "Hello"
// Non-const lvalue reference CANNOT bind to rvalue:
// std::string& bad = std::string("oops"); // COMPILER ERROR
std::string name = "Alice";
append_greeting(name);
std::cout << name << '\n'; // "Alice, welcome!"
}C++ 中的值类别
C++ 对表达式的值类别有精确的分类法。理解它们对于移动语义和完美转发至关重要:
- 左值(lvalue) ── 有标识,不能被隐式移动。例如:变量、*ptr、arr[i]、字符串字面量。
- 纯右值(prvalue) ── 没有标识,可以被移动。例如:42、x + y、std::string("temp")、按值返回的函数。
- 将亡值(xvalue) ── 有标识且可以被移动。例如:std::move(x)、static_cast、右值的成员。
- 泛左值(glvalue) ── 左值或将亡值(有标识)。
- 右值(rvalue) ── 纯右值或将亡值(可以被移动)。
关键洞察:一个右值引用变量本身是一个左值 ── 它有名字和地址。这是许多困惑的根源,也正是 std::forward 存在的原因。
右值引用(T&&)
右值引用绑定到右值(临时对象和 std::move 过的对象)。它们是移动语义的基础 ── 让你可以检测到对象即将被销毁并窃取其资源。
#include <iostream>
#include <string>
#include <utility> // std::move
void process(const std::string& s) {
std::cout << "lvalue: " << s << '\n';
}
void process(std::string&& s) {
std::cout << "rvalue: " << s << '\n';
// s is an lvalue inside this function (it has a name!)
// To move from s, you'd need std::move(s)
}
int main() {
std::string a = "hello";
process(a); // calls lvalue overload
process(std::string("world")); // calls rvalue overload
process(std::move(a)); // calls rvalue overload — a is now moved-from
// IMPORTANT: a is in a valid but unspecified state
// Only safe operations: assign to it, destroy it
std::cout << "a after move: '" << a << "'\n"; // likely empty
}引用折叠与转发引用
当引用在模板中组合时,引用折叠规则适用:
- T& & 折叠为 T&
- T& && 折叠为 T&
- T&& & 折叠为 T&
- T&& && 折叠为 T&&
规则很简单:如果任何一个引用是左值引用,结果就是左值引用。只有 && && 才产生 &&。
转发引用(也称为万能引用)是 T&&,其中 T 是一个被推导的模板参数。它可以同时绑定左值和右值。auto&& 也是转发引用。但 std::vector 不是 ── 因为 T 已经确定了。const T&& 也不是转发引用。
悬空引用指向已销毁的对象。与悬空指针不同,没有 nullptr 检查 ── 代码会默默地使用垃圾数据或崩溃。
常见原因:
- 返回局部变量的引用 ── 函数返回时变量被销毁。
- 在 lambda 中按引用捕获了生命周期短于 lambda 的变量。
- 对表达式内部的临时对象使用 const auto& ── 如果 get_map() 按值返回,const auto& val = get_map()["key"] 会悬空,因为临时 map 在分号处被销毁。
- 对临时对象使用基于范围的 for 循环 ── for (auto& x : get_vector()) 没问题(临时对象在循环期间存活),但 for (auto& x : get_wrapper().get_vector()) 可能悬空,如果包装器被提前销毁。
使用 const T& 延长生命周期
const 左值引用可以延长临时对象的生命周期,但仅在直接绑定时有效。这不能通过函数调用或成员访问链传播。
#include <iostream>
#include <string>
std::string make_greeting() { return "Hello, World!"; }
const std::string& identity(const std::string& s) { return s; }
int main() {
// SAFE: const ref directly bound to temporary
const std::string& good = make_greeting();
std::cout << good << '\n'; // OK — lifetime extended
// DANGEROUS: lifetime extension does NOT propagate through functions
const std::string& bad = identity(make_greeting());
// bad is DANGLING — the temporary was destroyed after identity() returned
// std::cout << bad << '\n'; // UNDEFINED BEHAVIOR
// SAFE: just use a value
std::string safe = make_greeting(); // move or copy-elision
std::cout << safe << '\n'; // always safe
}- 左值引用(
T&)是别名 ── 必须初始化,不能为空或重新绑定 const T&可以绑定到临时对象并延长其生命周期(但仅在直接绑定时)- 右值引用(
T&&)绑定到临时对象并启用移动语义 - 具名的右值引用本身是一个左值 ── 这一点至关重要
- 引用折叠:链中的任何
&都使结果为& - 转发引用(带有被推导的
T的T&&)可以同时绑定左值和右值
测验 — 检验你的知识
(15 经验值)1. 给定 `void f(std::string&& s)`,`s` 在 `f` 内部的值类别是什么?
2. 哪个引用折叠规则是正确的?
3. `const T&` 生命周期延长对临时对象何时有效?