乐趣区

关于c++:C的移动构造函数和移动赋值运算符

什么是挪动结构

在 C++ 11 规范之前(C++ 98/03 规范中),如果想用其它对象初始化一个同类的新对象,只能借助类中的复制(拷贝)构造函数。在 C ++11 中,引入了右值援用,提供了左值转右值的办法,防止了对象潜在的拷贝。而挪动构造函数和挪动赋值运算符也是通过右值的属性来实现的。直观的来讲,挪动结构就是将对象的状态或者所有权从一个对象转移到另一个对象。只是转移,没有内存的搬迁或者内存拷贝所以能够进步利用效率,改善性能。

右值和左值

CPU 视角的右值和左值

通过一个最简答的程序来看一下 CPU 是如何对待左值和右值的。

void push(int && x){int y = x;}

void push(int & x){int y = x;}

下面程序对应的汇编代码为:

push(int&&):
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-24], rdi
        mov     rax, QWORD PTR [rbp-24]
        mov     eax, DWORD PTR [rax]
        mov     DWORD PTR [rbp-4], eax
        nop
        pop     rbp
        ret
push(int&):
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-24], rdi
        mov     rax, QWORD PTR [rbp-24]
        mov     eax, DWORD PTR [rax]
        mov     DWORD PTR [rbp-4], eax
        nop
        pop     rbp
        ret

能够看到汇编指令对于右值和左值的解决是完全相同的,因而无论是左值和右值在 CPU 看来都是完全相同的。

语言层面的概念

对于底层 CPU 来说,左值和右值是齐全无感的,然而对于 C ++ 语言来说左值和右值是十分重要的概念。通俗的来看,对于左值来说,编译器是容许写操作的;然而对于右值来说,只容许读操作。左值是有残缺的生命周期的,而右值往往在执行完须要它的代码就间接被销毁了(如 x =1; 中的 1)。

晚期的 C ++ 左值就是左值,右值就是右值,不可扭转。来到了 C ++11,语言为程序员提供了将左值转换为右值的办法 —std::move。

std::move 并不能挪动任何货色,它惟一的性能是将一个左值强制转化为右值援用,继而能够通过右值援用应用该值,以用于挪动语义。从实现上讲,std::move 根本等同于一个类型转换。右值在实现期工作的时候会立即被析构,因而在应用挪动语义时须要避免产生空指针的问题。

挪动构造函数和挪动结构赋值函数

以一个例子来了解挪动构造函数和挪动结构赋值函数

class Item{
    public:
    int* x;

    Item()=default;
    Item(int val){x = new int(val);};
    Item(const Item& item){x = new int(*item.x);
        printf("copy\n");
    };
    Item(Item&& item){
        x = item.x;
        item.x = NULL;
        printf("move\n");
    };

    Item& operator=(const Item& item){if(this != &item){this->x = new int(*item.x);
        }
        printf("copy=\n");
        return *this;
    }

    Item& operator=(Item&& item){if(this != &item){
            this->x = item.x;
            item.x = NULL;
        }
        printf("move=\n");
        return *this;
    }

    ~Item(){delete x;};
};

咱们首先看一下挪动构造函数和一般复制构造函数的区别:

Item(const Item& item){x = new int(*item.x);
    printf("copy\n");
};

Item(Item&& item){
    x = item.x;
    item.x = NULL;
    printf("move\n");
};

能够发现有以下几点不同:

  1. 挪动构造函数没有新申请成员变量内存,而是间接拿来了输出成员变量指向的内存。
  2. 挪动构造函数对输出(右值)的 x 指针赋值为 NULL,因为右值在执行完之后会被析构,x 指向的内存会被开释,因而平安的做法是将右值中的 x 指向 NULL(析构时不会产生任何内存开释)。
  3. 挪动构造函数输出不能是 const 变量。因为须要批改成员变量 x 指向 NULL。

接下来察看以下复制赋值运算符和挪动赋值运算符的区别:

Item& operator=(const Item& item){this->x = new int(*item.x);
    printf("copy=\n");
    return *this;
}

Item& operator=(Item&& item){if(this != &item){
        this->x = item.x;
        item.x = NULL;
    }
    printf("move=\n");
    return *this;
}

这两种运算符类比于对应的结构构造函数原理基本相同,须要留神赋值运算符是单目运算符,调用方是等号左侧的对象。因而能够用 this 指针来对左侧元素进行批改。

须要留神的是: 挪动赋值运算符多了一步判断:判断以后指针与输出的右值地址是否雷同。

这是为了避免把本身作为输出进行挪动赋值,这样会导致失去的对象成员 x 指向 NULL。

总结

挪动结构晋升了赋值和初始化的性能,看似与复制结构只有输出上的不同,然而外部实现充斥了种种细节,在编写代码的时候肯定要留心,避免在拷贝过程中呈现外部指针对象为空的状况。

退出移动版