拷贝构造函数与移动构造函数

一、拷贝构造函数

当类没有定义拷贝构造函数的时候,编译器会默认提供一个,这个拷贝函数是浅拷贝

如果该类中含有指针,可能会发生内存泄漏,见下面的例子:

class Test
{
 public:
 int *p;
  Test(){ p=new int; };
  ~Test(){ delete p; };
};
void main()
{
    Test t1;
    Test t2(t1);
    Test t3 = t1;
}

t1、t2、t3的成员变量p指向的是同一块内存,程序结束后会出现重复释放的问题。

为了解决这个问题,可以自定义拷贝构造函数:

class Test
{
 public:
 int *p;
 Test(const Test &t)
 {
     p = new int (*(t.p));
 }
  Test(){ p=new int; };
  ~Test(){ delete p; };
};

二、右值引用

除了上述的解决方法,还可以使用C++11的【右值引用】新特性来解决,而且可以提高程序的性能,减少内存开销。

为了引出左值引用的概念,先来复习左值和右值

1.左值和右值

int a = 3 + 4 ;

上面的式子中,变量 a 就是左值,右边的表达式会生成一个临时变量存放 (3+4) 的值,这个变量称之为右值。

有两种方式可以判断:

(1)只能放在等号(=)右侧的即为右值,可以放在左侧的为左值

int a = 10 ;
10 = a ; //错误

(2)左值可以取地址,而右值不允许:

int a = 3 + 4 ;
int * b = & a ;  //ok
b = & (3+4) ; //错误

2.右值引用

 使用方法如下,b就是对右值 (3+4) 的引用。

int && b = 3 + 4 ;

先看下下面的左值引用:

int a = 0 ;
int &b = 4 ; //错误!
int &b = a ; //左值引用

如上例所示,左值引用只能对左值进行别名引用,无法引用右值

于是C++11增加了右值引用,使用 && 表示(和逻辑运算中的”且“一致)。

int a = 0 ;
int b = 1 ;
int && c = a+c ; //右值引用
int && c = 3 ; //右值引用
int && c = 3 +4 ; //右值引用
int && c = a ; //错误!

注意不能直接右值引用左值,C++提供了一个函数std::move()函数,可以将左值变成右值:

string str1 = "aa" ;
string && str2 = std::move( str1 );  //ok

3.右值引用的应用场景

(1)案例:

还是回到之前的例子:

class Test
{
 public:
 int *p;
 Test(const Test &t)
 {
     p = new int (*(t.p)); cout<<"copy construct"<<endl;
 }
  Test(){ p=new int; cout<<"construct"<<endl; };
  ~Test(){ delete p; cout<<"destruct"<<endl; };
};

Test getTest()
{
    return Test();
}

void main()
{
    {
        Test t = getTest();
    }
}

使用vs2012运行,结果为:

construct                 //执行 Test()
destruct                  //销毁 t

 但需要注意的是,这是vs编译器对拷贝构造函数优化后的结果。禁止优化,结果为:

construct                 //执行 Test()
copy construct            //执行 return Test()
destruct                  //销毁 Test() 产生的匿名对象
copy construct            //执行 t = getTest()
destruct                  //销毁 getTest() 返回的临时对象
destruct                  //销毁 t

可以看到,进行了两次的深拷贝,对于对内存要求不高、本例这种占内存比较小的类Test而言(申请的堆空间小),可以接受。

但如果临时对象中的指针成员申请了大量的堆空间,那将严重影响程序的执行效率。

C++11为了解决这一问题(深拷贝占用大量空间),引入移动构造函数

 (2)移动构造函数

所谓的移动,就是将其他的内存资源,“移为己有”,这些资源通常是临时对象,比如上文所叙的右值

修改如下(增加一个移动构造函数):

class Test
{
 public:
 int *p;
 Test(Test &&t) //移动构造函数
 {
     p = t.p;
     t.p = nullptr;//将临时对象的指针赋值为空
     cout<<"copy construct"<<endl;
 }
 Test(const Test &t) //拷贝构造函数
 {
     p = new int (*(t.p));
     cout<<"move construct"<<endl;
 }
  Test(){ p=new int; cout<<"construct"<<endl; };
  ~Test(){ delete p; cout<<"disconstruct"<<endl; };
};
Test getTest()
{
    return Test();
}
void main()
{
    {
        Test t = getTest();
    }
}

禁止vs优化,结果为:

construct                 //执行 Test()
move construct            //执行 return Test()
destruct                  //销毁 Test() 产生的匿名对象
move construct            //执行 t = getTest()
destruct                  //销毁 getTest() 返回的临时对象
destruct                  //销毁 t

可以看到,定义了移动构造函数后,临时对象的创建使用移动构造函数创建,如下,没有在堆上创建对象,减少了开销。

 Test(Test &&t) //移动构造函数
 {
     p = t.p;
     t.p = nullptr;//将临时对象的指针赋值为空
     cout<<"copy construct"<<endl;
 }

那么问题来了,什么时候调用移动构造函数,什么时候调用拷贝构造函数呢?将在后面的文章中分析。

原文地址:https://www.cnblogs.com/hosseini/p/15089358.html