对象移动:
就是把一个不想用了的对象A中的一些有用的数据提取出来,在构建新对象B的时候就不需要重新构建对象中的所有数据--从不想用了的对象A中提取出来的有用数据在构建对象B时都可以拿来使用。
移动构造函数和移动赋值运算符都是C++11中引进的新概念,这些概念都是用来解决和提高程序执行效率的问题。
移动构造函数和移动赋值运算符的概念,与拷贝构造函数和拷贝赋值运算符类似。有两点需要说明:
(1)如果复制数据,如要把对象A复制给对象B,那对象A里面的数据还能使用,但是如果把对象A(实际上是对象A中部分数据)移动给对象B(对象A的数据就会出现残缺),那显然对象A就不能再被使用,否则因为数据残缺可能导致出现问题。
(2)这里移动的概念并不是把内存中的数据从一个地址倒腾到另外一个地址,因为倒腾数据这个动作量很大(跟复制没有区别),影响效率。而是把一块内存地址中的数据的所有者从原来的所有者标记为新所有者,如原来这块数据的所有数据的所有者是对象A,经过“移动”后,这块数据的所有者就变成对象B了,此时对象A就变得残缺了,原则上就不要再去使用对象A了。
显然,移动可以使程序效率提高,比复制效率高得多,如果源对象A不再使用,那么,直接把源对象A中的某些new出来的数据移动给目标对象B,那就相当于数据还是那一堆数据,只是属主换了另外一个人。
移动构造函数与拷贝构造函数很类似,拷贝构造函数的写法如下:
Time::time(const Time& myTime){......}
在这个拷贝构造函数中,形参是一个const引用,也是一个左值引用带一个“&”。在移动构造函数中,这个形参是一个右值引用而不是左值引用,也就是带两个“&”(&&)的引用。右值引用是为了支持对象移动的操作的。
拷贝构造函数形参是带一个“&”的引用;移动构造函数,形参是带两个“&&”的引用,也就是右值引用。
C++里面这样规定:移动构造函数的第一个参数就是一个右值引用参数(实参就必须传递进来一个右值,因为右值引用形参正是要绑定右值的,所以,右值作为实参),C++就是根据传递进来的是否是一个右值实参来确定是不是要调用移动构造函数或者是不是要调用移动赋值运算符。
在移动构造函数/移动赋值运算符中,传递进来的这第一个参数(右值),换句话说就是对象A,那么读者就会明白很多资料里面所说的,右值引用主要用来绑定到那些即将销毁/一些临时的对象上,这里的对象A就是程序员不想在后续再使用的对象,也就是这个即将销毁的或者说临时的对象。
移动构造函数除了第一个参数是右值引用之外,如果有其它额外的参数,这些额外的参数都要有默认值,这一点和拷贝构造函数完全相同。
移动构造函数和移动赋值运算符都是函数,函数体代码需要由程序员自己来写。这些代码应该规范地完成移动构造函数和移动赋值运算符本来应该完成的使命功能。
所以写移动构造函数或者移动赋值运算符时,要实现一些主要的功能:
(1)要完成资源的移动(对于对象A中要移动给对象B的内存,让对象B指向这块内存,斩断对象A和这段内存的关系,防止后续对象A误操作这块内存,这块内存已经不再属于对象A了)。
(2)确保移动后源对象处于一种“即便被销毁也没有什么问题”的状态,这个就是上面所说的对象A。在程序代码中要自动自觉地确保执行完移动构造函数后,再不使用对象A,因为对象A的数据有一部分已经转移给了对象B,本身已经残缺。
1、如果没有移动构造函数,编译器会调用拷贝构造函数,如果程序里面有移动构造函数,则编译器会调用移动构造函数。
2、当类中拥有指针类型的成员变量时,拷贝构造函数中需要以深拷贝(而非浅拷贝)的方式复制该指针成员。
(1)在moveconst.cpp文件下编写深度拷贝调用拷贝构造函数例子如下(以下程序是在linux环境下实现和编译运行):
- class student
- {
- public:
- //构造函数
- student() :num(new int(10001))
- {
- //cout << *num << endl;
- cout << "执行构造函数!" << endl;
- }
-
- //拷贝构造函数
- student(const student& stu) :num(new int(*stu.num))
- {
- cout << "执行拷贝构造函数!" << endl;
- }
-
- //移动构造函数
- student(student&&stu):num(stu.num)
- {
- stu.num = nullptr;
- cout << "执行移动构造函数!" << endl;
- }
-
- //析构函数
- ~student()
- {
- delete num;
- cout << "执行析构函数!" << endl;
- }
- private:
- int* num;
- };
-
- //成员函数
- student get_student()
- {
- return student();
- }
-
-
- int main()
- {
- student stu = get_student(); //等价:student a = student();
- return 0;
- }
在 Linux 上使用g++ moveconst.cpp -fno-elide-constructors运行结果如下:

注意:-fno-elide-constructors是禁止编译器对c++11右值引用程序优化。
可以看到,
程序进行了2次深度拷贝,调用了2次拷贝构造函数。
经过调试发现,程序在运行时执行的步骤如下:
//执行构造函数!< --执行 student()
//执行拷贝构造函数!< --执行 return student()
//执行析构函数!< --销毁 student() 产生的匿名对象
//执行拷贝构造函数!< --执行 stu = get_student()
//执行析构函数!< --销毁 get_student() 返回的临时对象
//执行析构函数!< --销毁 stu
利用拷贝构造函数实现对 a 对象的初始化,底层实际上进行了 2 次拷贝(而且是深拷贝)操作。当然,对于仅申请少量堆空间的临时对象来说,深拷贝的执行效率依旧可以接受,但如果临时对象中的指针成员申请了大量的堆空间,那么 2 次深拷贝操作势必会影响 a 对象初始化的执行效率。
对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这会大大提高了初始化的执行效率。
(2)我们在上面的程序中添加移动构造函数,如下:
- class student
- {
- public:
- //构造函数
- student() :num(new int(10001))
- {
- //cout << *num << endl;
- cout << "执行构造函数!" << endl;
- }
-
- //拷贝构造函数
- student(const student& stu) :num(new int(*stu.num))
- {
- cout << "执行拷贝构造函数!" << endl;
- }
-
- //移动构造函数
- student(student&&stu):num(stu.num)
- {
- stu.num = nullptr;
- cout << "执行移动构造函数!" << endl;
- }
-
- //析构函数
- ~student()
- {
- delete num;
- cout << "执行析构函数!" << endl;
- }
- private:
- int* num;
- };
-
- //成员函数
- student get_student()
- {
- return student();
- }
-
- int main()
- {
- student stu = get_student(); //等价:student a = student();
- return 0;
- }
运行结果如下:

可以看到,
在之前 student类的基础上,我们又手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 stu.num,有效避免了“同一块对空间被释放多次”情况的发生。
经过调试发现,程序运行按如下步骤执行:
//执行构造函数!< --执行 student()
//执行拷贝构造函数!< --执行 return student()
//执行析构函数!< --销毁 student() 产生的匿名对象
//执行拷贝构造函数!< --执行 stu = get_student()
//执行析构函数!< --销毁 get_student() 返回的临时对象
//执行析构函数!< --销毁 stu
当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
在student.h头文件中这样声明:
- #ifndef __STUDENT_H__
- #define __STUDENT_H__
-
- #include
- #include
- #include
-
- using namespace std;
-
- class student
- {
- public:
- //构造函数
- student();
-
- //拷贝构造函数
- student(const student& stu);
-
- //移动构造函数
- student(student&& stu)noexcept;
-
- //赋值运算符
- student operator=(student& stu);
-
- //移动赋值运算符
- student operator=(student&& stu)noexcept;
-
- //析构函数
- virtual ~student();
- private:
- int* num;
- };
-
- #endif // !__STUDENT_H__
在student.cpp文件中这样定义:
- #include"student.h"
-
- using namespace std;
-
- //构造函数
- student::student():num(new int(10001))
- {
- //cout << *num << endl;
- cout << "执行构造函数!" << endl;
- }
-
- //拷贝构造函数
- student::student(const student& stu) :num(new int(*stu.num))
- {
- cout << "执行拷贝构造函数!" << endl;
- }
-
- //移动构造函数
- student::student(student&& stu) noexcept :num(stu.num)
- {
- stu.num = nullptr;
- cout << "执行移动构造函数!" << endl;
- }
-
- //赋值运算符
- student student::operator=(student& stu)
- {
- cout << "执行赋值运算符!" << endl;
- num = stu.num;
- stu.num = nullptr;
- return *this;
- }
-
- //移动赋值运算符
- student student::operator=(student&& stu)noexcept
- {
- cout << "执行移动赋值运算符!" << endl;
- num = stu.num;
- stu.num = nullptr;
- return *this;
- }
-
- //析构函数
- student::~student()
- {
- delete num;
- cout << "执行析构函数!" << endl;
- }
-
- //成员函数
- student get_student()
- {
- return student();
- }
-
- int main()
- {
- cout << "stu1::start" << endl;
- student stu1 = get_student();
- cout << "stu1::end" << endl;
-
- cout << "stu2::start" << endl;
- student stu2;
- cout << "stu2::state" << endl;
- stu2 = std::move(stu1); //这里这样写才能调用移动赋值运算符,stu2=stu1;调用赋值运算符。不能写成student stu2 = stu1;,否则不会调用赋值运算符或移动赋值运算符。
- cout << "stu2::end" << endl;
-
- return 0;
- }
运行结果如下所示:

经过调试发现程序按如下步骤执行:
stu1::start
执行构造函数! //执行student()
执行移动构造函数! //执行 return student()
执行析构函数! //销毁 student() 产生的匿名对象
执行移动构造函数! //执行 stu1 = get_student()
执行析构函数! //销毁 get_student() 返回的临时对象
stu1::end
stu2::start
执行构造函数! //执行student stu2;
stu2::state
执行移动赋值运算符! //执行move(stu1)
执行拷贝构造函数! //执行stu2 = std::move(stu1);
执行析构函数! //销毁std::move(stu1)返回的临时对象
stu2::end
执行析构函数! //-销毁 stu2
执行析构函数! //-销毁 stu1
1、在有必要的情况下,应该考虑尽量给类添加移动构造函数和移动赋值运算符,达到减少拷贝构造函数和拷贝赋值运算符调用的目的,尤其是需要频繁调用拷贝构造函数和拷贝赋值运算符的场合。一般来讲,只有使用new分配了大量内存的这种类才比较需要移动构造函数和移动赋值运算符。
2、不抛异常的移动构造函数,移动赋值运算符都应该加上noexcept,用于通知编译器该函数本身不抛出异常。否则有可能因为系统内部的一些动作机制原本程序员认为可能会调用移动构造函数的地方却调用了拷贝构造函数,此外,此举还可以提高编译器的工作效率。
3、一个对象移动完数据后当然不会自动销毁,但是,程序员有责任使这种数据被移走的对象处于一种可以被释放(析构)的状态。
4、一个本该由系统调用移动构造函数和移动赋值运算符的地方,如果类中没有提供移动构造函数和移动赋值运算符,则系统会调用拷贝构造函数和拷贝赋值运算符代替。
2022.07.27结。