第19天:移动语义

学习目标

掌握C++11引入的移动语义,理解左值与右值的概念,学会使用移动构造函数和移动赋值运算符优化性能。

核心知识点

1. 移动语义概述

为什么需要移动语义?

  • 性能优化:避免不必要的深拷贝
  • 资源管理:高效转移资源所有权
  • 异常安全:减少异常时的资源浪费
  • 现代C++:支持现代C++编程模式

移动语义的核心思想

  • 转移而非复制:将资源从一个对象转移到另一个对象
  • 临时对象优化:充分利用临时对象的资源
  • 零拷贝:避免不必要的资源复制

参考资料

2. 左值与右值

左值 (Lvalue)

左值是可以取地址的表达式,通常表示一个对象的内存位置。

1
2
3
int x = 42;        // x是左值
int& ref = x; // ref是左值引用
int* ptr = &x; // 可以取x的地址

右值 (Rvalue)

右值是不能取地址的表达式,通常是临时对象或字面量。

1
2
3
4
5
6
7
8
9
int x = 42;                    // 42是右值
std::string s = "Hello"; // "Hello"是右值
int result = x + 5; // x + 5是右值

// 函数返回的临时对象是右值
std::string getString() {
return "World";
}
std::string str = getString(); // getString()返回的是右值

左值引用与右值引用

1
2
3
4
5
6
7
8
9
10
int x = 42;

// 左值引用
int& lref = x; // 正确:绑定到左值
// int& lref2 = 42; // 错误:不能绑定到右值

// 右值引用
int&& rref = 42; // 正确:绑定到右值
int&& rref2 = x + 5; // 正确:绑定到右值
// int&& rref3 = x; // 错误:不能绑定到左值

参考资料

3. 移动构造函数

移动构造函数用于从右值转移资源,避免深拷贝。

基本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class MyString {
private:
char* data;
size_t size;

public:
// 构造函数
MyString(const char* str = "") : size(strlen(str)) {
data = new char[size + 1];
strcpy(data, str);
}

// 拷贝构造函数
MyString(const MyString& other) : size(other.size) {
data = new char[size + 1];
strcpy(data, other.data);
}

// 移动构造函数
MyString(MyString&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}

// 析构函数
~MyString() {
delete[] data;
}

const char* c_str() const { return data; }
size_t length() const { return size; }
};

移动构造函数要点

  • noexcept:移动操作不应抛出异常
  • 资源转移:将资源从源对象转移到目标对象
  • 源对象重置:将源对象重置为有效但未指定状态

参考资料

4. 移动赋值运算符

移动赋值运算符用于从右值转移资源到已存在的对象。

基本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MyString {
// ... 其他成员 ...

// 拷贝赋值运算符
MyString& operator=(const MyString& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new char[size + 1];
strcpy(data, other.data);
}
return *this;
}

// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
};

移动赋值运算符要点

  • 自赋值检查:检查是否是自己赋值给自己
  • 资源释放:先释放当前对象的资源
  • 资源转移:转移源对象的资源
  • 源对象重置:重置源对象状态

参考资料

5. std::move

std::move用于将左值转换为右值引用,启用移动语义。

基本使用

1
2
3
4
5
6
7
8
9
10
11
#include <utility>

std::string str1 = "Hello";
std::string str2 = "World";

// 使用std::move
str2 = std::move(str1); // str1的资源被移动到str2

// str1现在处于有效但未指定状态
std::cout << "str1: " << str1 << std::endl; // 可能为空
std::cout << "str2: " << str2 << std::endl; // "Hello"

容器中的移动

1
2
3
4
5
6
7
8
std::vector<std::string> vec;

std::string temp = "Hello World";
vec.push_back(std::move(temp)); // 移动temp到容器中

// temp现在可能为空
std::cout << "temp: " << temp << std::endl;
std::cout << "vec[0]: " << vec[0] << std::endl;

std::move的本质

1
2
3
4
5
// std::move的简化实现
template<typename T>
typename std::remove_reference<T>::type&& move(T&& t) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(t);
}

参考资料

6. 完美转发 (Perfect Forwarding)

完美转发允许函数模板将其参数完美地转发给另一个函数。

基本概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <utility>

// 完美转发示例
template<typename T>
void forwardFunction(T&& arg) {
// 使用std::forward保持参数的值类别
actualFunction(std::forward<T>(arg));
}

// 工厂函数示例
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

万能引用 (Universal Reference)

1
2
3
4
5
6
7
template<typename T>
void func(T&& param); // 万能引用,不是右值引用

// 根据传入参数的类型推导
int x = 42;
func(x); // T推导为int&,param类型为int&
func(42); // T推导为int,param类型为int&&

参考资料

7. 移动语义与容器

容器中的移动优化

1
2
3
4
5
6
7
8
9
10
11
std::vector<std::string> vec;

// emplace_back避免不必要的拷贝
vec.emplace_back("Hello");
vec.emplace_back("World");

// 移动元素
std::string temp = std::move(vec[0]);

// 移动整个容器
std::vector<std::string> vec2 = std::move(vec);

移动语义与算法

1
2
3
4
5
std::vector<std::string> source = {"a", "b", "c"};
std::vector<std::string> dest;

// 移动算法
std::move(source.begin(), source.end(), std::back_inserter(dest));

参考资料

8. 移动语义最佳实践

五法则 (Rule of Five)

如果类需要自定义以下任何一个,通常需要定义全部五个:

  1. 析构函数
  2. 拷贝构造函数
  3. 拷贝赋值运算符
  4. 移动构造函数
  5. 移动赋值运算符

零法则 (Rule of Zero)

尽量使用RAII和智能指针,避免手动资源管理。

性能考虑

  • 移动优于拷贝:在可能的情况下使用移动
  • noexcept:移动操作应标记为noexcept
  • 返回值优化:利用RVO和NRVO

参考资料

实用教程和文档

官方文档

优质教程

实战案例

今日练习

基础练习

  1. 实现一个资源管理类,支持移动构造和移动赋值
  2. 编写一个性能测试,比较拷贝和移动的性能差异
  3. 实现一个工厂模式,使用移动语义优化对象创建

算法题推荐

  1. LeetCode 146. LRU Cache - 使用移动语义优化缓存操作
  2. LeetCode 208. Implement Trie (Prefix Tree) - 使用移动语义优化字典树
  3. LeetCode 341. Flatten Nested List Iterator - 使用移动语义优化迭代器
  4. LeetCode 460. LFU Cache - 使用移动语义优化LFU缓存

学习要点总结

  1. 左值右值:理解左值和右值的区别
  2. 移动语义:掌握移动构造函数和移动赋值运算符
  3. std::move:学会使用std::move启用移动语义
  4. 完美转发:理解std::forward的作用
  5. 性能优化:通过移动语义提高程序性能

下一天预告

明天我们将学习并发编程基础,包括std::thread、同步机制、异步编程等C++11的并发特性。

上一天:Lambda表达式 | 返回第三周总览 | 下一天:并发编程基础