C++开发初级


869 浏览 5 years, 3 months

3.1.11 移动拷贝和移动赋值

版权声明: 转载请注明出处 http://www.codingsoho.com/
移动拷贝和移动赋值
右值和右值引用

什么是右值?不能取地址的都是右值,包括即将消失的变量,常量等;能取地址的都是左值

C++中的引用必须绑定到1个左值,无法定义1个常量、表达式的引用

void fun(int& a);
int b=2, c=4;
fun(b); // CORRECT
fun(b+c); // WRONG

函数参数修改为const int&后,可以传递参数, 但无法修改该参数。

下面是几个常见符号

  • & 左值引用
  • && 右值引用
  • *& 指针类型变量的引用
  • &* 错误,不会用指针指向引用,没有引用类型的指针

常见的左值右值的赋值方式

int & ref1 = 5; // WRONG, 5 is rvalue
int & ref1 = x; // CORRECT
const int & ref2 =5; // CORRECT
const int & ref2 =y; // CORRECT 
// const & 是万能引用,匹配左值和右值
int & ref3 = z;
ref3 = ref1; // 相当于 z=x 赋值,引用本身不能修改
move

举一个常见的swap的例子

常见的是实现方法如下:

swap(int a, int b)
{
    int tmp;
    tmp = a;
    a = b;
    b = tmp;
}
int x=1; y=2;
swap(x,y);
swap(1,2);

上面的代码执行效率较低,进行了多次内容赋值

当然,也可以通过指针来实现

swap(int* a, int* b)
{
    int tmp;
    tmp = *a;
    *a = *b;
    b = *tmp;
}
int x=1; y=2;
swap(&x,&y);

在前面的调用中swap(1,2)是没有意义的,两个常量不会交换,我们可以用下面两个函数进行左值和右值的区分

swap(const int& a, const int& b) // 接收右值
{
    int tmp;
    tmp = a;
    a = b;
    b = tmp;
}
int x=1; y=2;
swap(x,y); // CORRECT
swap(1,2); // WRONG, value changed in function
swap(int& a, int& b) // 接收左值
{
    int tmp;
    tmp = std.move(a);
    a = std.move(b);
    b = std.move(tmp);
}

通过move操作,减少了复制的过程

move的动机 - 复制的高昂代价

当对象非常庞大,如array数组类,复制构造1个数组的开销非差大。复制1个临时数组时,可以考虑使用移动的策略,将临时数组的“内部数据”直接移动到目标对象,而不是重新构建目标对象。 移动复制是一种破坏性复制,源对象的数据和状态被转移到目标对象,复制后,源对象不再有效。

假设有一个一维数组Array1D

#include <iostream>
#include<vector>

using namespace std;

class Array1D
{
    public:
        Array1D(int inSize);
        Array1D(const Array1D& stc);
        Array1D& operator=(const Array1D& rhs);
        ~Array1D();
    protected:
        int mSize;
        char* pData;
};

Array1D::Array1D(int inSize)
{
    static int count = 0;
    mSize = inSize;
    pData = new char[mSize];
    count++;
    cout << "Array1D(int inSize) was called " << count << " times!" << endl;
}

Array1D::~Array1D()
{
    delete [] pData;
    cout << "~Array1D() was called " << endl;
}

Array1D::Array1D(const Array1D& src)
{
    static int count = 0;
    mSize = src.mSize;
    pData = new char[mSize];
    memcpy(pData, src.pData, mSize);
    count++;
    cout << "Array1D(const Array1D& src) was called " << count << " times!" << endl;
}

Array1D& Array1D::operator=(const Array1D& rhs)
{
    static int count = 0;
    count++;
    cout << "Array1D::operator=(const Array1D& rhs) was called " << count << " times!" << endl; 
    if(this == &rhs) return *this;    
    delete [] pData ;
    // ...copy
    mSize = rhs.mSize;
    pData = new char[mSize];
    memcpy(pData, rhs.pData, mSize);
    return *this;
}

执行下面操作,ff()函数内构造Array1D对象通过temp返回,然后在s的复制构造函数中传给s. 注:temp的生命周期在Array1D s=ff();这句中仍然有效

Array1D ff()
{
    Array1D temp(100);
    return temp;
}
Array1D s=ff();

虽然随着ff的返回,temp已经没有用处, 但仍然执行了内存分配与释放过程。

实际运行时并没有2次分配和释放,应该是编译器做了优化

移动的目标:让s的pData直接指向temp原来的内存,“接管”其内部数据,temp中的pData不再有效。

class Array1D
{
    public:
        Array1D(int inSize);
        Array1D(const Array1D& src); //拷贝构造
        Array1D(Array1D&& src); //移动构造
        Array1D& operator=(const Array1D& rhs);  //拷贝赋值
        Array1D& operator=(Array1D&& rhs); //移动赋值               
        ~Array1D();
    protected:
        int mSize;
        char* pData;
};

如果要增加右值引用move语义,在原来正常拷贝构造 与拷贝赋值的基础上,增加移动构造和移动赋值方法。

编译器根据上下文环境,选择不同的函数。

Array1D::Array1D(Array1D&& src)
{
    static int count = 0;
    mSize = src.mSize;
    pData = src.pData;
    src.mSize = 0;
    src.pData = nullptr;
    count++;
    cout << "Array1D move constructor was called " << count << " times!" << endl;
}

移动时直接接管参数对象的数据,然后将其成员置空。

移动赋值运算符函数实现

Array1D& Array1D::operator=(Array1D&& lhs)
{
    static int count = 0;
    count++;
    cout << "Array1D move assignment operator was called " << count << " times!" << endl;   
    if(this == &lhs) return *this;    
    delete [] pData ;
    // ...move
    mSize = lhs.mSize;
    pData = lhs.pData;
    lhs.mSize=0;
    lhs.pData=nullptr;
    return *this;
}

需要编写移动构造函数的类,往往也需要提供移动赋值 运算符,实现赋值时的移动。

普通拷贝和移动的示例

Array1D ff()
{
    Array1D temp(100);
    return temp;
}

Array1D a(100);
Array1D b(a); // 普通拷贝构造,需要内存分配
b=ff(); // 赋值运算

给b赋值的对象为左值,temp数组内部的数据将被移动,赋值给对象b。

强制移动的示例

Array1D a(100);
Array1D b=move(a);

通过move操作,强制将a移动并移动构造对象b 移动后,a对象不再有效,其数据已经被移走。

完整代码及输出如下

int main(){
    Array1D a(100);     // Array1D constructor was called 1 times!
    Array1D b(a);       // Array1D copy constructor was called 1 times!
    b = ff();           // Array1D constructor was called 2 times!
                        // Array1D move assignment operator was called 1 times!  传递的是右值,所以调用移动赋值 
    //
    Array1D am(100);    // Array1D constructor was called 3 times! 
    Array1D bm=move(am);// Array1D move constructor was called 1 times!  temp数组内部的数据将被移动,赋值给对象b。
    b = move(bm);       // Array1D move assignment operator was called 2 times!
    return 0;
}
// ~Array1D() destructor was called
// ~Array1D() destructor was called
// ~Array1D() destructor was called
// ~Array1D() destructor was called

代码取自 Move\Move.cpp