左值右值与移动语义
面试题 1:左值和右值的本质区别是什么?
#include <cstring>
int a = 42; // a 是左值,42 是右值
int* p = &a; // OK:可以取 a 的地址
// int* q = &42; // 编译错误:不能取右值的地址
能取地址的是左值(lvalue),不能取地址的是右值(rvalue)。 右值是即将销毁的临时对象,可以安全地"偷"走它的资源。
int x = 10; // x 是左值
int y = x + 5; // (x + 5) 是右值,运算结果是一个临时量
int&& rref = 42; // 右值引用可以绑定右值
// int& ref = 42; // 编译错误:非常量左值引用不能绑右值
const int& cref = 42; // OK:const 左值引用可以绑右值(延长生命周期)
面试追问:具名右值引用本身是左值还是右值?
void foo(int&& arg) { // arg 是 int&& 类型
// 但 arg 本身是一个具名变量,是左值
// int&& another = arg; // 编译错误:arg 是左值,不能绑到右值引用
}
面试题 2:std::move 到底做了什么?
template<typename T>
constexpr std::remove_reference_t<T>&& move(T&& t) noexcept {
return static_cast<std::remove_reference_t<T>&&>(t);
}
std::move 本质就是一个 static_cast<T&&>,不产生任何运行时代码。 它只是告诉编译器"这个对象可以移动",让重载决议选择移动版本的函数。
#include <iostream>
#include <string>
#include <vector>
struct Buffer {
char* data;
size_t size;
Buffer(size_t n) : size(n), data(new char[n]) {
std::cout << "Constructor\n";
}
~Buffer() { delete[] data; }
Buffer(const Buffer& other) : size(other.size), data(new char[other.size]) {
std::memcpy(data, other.data, size);
std::cout << "Copy constructor\n";
}
Buffer(Buffer&& other) noexcept : size(other.size), data(other.data) {
other.data = nullptr;
other.size = 0;
std::cout << "Move constructor\n";
}
};
int main() {
Buffer a(1024);
Buffer b = std::move(a); // 调用移动构造
// 此时 a.data == nullptr, a 处于"空"状态
}
移动构造/赋值必须标记 noexcept,否则 std::vector 扩容时会退化回拷贝:
#include <iostream>
#include <vector>
struct MayThrow {
MayThrow() = default;
MayThrow(MayThrow&&) { /* 没写 noexcept */ }
};
struct NoThrow {
NoThrow() = default;
NoThrow(NoThrow&&) noexcept = default;
};
int main() {
std::vector<MayThrow> v1;
v1.reserve(1);
v1.emplace_back();
// 再次插入触发扩容:因为移动不保证 noexcept,vector 用拷贝
v1.emplace_back(); // 调用拷贝而非移动
std::vector<NoThrow> v2;
v2.reserve(1);
v2.emplace_back();
v2.emplace_back(); // 调用移动,O(1) 指针交换
}
std::vector::reserve 之后扩容时,如果类型提供了 noexcept 移动构造,就用移动(常数时间);否则用拷贝(线性时间)。这是 C++ 标准库的强异常安全保证。
面试题 3:引用折叠四条规则
当模板参数 T 推导为引用类型时,T&& 的行为由引用折叠(reference collapsing)确定:
| T 被推导为 | T&& 实际类型 | 规则 |
|---|---|---|
T& | T& | T& + && → T& |
T& & | T& | T& + & → T& |
T&& & | T& | T&& + & → T& |
T&& && | T&& | T&& + && → T&& |
核心结论:只要出现左值引用(&),结果就是左值引用;只有全是右值引用(&& &&)才得到右值引用。
template<typename T>
void foo(T&& t) { // 万能引用(forwarding reference)
// T 的推导结果决定 t 的类别
}
int main() {
int x = 0;
foo(x); // T = int& ⇒ T&& = int& ⇒ 左值引用
foo(0); // T = int ⇒ T&& = int&& ⇒ 右值引用
}
面试追问:为什么模板中的 T&& 叫万能引用,而写死的 int&& 不是?
答:只有发生在模板参数推导或 auto&& 场景中的 T&& 才是万能引用(前向引用),会根据实参自动折叠。显式写 int&& 永远只是一个右值引用,不能绑定左值。
面试题 4:std::forward 与完美转发原理
完美转发(perfect forwarding)的目标:将参数原样(包括值类别)传递给下一个函数。
#include <iostream>
#include <string>
#include <utility>
void process(int& x) { std::cout << "Lvalue\n"; }
void process(int&& x) { std::cout << "Rvalue\n"; }
template<typename T>
void relay(T&& t) {
process(std::forward<T>(t));
}
int main() {
int a = 0;
relay(a); // 输出 Lvalue
relay(42); // 输出 Rvalue
}
std::forward 的实现原理:
template<typename T>
constexpr T&& forward(std::remove_reference_t<T>& t) noexcept {
return static_cast<T&&>(t);
}
template<typename T>
constexpr T&& forward(std::remove_reference_t<T>&& t) noexcept {
static_assert(!std::is_lvalue_reference_v<T>);
return static_cast<T&&>(t);
}
当 T = int& 时:forward<int&> 返回 int& && → 折叠为 int&,左值转发。
当 T = int 时:forward<int> 返回 int&&,右值转发。
对比 std::move 和 std::forward:
| 函数 | 行为 | 典型场景 |
|---|---|---|
move(t) | 无条件转右值引用 | 明确要转移资源 |
forward<T>(t) | 保真传递值类别 | 模板中转发参数给下一层 |
面试题 5:push_back 与 emplace_back 的区别
#include <iostream>
#include <string>
#include <vector>
struct Expensive {
std::string name;
explicit Expensive(std::string s) : name(std::move(s)) {
std::cout << "构造\n";
}
Expensive(const Expensive&) { std::cout << "拷贝\n"; }
Expensive(Expensive&&) noexcept { std::cout << "移动\n"; }
};
int main() {
std::vector<Expensive> v;
v.reserve(10);
std::cout << "--- push_back 传入临时对象 ---\n";
v.push_back(Expensive("a"));
// 构造 + 移动
std::cout << "--- emplace_back 直接构造 ---\n";
v.emplace_back("b");
// 只构造,无移动/拷贝
}
emplace_back 直接在 vector 分配的内存上构造对象,传的是构造参数;push_back 传的是已构造好的对象(需要构造一次 + 移动/拷贝一次)。
C++17 起 push_back 也有右值重载,但 emplace_back 在传入多个参数或不想写临时对象时更优。
面试题 6:RVO / NRVO / Guaranteed Copy Elision
RVO(Return Value Optimization)
编译器在调用方栈帧上直接构造返回值,省略拷贝/移动:
#include <iostream>
struct Big {
Big() { std::cout << "构造\n"; }
Big(const Big&) { std::cout << "拷贝\n"; }
Big(Big&&) noexcept { std::cout << "移动\n"; }
};
Big makeRVO() {
return Big{}; // 临时对象 —— RVO 消除拷贝
}
int main() {
std::cout << "--- RVO ---\n";
Big b = makeRVO(); // 只输出一次"构造"
}
NRVO(Named RVO)
对具名变量做返回值优化,但编译器不强制:
Big makeNRVO() {
Big local; // 具名对象
// 一些操作...
return local; // NRVO:编译器尝试省略拷贝/移动
}
C++17 Guaranteed Copy Elision
C++17 起,prvalue(纯右值)不再触发拷贝/移动,语言层面保证省略:
#include <iostream>
struct Guaranteed {
Guaranteed() { std::cout << "构造\n"; }
Guaranteed(const Guaranteed&) = delete; // 拷贝删除
Guaranteed(Guaranteed&&) = delete; // 移动也删除
};
int main() {
Guaranteed g = Guaranteed{}; // C++17 OK!prvalue 直接构造
// C++14 中不行(需要可拷贝/移动)
}
C++17 guaranteed copy elision 的前提是从 prvalue 初始化。 而 NRVO 不是强制性的,所以即使 C++17 也不能保证 NRVO。
完整速查表
| 概念 | 一句话总结 |
|---|---|
| 左值 | 有地址、有名字,取址 & 合法 |
| 右值 | 临时对象、字面量,取址不合法 |
std::move | static_cast<T&&>,只标记、不移动 |
std::forward | 根据模板参数保真传递值类别 |
T&&(模板中) | 万能引用,自动折叠 |
| 引用折叠 | 有 & 就 &,只有 && && 才是 && |
move noexcept | 必须写,否则 vector 扩容退化为拷贝 |
emplace_back | 原地构造,零额外移动/拷贝 |
| RVO | 编译器跳过拷贝,直接构造到返回位置 |
| C++17 保证省略 | prvalue 初始化零拷贝,语言级承诺 |
5658



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



