【c++面向对象编程】第32篇:移动语义与右值引用:现代C++性能优化核心
目录
一、一个昂贵的拷贝
二、左值 vs 右值:核心概念
三、右值引用 T&&
区分左值引用与右值引用
四、移动构造函数与移动赋值运算符
编写移动构造函数
使用示例
五、std::move:仅仅是转型
六、移动语义的收益示例
七、移动语义发生的典型场景
返回值优化(RVO)vs 移动
八、完美转发初探
引用折叠规则
转发函数
九、std::move 与 std::forward 的区别
十、常见错误
1. move 后继续使用对象
2. 不必要的 move(阻止 RVO)
3. const 对象不能移动
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 的基本用法。
