小学生学C++编程语法知识(C++移动构造函数)
一、先问一个问题:什么是“移动”?
先别急着看代码。
假设你有一个装满玩具的大箱子:
小明的箱子 ┌─────────────────┐ │ 🚗 🚕 🚌 🚲 🎮 │ └─────────────────┘现在小红也想要这些玩具。
有两种办法。
方法一:全部复制
把小明箱子里的玩具,一个一个复制一份给小红:
小明:🚗 🚕 🚌 🚲 🎮 ↓ 复制 小红:🚗 🚕 🚌 🚲 🎮这样当然可以。
但是:
如果箱子里有100万个玩具呢?
复制会很慢,而且需要额外的空间。
方法二:直接把箱子交给小红
小明说:
“我已经不要这个箱子了,你直接拿走吧!”
于是:
小明的箱子 ↓ └────────→ 小红的箱子根本不用把100万个玩具重新复制一遍。
这就是我们今天要学习的核心思想:
移动
二、C++为什么需要“移动”?
我们先看看一个比较典型的类。
class Box { public: int *data; int size; Box(int n) { size = n; data = new int[n]; } ~Box() { delete[] data; } };假设:
Box a(1000000);那么:
a │ ├── size = 1000000 │ └── data ───────→ 堆内存 ┌──────────────┐ │ 100万个数据 │ └──────────────┘现在:
Box b = a;如果使用普通的拷贝构造函数,就可能需要:
a │ └──────→ [100万个数据] b │ └──────→ [重新复制的100万个数据]也就是说:
复制一份非常大的数据。
这可能非常浪费时间。
三、于是C++11发明了一个好东西
C++11提供了:
移动构造函数
英文:
Move Constructor
它的作用可以简单理解成:
“你不用复制了,把资源直接交给我!”
四、移动构造函数长什么样?
最基本的形式:
class Box { public: int *data; int size; Box(int n) { size = n; data = new int[n]; } // 移动构造函数 Box(Box&& other) { data = other.data; size = other.size; other.data = nullptr; other.size = 0; } ~Box() { delete[] data; } };这里最重要的是:
Box(Box&& other)看到:
&&先不要害怕。
这里的:
Box&&叫做:
右值引用
我们后面可以专门学习。
现在先把它简单理解成:
“这个对象马上就不用了,可以把里面的资源搬走。”
五、移动构造函数到底干了什么?
这是最重要的部分。
假设:
Box a(1000000);现在:
a │ └──────→ 大量数据然后:
Box b(std::move(a));移动构造函数执行:
data = other.data;相当于:
a.data ───────────┐ ↓ 大量数据 ↑ │ b.data ───────────┘注意:
没有复制100万个数据!
只是把:
地址交给了b。
六、但是有一个非常重要的问题
如果只是:
data = other.data;那么:
a.data ──┐ ↓ 大数据 ↑ │ b.data ──┘现在:
a和b都认为自己拥有这块内存。
最后会发生什么?
b析构:
delete[] data;把内存释放掉。
然后a析构的时候:
delete[] data;又释放一次。
这就出问题了!
七、所以移动之后,要把“原来的对象”清空
所以我们必须写:
other.data = nullptr;也就是:
移动之前: a ─────→ 大数据 移动之后: a ─────→ nullptr b ─────→ 大数据这就非常安全了。
所以移动构造函数通常有一个非常重要的动作:
把资源交给新对象,然后把旧对象设置成“空壳”。
八、用搬家来理解
这个比喻非常适合小朋友。
假设:
小明家 ┌─────────────────┐ │ 🛏️ 🪑 📺 🧸 🎮 │ └─────────────────┘小明要搬家。
普通拷贝
小明把所有东西复制一份:
小明家 小红家 🛏️ 🛏️ 🪑 复制 🪑 📺 ─────→ 📺 🧸 🧸 🎮 🎮很慢。
移动
小明说:
“我不住这里了,这些东西全部给你。”
于是:
小明家 小红家 空房子 🛏️ 🪑 📺 🧸 🎮东西没有复制。
只是:
换了一个主人。
这就是移动构造函数。
九、移动构造函数和拷贝构造函数有什么区别?
这是考试和学习中非常重要的一组概念。
| 拷贝构造 | 移动构造 | |
|---|---|---|
| 英文 | Copy Constructor | Move Constructor |
| 主要操作 | 复制资源 | 转移资源 |
| 是否复制数据 | 通常是 | 通常不是 |
| 是否更快 | 大对象时较慢 | 大对象时通常更快 |
| 原对象 | 保持原来的数据 | 通常变成可安全析构的空状态 |
| 参数 | const T& | T&& |
例如:
拷贝
Box(const Box& other) { // 复制数据 }意思:
给我复制一份。
移动
Box(Box&& other) { // 接管资源 }意思:
不用复制,直接把你的资源给我。
十、为什么要写std::move?
例如:
Box a(1000000); Box b(std::move(a));这里:
std::move(a)可以理解成告诉编译器:
“a后面我基本不需要它原来的资源了,你可以把它搬走。”
于是优先使用:
Box(Box&& other)移动构造函数。
十一、一个完整的简单例子
#include <iostream> #include <utility> using namespace std; class Box { private: int *data; int size; public: // 普通构造函数 Box(int n) { size = n; data = new int[n]; cout << "构造函数" << endl; } // 拷贝构造函数 Box(const Box& other) { size = other.size; data = new int[size]; for (int i = 0; i < size; i++) { data[i] = other.data[i]; } cout << "拷贝构造函数" << endl; } // 移动构造函数 Box(Box&& other) { size = other.size; data = other.data; // 原来的对象放弃资源 other.size = 0; other.data = nullptr; cout << "移动构造函数" << endl; } // 析构函数 ~Box() { delete[] data; cout << "析构函数" << endl; } }; int main() { Box a(1000000); Box b(std::move(a)); return 0; }这里:
Box b(std::move(a));大致发生:
a │ └────→ 100万个数据 移动 ↓ a │ └────→ nullptr b │ └────→ 100万个数据数据没有复制。
只是:
所有权发生了转移。
十二、移动构造函数最大的作用是什么?
可以总结成三个字:
更高效!
特别是对象内部管理:
大数组 大字符串 文件资源 动态内存 大型容器的时候。
例如:
vector<int> a(10000000);如果把这么大的vector完完整整复制一份:
a ↓ 1000万个元素 ↓复制 b ↓ 1000万个元素很浪费。
移动就可以:
a ↓ 数据地址 ─────────┐ │ ↓ 数据 ↑ │ b ────────────────┘直接把内部资源接过来。
这也是为什么现代 C++ 中:
移动语义非常重要。
十三、移动构造函数并不是“复制的高级版本”
这一点一定要记住。
很多初学者会认为:
移动构造 = 更快的拷贝构造。
其实这个理解不够准确。
它们的思想完全不同:
拷贝
A │ └──→ 资源 ↓复制 B │ └──→ 新资源是:
我要一份新的。
移动
A │ └──→ 资源 ↓搬走 A │ └──→ 空 B │ └──→ 原来的资源是:
我不要了,你拿去用。
所以:
拷贝 = 复制
移动 = 转让
十四、什么时候会用到移动构造函数?
初学阶段可以先记住一种典型情况:
Box b(std::move(a));以及以后你会经常看到:
return 一个对象;例如:
Box createBox() { Box temp(1000000); return temp; }这里涉及:
创建临时对象 ↓ 把对象交给外面 ↓ 如果每次都完整复制 ↓ 可能很浪费现代 C++ 会通过:
返回值优化(RVO/NRVO)
以及:
移动语义
来尽可能避免不必要的复制。
所以以后你看到:
vector<int> getData() { vector<int> v(1000000); return v; }不要简单理解成:
“把100万个元素复制出去。”
现代 C++ 通常会尽量避免这种昂贵的复制。
十五、一个非常重要的误区
千万不要认为:
“只要有
&&,就一定会发生移动。”
例如:
Box&&是:
右值引用类型
而:
std::move(a)是:
把一个表达式转换成可以被移动的右值形式。
真正是否发生移动,还要看:
有没有匹配的移动构造函数/移动赋值运算符,以及具体的重载决议。
初学阶段可以先记:
Box b(std::move(a));→ 很可能调用移动构造函数。
十六、为什么移动构造函数参数一定是&&?
你可以把:
Box(const Box& other)理解成:
“这个东西还要用,所以我只能复制。”
而:
Box(Box&& other)理解成:
“这个东西马上要被处理掉了,你可以把里面的资源拿走。”
所以:
const T& ↓ 我还需要你 ↓ 复制 T&& ↓ 我准备把你搬走 ↓ 移动这就是右值引用存在的重要意义之一。
十七、最后给孩子一个“超级记忆法”
以后看到:
Box(const Box& other)就想:
📋复印机
“给我复制一份。”
看到:
Box(Box&& other)就想:
🚚搬家公司
“你不用复制,东西直接搬给我。”
所以:
对象资源 │ ┌────────┴────────┐ ↓ ↓ 拷贝 移动 │ │ ↓ ↓ 复制一份 直接搬走 │ │ ↓ ↓ 两份资源 一份资源⭐ 最后用一句话记住
移动构造函数就是:当一个新对象可以“接管”另一个对象的资源时,不再傻傻地复制数据,而是把资源直接搬过来,从而提高程序效率。
最核心的代码就是:
Box(Box&& other) { data = other.data; // 接管资源 other.data = nullptr; // 原对象放弃资源 }可以把它牢牢记成:
“资源搬过来,旧主人变空壳。”
而下节课继续学习“右值引用&&到底是什么?为什么std::move()能让移动构造函数被调用?”。把这两个知识点弄明白,移动构造函数就真正理解了。
