目录
[二、左值 vs 右值:核心概念](#二、左值 vs 右值:核心概念)
[三、右值引用 T&&](#三、右值引用 T&&)
[返回值优化(RVO)vs 移动](#返回值优化(RVO)vs 移动)
[九、std::move 与 std::forward 的区别](#九、std::move 与 std::forward 的区别)
[1. move 后继续使用对象](#1. move 后继续使用对象)
[2. 不必要的 move(阻止 RVO)](#2. 不必要的 move(阻止 RVO))
[3. const 对象不能移动](#3. const 对象不能移动)
[4. 忘记标记 noexcept](#4. 忘记标记 noexcept)
一、一个昂贵的拷贝
cpp
vector<string> createBigVector() {
vector<string> v(1000000, "hello");
return v; // 返回时发生了什么?
}
vector<string> v = createBigVector();
C++98/03 时代,这个返回会触发一次或多次拷贝:构造临时对象、拷贝给接收变量,100 万个字符串被逐个复制,性能极差。
现代 C++ 的做法:移动,而不是拷贝。将内部指针直接"偷"过来,原对象置空------O(1) 操作,而不是 O(n)。
二、左值 vs 右值:核心概念
| 概念 | 特征 | 例子 |
|---|---|---|
| 左值 | 有名字、可取地址、持久存在 | 变量名 a、std::cout、*ptr |
| 右值 | 临时值、无名、即将销毁 | 字面量 42、表达式 a+b、函数返回值 |
cpp
int a = 42; // a 是左值,42 是右值
int b = a; // b 是左值,a 是左值(但可以读取)
int c = a + b; // a+b 是右值(临时结果)
简单判断 :能对表达式用 & 取地址的是左值,不能的是右值。
cpp
int x = 5;
&x; // ✅ 合法,x 是左值
&5; // ❌ 非法,5 是右值
三、右值引用 T&&
右值引用专门绑定到右值,语法是 T&&。
cpp
int&& rref = 42; // 右值引用绑定到临时值
// int&& rref2 = x; // ❌ 不能绑定到左值(x 是左值)
区分左值引用与右值引用
cpp
void process(int& lref) {
cout << "左值引用版本" << endl;
}
void process(int&& rref) {
cout << "右值引用版本" << endl;
}
int main() {
int x = 10;
process(x); // 调用左值版本(x 是左值)
process(10); // 调用右值版本(10 是右值)
process(move(x)); // 把 x 转为右值,调用右值版本
}
四、移动构造函数与移动赋值运算符
编写移动构造函数
cpp
class Buffer {
private:
int* data;
size_t size;
public:
// 普通构造
Buffer(size_t n) : size(n), data(new int[n]) {}
// 析构
~Buffer() { delete[] data; }
// 拷贝构造(深拷贝)
Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
copy(other.data, other.data + size, data);
cout << "拷贝构造" << endl;
}
// 移动构造(“偷”资源)
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
cout << "移动构造" << endl;
}
// 移动赋值
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data; // 释放当前资源
data = other.data; // 转移所有权
size = other.size;
other.data = nullptr; // 置空源对象
other.size = 0;
cout << "移动赋值" << endl;
}
return *this;
}
// 拷贝赋值...
};
关键点:
-
参数是
T&& -
将源对象的资源"偷"过来
-
将源对象置于"有效但未定义"状态(通常是
nullptr) -
标记
noexcept(移动操作不应抛异常,便于标准库优化) -
移动后源对象仍然可以被析构(所以不能让它持有资源)
使用示例
cpp
Buffer a(100);
Buffer b = move(a); // 移动构造(a 不再拥有资源)
Buffer c;
c = move(b); // 移动赋值
五、std::move:仅仅是转型
std::move 这个名字容易误导------它并不移动任何东西。它只是一个类型转换:将左值转换为右值引用。
cpp
template<typename T>
decltype(auto) move(T&& arg) {
return static_cast<remove_reference_t<T>&&>(arg);
}
cpp
int x = 10;
int&& y = move(x); // 将 x 转为右值引用(但 x 仍然是左值)
// move(x) 告诉编译器:请把我当作右值对待
重要 :move 只是"请求"移动,真正移动发生在移动构造/赋值中。如果类型没有移动构造,仍会调用拷贝。
六、移动语义的收益示例
cpp
#include <iostream>
#include <vector>
#include <chrono>
using namespace std;
class BigData {
vector<int> data;
public:
BigData(size_t n) : data(n, 0) {
// cout << "构造" << endl;
}
// 拷贝构造(深拷贝)
BigData(const BigData& other) : data(other.data) {
cout << "拷贝构造(O(n))" << endl;
}
// 移动构造(O(1))
BigData(BigData&& other) noexcept : data(move(other.data)) {
cout << "移动构造(O(1))" << endl;
}
};
int main() {
cout << "=== 拷贝方式 ===" << endl;
BigData d1(1000000);
BigData d2 = d1; // 拷贝:100万整数被复制
cout << "\n=== 移动方式 ===" << endl;
BigData d3(1000000);
BigData d4 = move(d3); // 移动:只是交换指针,O(1)
return 0;
}
输出:
text
=== 拷贝方式 ===
拷贝构造(O(n))
=== 移动方式 ===
移动构造(O(1))
七、移动语义发生的典型场景
| 场景 | 说明 | 示例 |
|---|---|---|
| 函数返回局部变量 | 编译器自动移动 | return vec; |
std::move 显式转换 |
主动请求移动 | move(obj) |
| 标准库容器 | push_back 的右值版本 |
vec.push_back(move(s)) |
| 算法 | std::sort 等内部移动元素 |
swap 底层用移动 |
返回值优化(RVO)vs 移动
cpp
vector<int> createVector() {
vector<int> v(1000);
return v; // 编译器会使用 RVO(复制省略)或移动
}
现代编译器在返回局部变量时通常使用复制省略(Copy Elision),连移动都不需要,直接构造在目标位置。
八、完美转发初探
完美转发用于模板函数:将参数原封不动地转发给另一个函数,保持其左值/右值属性。
cpp
template<typename T>
void wrapper(T&& arg) {
// 想要把 arg 转发给 process,保持 arg 的原始类型
process(forward<T>(arg));
}
引用折叠规则
| 类型 | 折叠结果 |
|---|---|
T& & |
T& |
T& && |
T& |
T&& & |
T& |
T&& && |
T&& |
转发函数
cpp
void process(int& x) { cout << "左值" << endl; }
void process(int&& x) { cout << "左值" << endl; }
template<typename T>
void forwarder(T&& arg) {
process(forward<T>(arg)); // 保持 arg 的左值/右值属性
}
int main() {
int x = 10;
forwarder(x); // 转发左值 → 调用 process(int&)
forwarder(20); // 转发右值 → 调用 process(int&&)
}
九、std::move 与 std::forward 的区别
| 特性 | std::move |
std::forward |
|---|---|---|
| 作用 | 无条件转为右值引用 | 有条件地转为右值(仅当参数是右值时) |
| 常用场景 | 明确要移动对象 | 完美转发模板参数 |
| 典型写法 | move(obj) |
forward<T>(arg) |
十、常见错误
1. move 后继续使用对象
cpp
Buffer a(100);
Buffer b = move(a);
a.someMethod(); // ❌ 危险!a 处于未定义状态
2. 不必要的 move(阻止 RVO)
cpp
vector<int> createVector() {
vector<int> v(1000);
return move(v); // ❌ 阻止 RVO,反而可能变慢
}
3. const 对象不能移动
cpp
const Buffer a(100);
Buffer b = move(a); // ❌ 调用拷贝构造(const 不能绑定到 T&&)
4. 忘记标记 noexcept
移动操作不抛异常时应标记 noexcept,否则标准库(如 vector 扩容)可能选择拷贝而非移动。
十一、这一篇的收获
你现在应该理解:
-
左值 :有地址,持久;右值:临时,即将销毁
-
右值引用
T&&:绑定到右值,用于移动语义 -
移动构造/赋值:转移资源所有权,O(1) 操作,源对象置空
-
std::move:只是转型(左值 → 右值引用),不移动任何东西 -
std::forward:完美转发,保持参数原始类型 -
关键收益:避免深拷贝,尤其是容器、大对象
💡 小作业:实现一个
String类(类似std::string的子集),包含普通构造、拷贝构造、移动构造、析构。测试vector<String>的push_back在 C++11 前后的性能差异(模拟)。
下一篇预告:第33篇《C++异常处理:try/throw/catch的基本流程》------进入异常安全章节。异常是 C++ 的错误处理机制,但使用不当会导致资源泄漏。下篇讲清楚 try/throw/catch 的基本用法。