首页 > 编程笔记

C++移动构造函数的定义和使用

C++11 标准提出右值引用主要的目的就是在函数调用中解决将亡值(临时对象)带来的效率问题。

【示例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;
}
运行结果:

构造函数
拷贝构造函数
析构函数
析构函数

示例分析:
在 func() 函数的调用过程中,func() 函数并不会直接将对象 a 返回出去,而是创建一个临时对象,将对象 a 的值赋给临时对象,在返回时,将临时对象返回给对象 b。

图1 func()函数的返回过程

图1 func()函数的返回过程

由图 1 可知,func() 函数在返回过程中经过了两次拷贝。函数调用结束后,对象 a 和临时对象都会被析构,这个过程就是重复的分配、释放内存。

注意,图 1 只显示了一次拷贝构造函数的调用,因为 VS2019 编译器(遵守 C++11 标准)对程序进行了优化,减少了临时对象的生成。

如果函数返回的数据是非常大的堆内存数据,那么频繁的拷贝、析构过程会严重影响程序的运行效率。针对这个问题,C++11 标准提出了右值引用的方法,在构造对象 b 时,直接通过右值引用的方式,让对象 b 引用临时对象,即对象 b 指向临时对象的内存空间。

返回右值引用的 func() 函数返回过程如图 2 所示。

图2 返回右值引用的func()函数返回过程

图2 返回右值引用的func()函数返回过程

由图 2 可知,使用右值引用时,对象 b 就指向了临时对象的内存空间,这块内存空间只有等到对象 b 被析构时才会被回收。右值引用实质上延长了临时对象的生命周期,减少了对象的拷贝、析构的次数,在实际项目开发中可以极大地提高程序运行效率。

要实现图 2 的构造过程,就需要定义相应的构造函数,这样的构造函数称为移动构造函数。移动构造函数也是特殊的成员函数,函数名称与类名相同,有一个右值引用作为参数。

移动构造函数定义格式如下所示:

class 类名
{
public:
移动构造函数名称(类名&& 对象名)
{
   函数体
}

};

在定义移动构造函数时,由于需要在函数内部修改参数对象,因此不使用 const 修饰引用的对象。

【示例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;
}
运行结果:

构造函数
移动构造函数
析构函数
析构函数

本例在类 A 中增加了一个成员变量 int*p,并且定义了移动构造函数。

本例第 22~27 行代码在类外实现移动构造函数,在函数内部,首先使用 a.p 给当前对象的指针 p 赋值,即将当前对象的指针 p 指向参数对象 a 的指针指向的内存空间,然后将 a.p 赋值为 nullptr。这样,一块内存空间只有一个指针是有效的,避免了同一块内存空间被析构两次。

由运行结果可知,程序调用了移动构造函数,而没有调用拷贝构造函数。在函数调用过程中,编译器会判断函数是否产生临时对象,如果产生临时对象,就会优先调用移动构造函数。若类中没有定义移动构造函数,编译器会调用拷贝构造函数。

与拷贝构造函数相比,移动构造函数是高效的,但它没有拷贝构造函数安全。例如,当程序抛出异常时,移动构造可能还未完成,这样可能会产生悬挂指针,导致程序崩溃。程序设计者在定义移动构造函数时,需要对类的资源有全面了解。

优秀文章