“移动语义”这个高深词汇背后是什么?
\n\n想象一下,你要搬进新公寓。你有一个又大又重的沙发。哪种方式更快:自己扛着它爬五层楼梯,还是直接把钥匙交给搬运工,说:“伙计们,沙发归你们了,随便处理,我不需要了”?第二种方式,就是C++中的移动语义。
\n\n在C++11标准出现之前,程序员们不得不痛苦地复制数据。每次从函数返回一个向量或字符串时,编译器都会创建一个精确的副本。如果向量包含一百万个元素——它会老老实实地复制每一个。这很慢、效率低下,就像用手抄写一本书,而不是直接把原稿给别人。
\n\n移动语义是一种机制,它允许将资源(内存、文件描述符、套接字)从一个对象移动到另一个对象,而无需昂贵的复制操作。移动后,原始对象会处于“空”但有效的状态,可以安全地销毁或为其赋予新值。
\n\n在本文中,我们将用通俗易懂的语言和代码示例,讲解右值引用、std::move函数、移动构造函数和移动赋值运算符。
\n\n为什么没有移动语义代码会变慢?
\n\n让我们看一个经典的例子,这个问题在2011年之前一直困扰着所有C++程序员:
\n\n#include <iostream>\n#include <vector>\n#include <string>\n\nstd::vector<std::string> createBigVector() {\n std::vector<std::string> vec(1000000, "Hello");\n return vec; // 糟糕!复制了一百万个字符串!\n}\n\nint main() {\n std::vector<std::string> data = createBigVector();\n std::cout << "Size: " << data.size() << std::endl;\n return 0;\n}\n\n这里发生了什么?函数createBigVector创建了一个向量,填充数据并返回。编译器(未优化时)会执行以下操作:
- \n
- 在函数内部创建向量。 \n
- 将整个向量复制到返回值中。 \n
- 销毁原始向量。 \n
- 在
main函数中,将该值复制到变量data中。 \n
结果:对一百万个字符串进行了两次完整复制。这不仅慢——对性能来说简直是灾难。
\n\n右值和左值:C++的两个世界
\n\n要理解移动语义,需要先了解两个基本概念:左值和右值。
\n\n- \n
- 左值 — 在内存中有地址和名称的对象。你可以获取它的地址(
&obj)。例如:变量int x = 5;—x是左值。 \n - 右值 — 没有名称的临时值,不能出现在赋值运算符的左侧。例如:
5、x + 3、函数getTemp()的返回值。你不能写&(x + 3)。 \n
以前,右值引用(用&&表示)并不存在。编译器看到一个临时对象会说:“嗯,这是右值,我唯一能做的就是复制它或销毁它。”移动语义给了编译器第三种选择:窃取右值的资源,因为它反正很快就会被销毁。
std::move和右值引用是如何工作的?
\n\nstd::move函数只是一个类型转换。它实际上并不移动数据,只是将左值转换为右值,从而允许编译器“掠夺”该对象。
\n\n考虑一个管理动态内存的简单类:
\n\n#include <iostream>\n#include <cstring>\n\nclass MyString {\nprivate:\n cha