拷贝构造vs移动构造

前言

我们可能不知道copy这个操作是多么容易发生,如下代码,你敢相信创建了2个临时变量,一个临时变量是为a+b所创建,还有一个临时变量是为右值创建,最后这个右值的临时变量copy给左边的x1,copy完成后临时变量就会被销毁,所以用指针的方法要比较好,特别当我们的这些临时对象特别大的时候,copy和destroy开销有点大

 x=3*(a+b)

简介

拷贝构造

The copy constructors in C++ work with the l-value references and copy semantics(copy semantics means copying the actual data of the object to another object rather than making another object to point the already existing object in the heap). While move constructors work on the r-value references and move semantics(move semantics involves pointing to the already existing object in the memory).
On declaring the new object and assigning it with the r-value, firstly a temporary object is created, and then that temporary object is used to assign the values to the object. Due to this the copy constructor is called several times and increases the overhead and decreases the computational power of the code. To avoid this overhead and make the code more efficient we use move constructors.

看下面示例代码

#include 

using namespace std;
class A{
public:
    A(const A& a){ //copy constructor
        classid = a.classid + 1;
        cout <<"copy construct from class" << a.classid << "\n";
        
    }
       
    A(int a)
    :classid(a)
    {
        cout << "class " << a << " normal construct " << "\n";
    }
    
private:
    int classid;
    
    
};

int main()
{
    A a(1);
    A b = a;  //引发拷贝构造
    return 0;
}

什么时候会引发拷贝构造?

  1. 将一个obj赋值给另一个新obj对象,这2个obj都是一个class实例化后得到的
  2. 我们一个函数把obj当成返回值的时候(接收返回值的函数会创建一个临时obj变量接收返回值,此时引发拷贝构造)
  3. 当一个obj对象当作参数传入函数的时候(调用函数会创建一个临时class变量接收传进来的obj,此时引发拷贝构造)

移动构造

Move constructor moves the resources in the heap, i.e., unlike copy constructors which copy the data of the existing object and assigning it to the new object move constructor just makes the pointer of the declared object to point to the data of temporary object and nulls out the pointer of the temporary objects. Thus, move constructor prevents unnecessarily copying data in the memory.
Work of move constructor looks a bit like default member-wise copy constructor but in this case, it nulls out the pointer of the temporary object preventing more than one object to point to same memory location.

左值引用vs右值引用
个人认为i左值和右值的本质区别是=号左右之分,等号右边一般是常量,所以我们右值引用的对象是常量,常量不能更改一定要记住,左值引用和右值引用的字面区别是&,和&&之分,比如左值引用int i =20; int l& = i,右值引用为int && r = 200,不能用左值取初始化右值,比如int x = 20;//左值,int && r = x //error!!!

移动构造是c++11引进的就是为了解决我们前言提到的问题,因为一个class对象可能非常大,copy和销毁的开销可能很大1

#include 

using namespace std;
class A{
public:
    A(const A& a){ //copy constructor
        classid = a.classid + 1;
        cout <<"copy construct from class" << a.classid << "\n";
        
    }
       
     A(const A&& a){ //move constructor
        classid = a.classid + 1;
        cout <<"move construct from class" << a.classid << "\n";
        
    }   
       
    A(int a)
    :classid(a)
    {
        cout << "class " << a << " normal construct " << "\n";
    }
    
private:
    int classid;
    
    
};

int main()
{
    A a(1); //构建一个左值class变量 a
    
    A b = std::move(a);  //将a从左值变成右值,并且传入b,发生移动构造
    return 0;
}

直接传值A b = A(1);是不能引发移动构造的,因为右边本来是常量然后赋值给一个临时的变量,这个临时变量就成为了左值

但是!!!这样写也可以加一个函数返回class A

A 
returnclass(A a){
    return a;
}
///
///
///
A c = returnclass(A(2));//同样会引发移动构造

为啥我们的A(2)直接赋值给c因为中间有一个临时变量会导致不是右值,但是过一道函数就变成右值了呢?因为我们知道右值是没有名字的,通常右值是作为函数调用或其他操作的结果而存在于栈中的临时对象。从函数返回一个值会将这个值变成一个右值。一旦你对一个类对象调用 return(函数中创建的类对象) ,这个对象的名字就不再存在(它超出了范围),所以它变成了一个右值。2

总结:拷贝构造就是copy,移动构造是指针指向已经存在的对象,copy构造在构造函数之前会先新建一个临时class obj(在进入构造函数之后,就像我们进入函数之后会将实参赋值给形参),再由这个外部的obj将值赋给新建的class obj内部的这个临时obj后任务就结束了,后续的操作都是这个临时的obj和新的class obj之间的,最后在copy完成后就将这个临时obj析构,这个是非常耗时间,耗内存的,

例子1

看下面代码

#include 
#include 

using namespace std;

class Move{
private:
    int* data;
    
public:
    //构造函数
    Move(int d){
        data = new int; //new后返回的是指针
        *data = d;
        cout << "constructor is called for " << d << "\n";
    }
    //拷贝构造,传入source指针,这里也就是右值引用
    Move(const Move& source)
        : Move{ *source.data }{
          cout << "copy constructor is called " << "deep copy for "<< *source.data << "\n";  
        }
    
    //移动构造,传入相同class的常量,这里也是右值引用
    Move(Move&& source)
        : data{ source.data }{
            cout << "Move Constructor for " << *source.data << endl;
            source.data = nullptr; //安全起见
        }
        
    //析构函数
    ~Move(){
        if (data != nullptr)
 
            // If pointer is not pointing
            // to nullptr
            cout << "Destructor is called for "
                 << *data << endl;
        else
 
            // If pointer is pointing
            // to nullptr
            cout << "Destructor is called"
                 << " for nullptr "
                 << endl;
 
        // Free up the memory assigned to
        // The data member of the object
        delete data;
    }
    
};

int main()
{
    vector<Move> vec;
    
    vec.push_back(Move{10});
    vec.push_back(Move{20});

    return 0;
}

我们先只运行vec.push_back(move{10})看打印如下

constructor is called for 10
Move Constructor for 10
Destructor is called for nullptr 
Destructor is called for 10

从上面可以看到我们一个简单的push到vector的操作一共构建了2次,第一次是move{10}构建常量,第二次是在容器内先构造一个临时class(push_back()特点)再将其传进来就像这样Move v1(move{10});,所以会调用拷贝构造,然后将这个临时的class传入vector中(此时vector已经构建好空间),然后析构这个临时移动构造的class,最后程序结束析构这个常量

记住move{10}是常量,我们再传入左值发现会调用copy构造,将main函数的代码改成这样vector vec; Move v1(10) ; vec.push_back(v1);然后我们可以看到输出

constructor is called for 10
constructor is called for 10
copt constructor is called deep copy for 10
Destructor is called for 10
Destructor is called for 10

最后vec.push_back(Move{10});vec.push_back(Move{20});都加上打印如下

constructor is called for 10
Move Constructor for 10
Destructor is called for nullptr 
constructor is called for 20
Move Constructor for 20
constructor is called for 10
copt constructor is called deep copy for 10
Destructor is called for 10
Destructor is called for nullptr 
Destructor is called for 10
Destructor is called for 20

第一个会先调用构造函数构造常量,再调用拷贝构造,因为传入的是常量

例子2

我们再看一个例子

#include 
#include 

using namespace std;

class A{

public:
    A() { cout << "A constructor "<< "\n"; }  //普通构造函数
    A(const A& rhs ) { cout << "A copy constructor" << "\n"; }  //copy construct
};

int main()
{
    vector<A> v; //来一个vector
    
    cout << "vector v push_back 1st element" <<"\n";
    v.push_back(A());
    
    cout << "vector v push_back 2st element" << "\n";
    v.push_back(A());
    
    cout << "vector v push_back 3st element" << "\n";
    v.push_back(A());
    
    cout << "vector v push_back 4st element" << "\n";
    v.push_back(A());

    return 0;
}

我们看一下结果

vector v push_back 1st element
A constructor 
A copy constructor
vector v push_back 2st element
A constructor 
A copy constructor
A copy constructor
vector v push_back 3st element
A constructor 
A copy constructor
A copy constructor
A copy constructor
vector v push_back 4st element
A constructor 
A copy constructor

看上述的结构是不是发现拷贝构造发生了很多次,如我们前言所说是不是对效率有些不友好
我们来尝试解释一下这个结果,首先第一个push,我们先创建一个临时的class,所以会用到构造函数,所以有了第一个打印,然后我们是不是要将这个临时的变量copy到我们的vector里面(相当于BACK_ELEMENT_OF_VECTOR = TEMPORARY_CLASS),此时引发了第二条打印,如果按照这样的节奏我们第二次的push也应该打印1次copy constructor,但是他打印了2次,这个是vector的锅,因为第一次我们的vector的容量为1第一次push完后vector的容量满了,第二次push,vector发现自己容量不够会自己扩容,扩容的程序是先申请2个class A类型vector的空间,然后将老vector的class copy过去,这时候触发了一次copy constructor,然后再push新的class触发了第二次copy constructor,第三次打印了三次copy constructor也是这个原因,为什么第四次push只打印了一次copy construct,因为第二次的扩容把容量从2个class的vector扩容到4个class的vector,我们可以加上v.capacity()看容量

#include 
#include 

using namespace std;

class A{

public:
    A() { cout << "A constructor "<< "\n"; }  //普通构造函数
    A(const A& rhs ) { cout << "A copy constructor" << "\n"; }  //copy construct
};

int main()
{
    vector<A> v; //来一个vector
    
    cout << "befor push_back 1st element the capacity of vector is "<< (int)v.capacity() <<"\n";
    cout << "vector v push_back 1st element" <<"\n";
    v.push_back(A());
    
    cout << "befor push_back 2st element the capacity of vector is "<< (int)v.capacity() <<"\n";
    cout << "vector v push_back 2st element" << "\n";
    v.push_back(A());
    
    cout << "befor push_back 3st element the capacity of vector is "<< (int)v.capacity() <<"\n";
    cout << "vector v push_back 3st element" << "\n";
    v.push_back(A());
    
    cout << "befor push_back 4st element the capacity of vector is "<< (int)v.capacity() <<"\n";
    cout << "vector v push_back 4st element" << "\n";
    v.push_back(A());

    return 0;
}

再看打印

befor push_back 1st element the capacity of vector is 0
vector v push_back 1st element
A constructor 
A copy constructor
befor push_back 2st element the capacity of vector is 1
vector v push_back 2st element
A constructor 
A copy constructor
A copy constructor
befor push_back 3st element the capacity of vector is 2
vector v push_back 3st element
A constructor 
A copy constructor
A copy constructor
A copy constructor
befor push_back 4st element the capacity of vector is 4
vector v push_back 4st element
A constructor 
A copy constructor

一切都变得make sense

我们再加上移动构造函数看会发生什么

#include 
#include 

using namespace std;

class A{

public:
    A() { cout << "A constructor "<< "\n"; }  //普通构造函数
    A(const A& rhs ) { cout << "A copy constructor" << "\n"; }  //copy constructor
    A(A&& rhs) { cout << "A move constructor" << "\n"; } //move constructor
};

int main()
{
    vector<A> v; //来一个vector
    
    cout << "befor push_back 1st element the capacity of vector is "<< (int)v.capacity() <<"\n";
    cout << "vector v push_back 1st element" <<"\n";
    v.push_back(A());
    
    cout << "befor push_back 2st element the capacity of vector is "<< (int)v.capacity() <<"\n";
    cout << "vector v push_back 2st element" << "\n";
    v.push_back(A());
    
    cout << "befor push_back 3st element the capacity of vector is "<< (int)v.capacity() <<"\n";
    cout << "vector v push_back 3st element" << "\n";
    v.push_back(A());
    
    cout << "befor push_back 4st element the capacity of vector is "<< (int)v.capacity() <<"\n";
    cout << "vector v push_back 4st element" << "\n";
    v.push_back(A());

    return 0;
}

结果如下

befor push_back 1st element the capacity of vector is 0
vector v push_back 1st element
A constructor 
A move constructor
befor push_back 2st element the capacity of vector is 1
vector v push_back 2st element
A constructor 
A move constructor
A copy constructor
befor push_back 3st element the capacity of vector is 2
vector v push_back 3st element
A constructor 
A move constructor
A copy constructor
A copy constructor
befor push_back 4st element the capacity of vector is 4
vector v push_back 4st element
A constructor 
A move constructor

我们的每一次临时class进入vector都会触发移动构造,但是当我们的vector的空间不够了,他会copy老的vector中的元素进入新的vector中,所以我们还是可以看到有copy构造的存在,但是每一次新临时class进vector已经变成移动构造了
我们怎么解决这个vector因为空间不够自动copy发生的多个copy constructor问题呢?我们只需要在移动构造的后面加上一个关键字noexcept就像这样

A(A&& rhs) noexcept { cout << "A move constructor" << "\n"; } //move constructor

  1. http://www-h.eng.cam.ac.uk/help/tpl/languages/C++/morevectormemory.html ↩︎ ↩︎

  2. https://www.chromium.org/rvalue-references/ ↩︎

你可能感兴趣的:(c/c++,c++)