Ch 24: 移动语义
- 理解移动语义解决的核心问题(避免不必要的拷贝)
- 掌握右值引用
T&&的语义和用法 - 学会实现移动构造函数和移动赋值运算符
- 熟练使用
std::move和std::forward - 理解移动语义在容器和返回值优化中的应用
24.1 Python 赋值 vs C++ 移动
Section titled “24.1 Python 赋值 vs C++ 移动”Python 的赋值语义
Section titled “Python 的赋值语义”# Python - 赋值是引用拷贝a = [1, 2, 3] # a 指向列表对象b = a # b 也指向同一个列表对象b.append(4) # 修改的是同一个对象print(a) # [1, 2, 3, 4] - a 也被修改!
# 要独立副本需要 copyc = a.copy() # 或 list(a) 或 a[:]c.append(5) # c 修改,a 不变C++ 区分拷贝和移动
Section titled “C++ 区分拷贝和移动”#include <vector>#include <iostream>
int main() { std::vector<int> v1 = {1, 2, 3};
// 拷贝 - 复制所有元素 std::vector<int> v2 = v1; // v1 不变,v2 是独立副本
// 移动 - 转移所有权,不复制数据 std::vector<int> v3 = std::move(v1); // v1 变为空!v3 接管 v1 的内部数据 // 不需要分配新内存,不复制元素
std::cout << "v2 size: " << v2.size() << "\n"; // 3 std::cout << "v3 size: " << v3.size() << "\n"; // 3 std::cout << "v1 size: " << v1.size() << "\n"; // 0(空)
return 0;}为什么需要移动语义
Section titled “为什么需要移动语义”#include <vector>#include <string>#include <iostream>
// 假设有大型对象class HeavyObject {public: HeavyObject() { std::cout << "Default construct\n"; }
HeavyObject(const HeavyObject&) { std::cout << "Copy construct - expensive!\n"; }
HeavyObject(HeavyObject&&) noexcept { std::cout << "Move construct - cheap!\n"; }};
int main() { std::vector<HeavyObject> vec;
// push_back 需要拷贝 vec.push_back(HeavyObject()); // 移动而不是拷贝
// emplace_back 直接构造 vec.emplace_back(); // 直接构造,不移动不拷贝
return 0;}24.2 左值和右值详解
Section titled “24.2 左值和右值详解”#include <iostream>#include <utility>
int main() { int x = 42;
// 左值 - 有名字,可以取地址 int& lref = x; // 左值引用 const int& clref = x; // const 左值引用
// 右值 - 没有持久名字,临时对象 int&& rref = 42; // 右值引用,绑定到临时对象 int&& rref2 = x + 1; // x+1 是临时(右值)
// 常见右值 42; // 字面量 std::string("hello"); // 临时对象 x + 1; // 表达式结果 std::move(x); // std::move 返回右值引用
// 左值引用绑定规则 // int& r1 = 42; // ❌ 错误!左值引用不能绑定到右值 // const int& r2 = 42; // ✅ OK!const 引用可以绑定到右值
// 右值引用绑定规则 // int&& r3 = x; // ❌ 错误!右值引用不能绑定到左值 int&& r4 = 42; // ✅ OK int&& r5 = std::move(x); // ✅ OK!std::move 把左值转右值引用
return 0;}左值 vs 右值判断
Section titled “左值 vs 右值判断”#include <iostream>#include <utility>
// 左值引用void process(int& value) { std::cout << "Lvalue ref: " << value << "\n";}
// 右值引用void process(int&& value) { std::cout << "Rvalue ref: " << value << "\n";}
int main() { int x = 10;
process(x); // 调用 Lvalue ref(x 是左值) process(10); // 调用 Rvalue ref(10 是右值) process(x + 5); // 调用 Rvalue ref(x+5 是右值) process(std::move(x)); // 调用 Rvalue ref
return 0;}移动 vs 拷贝
Section titled “移动 vs 拷贝”#include <vector>#include <string>#include <iostream>
class Buffer {public: Buffer(std::size_t size) : size_(size), data_(new char[size]) { std::cout << "Construct buffer of size " << size_ << "\n"; }
~Buffer() { delete[] data_; std::cout << "Destroy buffer\n"; }
// 拷贝构造函数 Buffer(const Buffer& other) : size_(other.size_), data_(new char[other.size_]) { std::cout << "Copy buffer\n"; std::copy(other.data_, other.data_ + other.size_, data_); }
// 移动构造函数 Buffer(Buffer&& other) noexcept : size_(other.size_), data_(other.data_) { std::cout << "Move buffer\n"; other.data_ = nullptr; other.size_ = 0; }
std::size_t size() const { return size_; }
private: std::size_t size_; char* data_;};
int main() { std::cout << "=== Creating b1 ===\n"; Buffer b1(1000);
std::cout << "\n=== Copying b1 to b2 ===\n"; Buffer b2 = b1; // 拷贝(分配内存,复制数据)
std::cout << "\n=== Moving b1 to b3 ===\n"; Buffer b3 = std::move(b1); // 移动(只是转移指针,不分配内存)
std::cout << "\n=== b1 after move ===\n"; std::cout << "b1 size: " << b1.size() << "\n"; // 0(空)
std::cout << "\n=== End of main ===\n";
return 0;}24.3 移动构造函数详解
Section titled “24.3 移动构造函数详解”#include <utility>#include <iostream>#include <cstring>
class String {public: // 构造 String(const char* s) { std::size_t len = std::strlen(s); data_ = new char[len + 1]; std::copy(s, s + len + 1, data_); std::cout << "Construct: " << data_ << "\n"; }
// 拷贝构造 String(const String& other) { std::size_t len = std::strlen(other.data_); data_ = new char[len + 1]; std::copy(other.data_, other.data_ + len + 1, data_); std::cout << "Copy: " << data_ << "\n"; }
// 移动构造 String(String&& other) noexcept { data_ = other.data_; // 转移资源 other.data_ = nullptr; // 防止析构时删除 std::cout << "Move: " << data_ << "\n"; }
~String() { delete[] data_; }
const char* c_str() const { return data_ ? data_ : "(null)"; }
private: char* data_ = nullptr;};
int main() { String s1("Hello"); std::cout << "s1: " << s1.c_str() << "\n\n";
std::cout << "=== Copying s1 to s2 ===\n"; String s2 = s1; std::cout << "s2: " << s2.c_str() << "\n\n";
std::cout << "=== Moving s1 to s3 ===\n"; String s3 = std::move(s1); std::cout << "s3: " << s3.c_str() << "\n"; std::cout << "s1 after move: " << s1.c_str() << "\n\n";
return 0;}noexcept 的重要性
Section titled “noexcept 的重要性”#include <vector>#include <iostream>
class Movable {public: Movable() = default;
// noexcept 移动操作不会触发异常 // C++11/14 中 vector 在 realloc 时会使用移动而不是拷贝 Movable(Movable&& other) noexcept : data_(other.data_) { other.data_ = nullptr; }
Movable& operator=(Movable&& other) noexcept { if (this != &other) { delete[] data_; data_ = other.data_; other.data_ = nullptr; } return *this; }
~Movable() { delete[] data_; }
private: int* data_ = nullptr;};
int main() { std::vector<Movable> vec;
// 如果移动构造函数是 noexcept,vector realloc 时会使用移动 // 如果没有 noexcept,vector 会使用拷贝(更安全但更慢) for (int i = 0; i < 100; ++i) { vec.push_back(Movable()); }
return 0;}24.4 移动赋值运算符
Section titled “24.4 移动赋值运算符”#include <iostream>#include <utility>
class Buffer {public: Buffer(std::size_t size) : size_(size), data_(new int[size]) { std::cout << "Construct Buffer(" << size_ << ")\n"; }
// 移动赋值运算符 Buffer& operator=(Buffer&& other) noexcept { if (this != &other) { // 自赋值检查 delete[] data_; // 释放原有资源 data_ = other.data_; size_ = other.size_; other.data_ = nullptr; // 防止析构 other.size_ = 0; std::cout << "Move assignment\n"; } return *this; }
// 拷贝赋值运算符 Buffer& operator=(const Buffer& other) { if (this != &other) { delete[] data_; size_ = other.size_; data_ = new int[size_]; std::copy(other.data_, other.data_ + size_, data_); std::cout << "Copy assignment\n"; } return *this; }
~Buffer() { delete[] data_; std::cout << "Destroy Buffer(" << size_ << ")\n"; }
std::size_t size() const { return size_; }
private: std::size_t size_; int* data_;};
int main() { Buffer b1(100); Buffer b2(50);
std::cout << "\n=== b2 = std::move(b1) ===\n"; b2 = std::move(b1); // 移动赋值 std::cout << "b2 size: " << b2.size() << "\n";
std::cout << "\n=== End of main ===\n";
return 0;}24.5 std::move 和 std::forward
Section titled “24.5 std::move 和 std::forward”std::move
Section titled “std::move”#include <utility>#include <iostream>#include <vector>#include <string>
int main() { // std::move 将左值转为右值引用 int x = 42; int&& r = std::move(x); // x 变为右值
std::string s1 = "Hello"; std::string s2 = std::move(s1); // s1 变为空
std::cout << "s2: " << s2 << "\n"; std::cout << "s1: " << (s1.empty() ? "(empty)" : s1) << "\n";
// 在容器中使用 std::vector<std::string> vec; std::string s = "World"; vec.push_back(std::move(s)); // 移动而不是拷贝
std::cout << "vec[0]: " << vec[0] << "\n"; std::cout << "s after move: " << (s.empty() ? "(empty)" : s) << "\n";
return 0;}std::forward
Section titled “std::forward”#include <utility>#include <iostream>#include <string>
// 模板参数推导template<typename T>void process_value(T value) { // 传入什么就转发什么 std::cout << "process_value\n";}
template<typename T>void process_lvalue(T& value) { std::cout << "process_lvalue (T&)\n";}
template<typename T>void process_rvalue(T&& value) { std::cout << "process_rvalue (T&&)\n";}
// 完美转发:保持原始值类别template<typename T>void wrapper(T&& arg) { // 万能引用 // std::forward<T> 转发时保持原始值类别 // 如果传入左值,转发为左值引用 // 如果传入右值,转发为右值引用
process_value(std::forward<T>(arg)); // 或根据需要选择 // process_lvalue(arg); // process_rvalue(std::move(arg));}
void take_string(std::string& s) { std::cout << "Lvalue string: " << s << "\n";}
void take_string(std::string&& s) { std::cout << "Rvalue string: " << s << "\n";}
template<typename T>void wrapper2(T&& arg) { take_string(std::forward<T>(arg));}
int main() { std::string s = "Hello";
// 传入左值 wrapper(s); // T = std::string& wrapper("world"); // T = const char*(触发类型转换)
// 传入右值 wrapper(std::string("temp"));
// 完美转发保持值类别 wrapper2(s); // 调用 lvalue 版本 wrapper2(std::string("temp")); // 调用 rvalue 版本
return 0;}move 和 forward 的区别
Section titled “move 和 forward 的区别”#include <utility>#include <iostream>
// std::move 总是转发为右值template<typename T>void caller1(T&& arg) { // std::move 强制转右值 process(std::move(arg)); // 总是调用右值版本}
// std::forward 保持原始值类别template<typename T>void caller2(T&& arg) { // std::forward 保持左/右属性 process(std::forward<T>(arg)); // 传入什么转发什么}
void process(int&) { std::cout << "Lvalue\n"; }void process(int&&) { std::cout << "Rvalue\n"; }
int main() { int x = 42;
caller1(x); // 转发为右值,调用 process(int&&) caller1(42); // 转发为右值
caller2(x); // 转发为左值,调用 process(int&) caller2(42); // 转发为右值,调用 process(int&&)
return 0;}24.6 移动语义和返回值优化
Section titled “24.6 移动语义和返回值优化”返回值优化(RVO/NRVO)
Section titled “返回值优化(RVO/NRVO)”#include <iostream>#include <vector>#include <string>
// 情况 1:返回局部对象std::vector<int> create_vector() { std::vector<int> result = {1, 2, 3}; return result; // 可能触发移动或拷贝(C++17 保证移动)}
// 情况 2:直接构造返回值std::vector<int> create_vector2() { return std::vector<int>{1, 2, 3}; // 可能触发 RVO}
// 情况 3:命名返回值优化(NRVO)std::vector<int> create_vector3() { std::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); return v; // 编译器可能优化,不移动直接构造到返回值位置}
// C++17 保证 RVOtemplate<typename T>T make_holder(T&& value) { return std::forward<T>(value); // C++17 保证不拷贝不移动}
int main() { auto v1 = create_vector(); auto v2 = create_vector2();
std::cout << "v1 size: " << v1.size() << "\n"; std::cout << "v2 size: " << v2.size() << "\n";
return 0;}手动移动 vs 隐式移动
Section titled “手动移动 vs 隐式移动”#include <string>#include <iostream>
class Person {public: Person(const std::string& n) : name(n) { std::cout << "Construct: " << name << "\n"; }
Person(const Person& p) : name(p.name) { std::cout << "Copy: " << name << "\n"; }
Person(Person&& p) noexcept : name(std::move(p.name)) { std::cout << "Move: " << name << "\n"; }
Person& operator=(const Person& p) { name = p.name; std::cout << "Copy assign: " << name << "\n"; return *this; }
Person& operator=(Person&& p) noexcept { name = std::move(p.name); std::cout << "Move assign: " << name << "\n"; return *this; }
private: std::string name;};
Person create_person1() { Person p("Alice"); return p; // NRVO 可能发生(编译器决定)}
Person create_person2() { return Person("Bob"); // RVO 可能发生}
int main() { std::cout << "=== create_person1 ===\n"; Person p1 = create_person1();
std::cout << "\n=== create_person2 ===\n"; Person p2 = create_person2();
std::cout << "\n=== Move into existing ===\n"; Person p3("Charlie"); p3 = std::move(p1); // 移动赋值
return 0;}24.7 移动语义在容器中的应用
Section titled “24.7 移动语义在容器中的应用”#include <vector>#include <iostream>#include <string>
class Item {public: std::string name; Item(const std::string& n) : name(n) { std::cout << "Construct: " << name << "\n"; } Item(Item&& other) noexcept : name(std::move(other.name)) { std::cout << "Move: " << name << "\n"; } Item& operator=(Item&& other) noexcept { name = std::move(other.name); std::cout << "Move assign: " << name << "\n"; return *this; }};
int main() { std::vector<Item> vec;
std::cout << "=== emplace_back ===\n"; vec.emplace_back("Apple"); // 直接构造,不移动
std::cout << "\n=== push_back with move ===\n"; Item item("Banana"); vec.push_back(std::move(item)); // 移动
std::cout << "\n=== push_back with temp ===\n"; vec.push_back(Item("Cherry")); // 临时对象可能直接构造
std::cout << "\n=== End ===\n";
return 0;}24.8 章节总结
Section titled “24.8 章节总结”| 概念 | 说明 |
|---|---|
| 左值 | 有名字,持久存在,可以取地址 |
| 右值 | 临时对象,没有持久名字 |
左值引用 T& | 绑定左值 |
右值引用 T&& | 绑定右值(临时对象) |
std::move | 将左值转为右值引用 |
std::forward<T> | 保持原始值类别转发 |
| 移动构造函数 | 转移资源所有权,不复制数据 |
| 移动赋值运算符 | 释放当前资源,转移新资源所有权 |
使用原则:
- 拷贝构造复制数据,移动构造转移所有权
noexcept保证让容器在 realloc 时使用移动std::move用于将左值转右值(转移所有权)std::forward用于模板参数转发(保持值类别)- 移动后原对象处于有效但未定义状态
下章预告:ch25 学习 RAII 模式,理解资源获取即初始化的核心思想。