C 11 移动语义

时间:2020-11-24 18:10:02

【1】为什么引入移动语义?

拷贝构造函数中为指针成员分配新的内存再进行内容拷贝的做法在C 编程中几乎被视为是最合理的。

不过在有些时候,我们会发现确实不需要这样的拷贝构造语义。如下示例:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class HasPtrMem
 5 { 
 6 public:
 7     HasPtrMem() : d(new int(0))
 8     { 
 9         cout << "Construct: " <<   n_cstr << endl;
10     } 
11     HasPtrMem(const HasPtrMem & h) : d(new int(* h.d))
12     { 
13         cout << "Copy construct: " <<   n_cptr << endl;
14     }
15     ~ HasPtrMem()
16     { 
17         cout << "Destruct: " <<   n_dstr << endl;
18     } 
19     
20     int* d;
21     static int n_cstr; // 统计构造函数调用次数 
22     static int n_dstr; // 统计析构函数调用次数
23     static int n_cptr; // 统计拷贝构造函数调用次数
24 };
25 
26 int HasPtrMem::n_cstr = 0;
27 int HasPtrMem::n_dstr = 0;
28 int HasPtrMem::n_cptr = 0;
29 
30 HasPtrMem GetTemp()
31 { 
32     return HasPtrMem();
33 } 
34 
35 int main()
36 { 
37     HasPtrMem a = GetTemp();
38 }

声明了一个返回一个HasPtrMem对象的函数。

为了记录构造函数、拷贝构造函数,以及析构函数调用的次数,使用了三个静态变量。

在main函数中,简单地声明了一个HasPtrMem的对象a,要求它使用GetTemp的返回值进行初始化。

编译运行该程序,看到如下输出:

Construct: 1
Copy construct : 1
Destruct : 1
Copy construct : 2
Destruct : 2
Destruct : 3

构造函数被调用了一次,这是在GetTemp函数中HasPtrMem()表达式显式地调用了构造函数而打印出来的。

拷贝构造函数则被调用了两次:

一次是从GetTemp函数中HasPtrMem()生成的量上拷贝构造出一个临时值,以用作GetTemp的返回值;

另外一次则是由临时值构造出main中对象a调用的。

对应地,析构函数也就调用了3次。整个过程如下图所示:

C  11 移动语义

最让人感到不安就是拷贝构造函数的调用。在上例中,类HasPtrMem仅仅只有一个int类型的指针。

而如果HasPtrMem的指针指向非常大的堆内存数据的话,那么拷贝构造的过程就会非常昂贵。

可以想象,这种情况一旦发生,a的初始化表达式的执行速度将相当堪忧。

那肿么办呢?让我们把目光再次聚焦到临时对象上,即上图中的main函数的部分。

按照C 的语义,临时对象将在语句结束后被析构,会释放它所包含的堆内存资源。

而a在进行拷贝构造的时候,又会被分配堆内存,这样的一释放又一分配似乎并没有太大的意义。

那么我们是否可以在临时对象构造a的时候不分配内存,即不使用所谓的拷贝构造语义呢?

【2】移动语义

针对上节谈到的问题,其实问题真正的核心:

在临时对象将被眼睁睁地析构掉之前我们又重新申请与其相同内存且复制内容的多余过程。

为了不做这么多的无用功,请看下图(拷贝构造函数与移动构造函数)的新方法:

C  11 移动语义

上半部分(拷贝构造方式):

从临时对象进行拷贝构造对象a时,在执行拷贝时先分配新的堆内存,并从临时对象的堆内存中拷贝内容至a.d。

构造完成后,临时对象被将析构,因此其拥有的堆内存资源会被析构函数释放。

下半部分(移动构造方式):

在构造时,使得a.d指向临时对象的堆内存资源。

同时,我们保证临时对象不释放其所指向的堆内存(下面代码演示怎么实现)。

那么,在构造完成后,即使临时对象被析构,a就从中“偷”到了临时对象所拥有的堆内存资源。

在C 11中,这样的“偷走”临时对象中资源的构造函数,就被称为“移动构造函数”。而这样的“偷”的行为,则称之为“移动语义”(move semantics)。

请看如下代码实现:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class HasPtrMem
 5 { 
 6 public:
 7     HasPtrMem() : d(new int(0))
 8     { 
 9         cout << "Construct: " <<   n_cstr << endl;
10     } 
11     HasPtrMem(const HasPtrMem & h) : d(new int(* h.d))
12     { 
13         cout << "Copy construct: " <<   n_cptr << endl;
14     }
15     HasPtrMem(HasPtrMem&& h) : d(h.d)   // 移动构造函数
16     {
17         h. d = nullptr;  // 将临时值的指针成员置空
18         cout << "Move construct: " <<   n_mvtr << endl;
19     }
20     ~ HasPtrMem()
21     { 
22         cout << "Destruct: " <<   n_dstr << endl;
23     } 
24     
25     int* d;
26     static int n_cstr; // 统计构造函数调用次数 
27     static int n_dstr; // 统计析构函数调用次数
28     static int n_cptr; // 统计拷贝构造函数调用次数
29     static int n_mvtr; // 统计移动构造函数调用次数
30 };
31 
32 int HasPtrMem::n_cstr = 0;
33 int HasPtrMem::n_dstr = 0;
34 int HasPtrMem::n_cptr = 0;
35 int HasPtrMem::n_mvtr = 0;
36 
37 HasPtrMem GetTemp()
38 {
39     HasPtrMem h;
40     cout << "Resource from " << __func__ << ": " << hex << h.d << endl;
41     return h;
42 } 
43 
44 int main()
45 { 
46     HasPtrMem a = GetTemp();
47     cout << "Resource from " << __func__ << ": " << hex << a.d << endl;
48 }
49 
50 /*
51 Construct: 1                     // 调用构造函数,构造对象h
52 Resource from GetTemp: 0x603010  // GetTemp函数中打印对象h的资源
53 Move construct: 1                // 由对象h,调用移动构造函数,构造临时对象
54 Destruct: 1                      // 析构掉对象h
55 Move construct: 2                // 由临时对象,调用移动构造函数,构造对象a
56 Destruct: 2                      // 析构掉临时对象
57 Resource from main: 0x603010     // main函数中打印对象a的资源[本质上来自于对象h的资源]
58 Destruct: 3                      // 析构掉对象a
59 */

如上。

good good study, day day up.

顺序 选择 循环 总结