1) 左值、右值、纯右值(prvalue)、将亡值(xvalue)
面试答案要点
-
左值(lvalue):在表达式结束后仍然存在、可取地址的对象(有名字或持久存储)。
-
右值(rvalue):不能持久存在、一般不可取地址的临时值。包含两类:
-
纯右值(prvalue) :字面量、计算结果等,代表"值";如
42,x + y,std::string("a")(C++17 起 prvalue 直接初始化目标对象)。 -
将亡值(xvalue) :即将被销毁但仍可"窃取资源"的对象表达式;典型来源
std::move(x)、返回std::string&&的函数结果等。
-
-
右值引用(T&&) 绑定右值(尤其是 xvalue),为移动语义提供基础。
演示代码
#include <iostream>
#include <string>
#include <utility>
int main() {
std::string s = "hello"; // s 是左值
auto&& a = std::string("tmp"); // std::string("tmp") 是 prvalue,直接构造成临时对象,再被 a(万能引用)绑定
auto&& b = std::move(s); // std::move(s) 是 xvalue(将亡值)
// std::cout << "s: " << s << "\n"; // 此时 s 的状态未定义,访问它是不安全的,尽管在某些实现中它可能是空的
std::cout << "a: " << a << "\n";
std::cout << "b: " << b << "\n"; // b 绑定的是 s 的将亡值(资源移动后 b 拥有)
}
关键点 :std::move(s) 不是"移动",而是把 s 标记为可被移动(转为 xvalue)。
2) T&, const T&, T&& 的绑定规则
面试答案要点(最常考表)
-
T&:只能绑定左值。 -
const T&:能绑定左值 也能绑定右值(延长右值临时对象生命周期)。 -
T&&:只能绑定右值(prvalue/xvalue)。 -
模板形参
T&&(推导场景 )是转发引用/万能引用:-
实参是左值 ⇒
T推导为T&,形参折叠成T& &&⇒ T& -
实参是右值 ⇒
T推导为T,形参为 T&&
-
演示代码
#include <iostream>
#include <type_traits>
void bind_l(int& ) { std::cout << "int&\n"; }
void bind_cl(const int& ) { std::cout << "const int&\n"; }
void bind_r(int&& ) { std::cout << "int&&\n"; }
template <typename T>
void probe(T&& x) { // 转发(万能)引用
if constexpr (std::is_lvalue_reference_v<T&&>) std::cout << "probe: lvalue\n";
else std::cout << "probe: rvalue\n";
}
int main() {
int i = 0;
bind_l(i); // OK: 左值
bind_cl(i); // OK
// bind_r(i); // ❌ 左值不能绑定到 int&&
bind_r(42); // OK: 右值
const int ci = 1;
bind_cl(2); // OK: 右值绑定 const 引用
// bind_l(2); // ❌
// bind_r(ci); // ❌ const 左值不是右值
probe(i); // lvalue
probe(3); // rvalue
}
3) std::move 与 std::forward 的区别
面试答案要点
-
std::move(x):无条件 把表达式转成T&&(xvalue)。常用于"我确定要把资源移走"。 -
std::forward<T>(x):有条件 转发:当模板实参T为左值引用时保持左值,为非引用时保持右值;用于完美转发。 -
使用场景:
-
写类的移动构造/赋值 ⇒ 用
std::move。 -
写转发包装器/工厂函数 ⇒ 用
std::forward。
-
演示代码
#include <iostream>
#include <string>
#include <utility>
void take(const std::string& s) { std::cout << "take(const&): " << s << "\n"; }
void take(std::string&& s) { std::cout << "take(&&): " << s << "\n"; }
template <typename T>
void wrapper_move(T&& x) {
// 无论传左值还是右值,都被move成右值
take(std::move(x));
}
template <typename T>
void wrapper_fwd(T&& x) {
// 保留实参值类别(左值仍左值,右值仍右值)
take(std::forward<T>(x));
}
int main() {
std::string s = "hi";
wrapper_move(s); // 强行走 && 版本
wrapper_fwd(s); // 保持左值 => 走 const&
wrapper_fwd(std::string("tmp")); // 右值 => 走 &&
}
4) 移动构造 与 拷贝构造 的优先级
面试答案要点
-
函数重载决议中:能移动就不拷贝(当参数是右值且存在可行移动构造时,会优先选择移动构造)。
-
若用户显式声明拷贝构造而未 声明移动构造,某些情况下编译器不会合成移动构造,导致右值也走拷贝。
-
为了启用移动,通常显式声明:
A(A&&) noexcept和A& operator=(A&&) noexcept。 -
对含资源成员(如
std::unique_ptr、容器)移动能显著优化性能。
演示代码
#include <iostream>
#include <vector>
struct A {
std::vector<int> data;
A() { std::cout << "A()\n"; }
A(const A&) { std::cout << "A(const A&)\n"; }
A(A&&) noexcept { std::cout << "A(A&&)\n"; } // 移动构造
A& operator=(const A&) { std::cout << "copy=\n"; return *this; }
A& operator=(A&&) noexcept { std::cout << "move=\n"; return *this; }
};
A makeA() { A a; return a; }
int main() {
A a1;
A a2 = makeA(); // C++17 多为 RVO(见下一节);若未RVO,优先调用 A(A&&)
A a3 = std::move(a1); // 明确右值 => 调用 A(A&&)
}
5) RVO(返回值优化)与移动优化
面试答案要点
-
RVO(Return Value Optimization) :编译器直接在调用方栈/存储上就地构造返回对象,省去拷贝/移动。
-
C++17 保证性 RVO :
return T{...};/return obj;(局部同名对象)在某些情形下强制省略拷贝/移动(即使定义了移动构造,也不会调用)。 -
如果不能做(NRVO 失败),才会退回到移动(若可)或拷贝。
-
实践总结 :写返回值时尽量直接构造返回对象 ,不要为了"优化"而手工
std::move(returnObj)(反而可能禁用 NRVO)。
演示代码(观察是否打印 Move/Copy)
#include <iostream>
struct Big {
Big() { std::cout << "Big()\n"; }
Big(const Big&) { std::cout << "Copy\n"; }
Big(Big&&) noexcept { std::cout << "Move\n"; }
};
Big make1() {
return Big(); // C++17: 保证性RVO,通常不打印 Copy/Move
}
Big make2() {
Big b;
return b; // C++17: 多数实现做 NRVO;若失败,则优先 Move
}
int main() {
std::cout << "make1 call:\n";
Big x = make1();
std::cout << "make2 call:\n";
Big y = make2();
}
要点 :不要写 return std::move(b);------这会把 b 变成 xvalue,禁用 NRVO,迫使调用移动构造。
小结速背(面试 30 秒版)
-
左值可取址、持久存在;右值是临时值。右值含 prvalue 与 xvalue (如
std::move)。 -
绑定规则:
T&仅左值;const T&左/右都行;T&&仅右值;模板T&&是万能引用。 -
move:无条件右值化;forward:保持实参的值类别(完美转发)。 -
右值优先走移动;未声明移动可能退化成拷贝。移动函数请标
noexcept。 -
C++17:RVO/NRVO 常直接省略拷贝/移动;不要 对返回局部用
std::move。