ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

小学生学C++编程语法知识(C++移动构造函数)

小学生学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 ──┘

现在:

ab都认为自己拥有这块内存。

最后会发生什么?

b析构:

delete[] data;

把内存释放掉。

然后a析构的时候:

delete[] data;

又释放一次。

这就出问题了!


七、所以移动之后,要把“原来的对象”清空

所以我们必须写:

other.data = nullptr;

也就是:

移动之前: a ─────→ 大数据 移动之后: a ─────→ nullptr b ─────→ 大数据

这就非常安全了。

所以移动构造函数通常有一个非常重要的动作:

把资源交给新对象,然后把旧对象设置成“空壳”。


八、用搬家来理解

这个比喻非常适合小朋友。

假设:

小明家 ┌─────────────────┐ │ 🛏️ 🪑 📺 🧸 🎮 │ └─────────────────┘

小明要搬家。


普通拷贝

小明把所有东西复制一份:

小明家 小红家 🛏️ 🛏️ 🪑 复制 🪑 📺 ─────→ 📺 🧸 🧸 🎮 🎮

很慢。


移动

小明说:

“我不住这里了,这些东西全部给你。”

于是:

小明家 小红家 空房子 🛏️ 🪑 📺 🧸 🎮

东西没有复制。

只是:

换了一个主人。

这就是移动构造函数。


九、移动构造函数和拷贝构造函数有什么区别?

这是考试和学习中非常重要的一组概念。

拷贝构造移动构造
英文Copy ConstructorMove 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()能让移动构造函数被调用?”。把这两个知识点弄明白,移动构造函数就真正理解了。


返回列表