C++移动构造函数的定义和使用
C++11 标准提出右值引用主要的目的就是在函数调用中解决将亡值(临时对象)带来的效率问题。
【示例1】下面通过案例演示传统 C++ 程序对函数返回值的处理,C++ 代码如下:
在 func() 函数的调用过程中,func() 函数并不会直接将对象 a 返回出去,而是创建一个临时对象,将对象 a 的值赋给临时对象,在返回时,将临时对象返回给对象 b。
图1 func()函数的返回过程
由图 1 可知,func() 函数在返回过程中经过了两次拷贝。函数调用结束后,对象 a 和临时对象都会被析构,这个过程就是重复的分配、释放内存。
注意,图 1 只显示了一次拷贝构造函数的调用,因为 VS2019 编译器(遵守 C++11 标准)对程序进行了优化,减少了临时对象的生成。
如果函数返回的数据是非常大的堆内存数据,那么频繁的拷贝、析构过程会严重影响程序的运行效率。针对这个问题,C++11 标准提出了右值引用的方法,在构造对象 b 时,直接通过右值引用的方式,让对象 b 引用临时对象,即对象 b 指向临时对象的内存空间。
返回右值引用的 func() 函数返回过程如图 2 所示。
图2 返回右值引用的func()函数返回过程
由图 2 可知,使用右值引用时,对象 b 就指向了临时对象的内存空间,这块内存空间只有等到对象 b 被析构时才会被回收。右值引用实质上延长了临时对象的生命周期,减少了对象的拷贝、析构的次数,在实际项目开发中可以极大地提高程序运行效率。
要实现图 2 的构造过程,就需要定义相应的构造函数,这样的构造函数称为移动构造函数。移动构造函数也是特殊的成员函数,函数名称与类名相同,有一个右值引用作为参数。
移动构造函数定义格式如下所示:
【示例2】下面通过修改【示例1】演示移动构造函数的定义与调用,C++ 代码如下:
本例第 22~27 行代码在类外实现移动构造函数,在函数内部,首先使用 a.p 给当前对象的指针 p 赋值,即将当前对象的指针 p 指向参数对象 a 的指针指向的内存空间,然后将 a.p 赋值为 nullptr。这样,一块内存空间只有一个指针是有效的,避免了同一块内存空间被析构两次。
由运行结果可知,程序调用了移动构造函数,而没有调用拷贝构造函数。在函数调用过程中,编译器会判断函数是否产生临时对象,如果产生临时对象,就会优先调用移动构造函数。若类中没有定义移动构造函数,编译器会调用拷贝构造函数。
与拷贝构造函数相比,移动构造函数是高效的,但它没有拷贝构造函数安全。例如,当程序抛出异常时,移动构造可能还未完成,这样可能会产生悬挂指针,导致程序崩溃。程序设计者在定义移动构造函数时,需要对类的资源有全面了解。
【示例1】下面通过案例演示传统 C++ 程序对函数返回值的处理,C++ 代码如下:
#include<iostream> using namespace std; class A //定义类A { public: A(){ cout << "构造函数" << endl; } A(const A& a) { cout << "拷贝构造函数" << endl; } ~A(){ cout << "析构函数" << endl; } }; A func() //定义func()函数 { A a; //创建对象a return a; //返回对象a } int main() { A b = func(); //调用func()函数 return 0; }运行结果:
构造函数
拷贝构造函数
析构函数
析构函数
- 第 3~9 行代码定义了类 A,该类中定义了构造函数、拷贝构造函数和析构函数;
- 第 10~14 行代码定义了 func() 函数,在函数内部创建对象 a,并将对象 a 返回;
- 第 17 行代码调用 func() 函数构造对象 b。
在 func() 函数的调用过程中,func() 函数并不会直接将对象 a 返回出去,而是创建一个临时对象,将对象 a 的值赋给临时对象,在返回时,将临时对象返回给对象 b。
图1 func()函数的返回过程
由图 1 可知,func() 函数在返回过程中经过了两次拷贝。函数调用结束后,对象 a 和临时对象都会被析构,这个过程就是重复的分配、释放内存。
注意,图 1 只显示了一次拷贝构造函数的调用,因为 VS2019 编译器(遵守 C++11 标准)对程序进行了优化,减少了临时对象的生成。
如果函数返回的数据是非常大的堆内存数据,那么频繁的拷贝、析构过程会严重影响程序的运行效率。针对这个问题,C++11 标准提出了右值引用的方法,在构造对象 b 时,直接通过右值引用的方式,让对象 b 引用临时对象,即对象 b 指向临时对象的内存空间。
返回右值引用的 func() 函数返回过程如图 2 所示。
图2 返回右值引用的func()函数返回过程
由图 2 可知,使用右值引用时,对象 b 就指向了临时对象的内存空间,这块内存空间只有等到对象 b 被析构时才会被回收。右值引用实质上延长了临时对象的生命周期,减少了对象的拷贝、析构的次数,在实际项目开发中可以极大地提高程序运行效率。
要实现图 2 的构造过程,就需要定义相应的构造函数,这样的构造函数称为移动构造函数。移动构造函数也是特殊的成员函数,函数名称与类名相同,有一个右值引用作为参数。
移动构造函数定义格式如下所示:
class 类名
{
public:
移动构造函数名称(类名&& 对象名)
{
函数体
}
…
};
【示例2】下面通过修改【示例1】演示移动构造函数的定义与调用,C++ 代码如下:
#include<iostream> using namespace std; class A { public: A(int n); //构造函数 A(const A& a); //拷贝构造函数 A(A&& a); //移动构造函数 ~A(); //析构函数 private: int* p; //成员变量 }; A::A(int n):p(new int(n)) { cout << "构造函数" << endl; } A::A(const A& a) { p = new int(*(a.p)); cout << "拷贝构造函数" << endl; } A::A(A&& a) //类外实现移动构造函数 { p = a.p; //将当前对象指针指向a.p指向的空间 a.p = nullptr; //将a.p赋值为nullptr cout << "移动构造函数" << endl; } A::~A() { cout << "析构函数" << endl; } A func() { A a(10); return a; } int main() { A m = func(); return 0; }运行结果:
构造函数
移动构造函数
析构函数
析构函数
本例第 22~27 行代码在类外实现移动构造函数,在函数内部,首先使用 a.p 给当前对象的指针 p 赋值,即将当前对象的指针 p 指向参数对象 a 的指针指向的内存空间,然后将 a.p 赋值为 nullptr。这样,一块内存空间只有一个指针是有效的,避免了同一块内存空间被析构两次。
由运行结果可知,程序调用了移动构造函数,而没有调用拷贝构造函数。在函数调用过程中,编译器会判断函数是否产生临时对象,如果产生临时对象,就会优先调用移动构造函数。若类中没有定义移动构造函数,编译器会调用拷贝构造函数。
与拷贝构造函数相比,移动构造函数是高效的,但它没有拷贝构造函数安全。例如,当程序抛出异常时,移动构造可能还未完成,这样可能会产生悬挂指针,导致程序崩溃。程序设计者在定义移动构造函数时,需要对类的资源有全面了解。