本章目标:彻底搞懂左值/右值、移动构造/移动赋值、 std::move、std::forward、引用折叠、完美转发。 这是"会写 C++"和"懂 C++"的分水岭,请反复读两遍。
人话定义:
int a = 42; // a 是左值;42 是右值(字面量)
a + 1; // 表达式 a+1 的结果是右值(临时值)
&a; // 合法:左值有地址
// &42; // 非法:右值没有地址
std::string s = "hi"; // s 是左值;"hi" 是右值
关键区分:
int foo() { return 1; } // foo() 是右值
int& bar() { static int x = 1; return x; } // bar() 是左值
int&& r = 42; // 右值引用:只能绑定右值
int x = 10;
// int&& r2 = x; // 错误!x 是左值
右值引用存在的意义:**能安全地"偷走"临时对象的资源**。 因为临时对象马上就要销毁,拿走它的东西不会影响任何"人"。
所以 C++11 给类型系统加了右值引用,并由此引出移动语义。
拷贝:深复制(新内存 + 复制内容),O(n),安全但慢。 移动:偷指针(交接资源),O(1),快但原对象变"空壳"。
std::string s1 = "hello";
std::string s2 = std::move(s1); // 把 s1 的资源搬给 s2
// s1 现在内容未指定(通常是空),但合法可析构
标准库大量使用移动:
std::vector<std::string> v;
v.push_back(std::string(1000, 'x')); // 临时对象 → 移动进容器
std::move(x) 做了什么?人话: 它不移动任何东西!它只是把 x 标记成"可以被移动"(转成右值), 真正的移动发生在"移动构造/移动赋值"里。 std::move 就是一个 cast:static_cast<T&&>(x)。
class Buffer {
public:
// 拷贝构造(深复制)
Buffer(const Buffer& other)
: size_(other.size_), data_(new char[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
}
// 移动构造(偷资源,O(1))
Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.data_ = nullptr; // 原对象必须置空!
other.size_ = 0;
}
// 移动赋值
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) { // 防止自赋值
delete[] data_; // 释放自己的旧资源
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
~Buffer() { delete[] data_; }
private:
char* data_ = nullptr;
std::size_t size_ = 0;
};
移动语义三条铁律:
没有 noexcept 就退回拷贝——性能白搭)
自动移动的场景(写了移动构造才有意义):
(实际上现代编译器多数直接 RVO/NRVO 省略拷贝,连移动都省了)
不自动移动的场景(坑):
想要移动:s2 = std::move(s1);
场景:写一个包装函数,把参数"原样"转发给另一个函数, 要求:传进来的如果是左值就按左值传,右值就按右值传。
void inner(std::string&) { /* 左值版本 */ }
void inner(std::string&&) { /* 右值版本 */ }
template <typename T>
void wrapper(T&& arg) { // T&& 是"转发引用"
inner(std::forward<T>(arg)); // 完美转发
}
std::string s = "x";
wrapper(s); // 转发成左值 → inner(string&)
wrapper(std::move(s)); // 转发成右值 → inner(string&&)
关键知识:T&&(模板参数推导的&&)叫"转发引用"或"万能引用", 不是普通的右值引用!它既能绑定左值也能绑定右值:
引用折叠规则(记结论就行):
T& & → T&
T& && → T&
T&& & → T&
T&& && → T&&
只要有一个 & 就折叠成 &;两个 && 才折叠成 &&。
std::forward<T>(arg) 的机制:根据 T 是引用还是值, 决定把 arg 转成左值还是右值。人话:**forward = "参数进来是 啥类别,就保持啥类别传给下一个"**。
为什么不用 std::move?move 无条件转右值—— 如果原参数是左值也被转成右值,就破坏了"原样"。
template <typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args) {
return unique_ptr<T>(new T(std::forward<Args>(args)...));
}
template <typename F, typename... Args>
auto time_it(const char* name, F&& f, Args&&... args) {
auto start = std::chrono::steady_clock::now();
auto result = std::forward<F>(f)(std::forward<Args>(args)...);
auto end = std::chrono::steady_clock::now();
std::println("{} 耗时 {} 微秒", name,
std::chrono::duration_cast<std::chrono::microseconds>
(end - start).count());
return result;
}
// 用法
std::vector<int> v(1'000'000);
std::iota(v.begin(), v.end(), 0);
auto mx = time_it("求最大值", [&] {
return *std::ranges::max_element(v);
});
forward<F>(f):函数对象本身也可能需要移动/拷贝转发。 (完整可运行版本见配套测试文件。)
std::string make() {
std::string s = "hi";
return s; // 编译器直接构造在调用方的目标位置
} // 不拷贝、不移动、零开销!
条件:返回局部变量本身(不能是条件表达式挑一个返回)。 C++17 起 RVO 是"保证"的,不再是优化可选。
"移动并不总是发生"是性能优化的重要一环—— 能 RVO 就不 move,能 move 就不 copy。
练习题(配套测试:测试_第19章_移动语义.cpp)