C++ 对象移动

一、右值引用

我们常规的引用也叫左值引用,我们不能将左值引用绑定到要求转换的表达式、字面常量或是返回右值的表达式。 而所谓右值引用就是必须绑定到右值的引用,他有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定在左值上.

int i = 42; int& r = i;//正确,常规左值引用 
int&& rr = i; //错误,不能将一个右值引用绑定在左值上 
int& r2 = i * 42;//错误,不能将左值引用绑定到一个右值上 
const int& r3 = i * 42;//正确,可以将一个指向常量的引用绑定在一个右值上 
int&& rr2 = i * 42;//正确,将一个右值引用绑定在右值上

右值引用一般只能绑定在临时对象,有如下特点:
1、所引用的对象将要被销毁
2、该对象没有其它用户

这两个特性意味着:使用右值引用的代码可以自由地接管所引用地对象的资源

NOTE: 变量表达式都是左值,因此我们不能将一个右值引用绑定到一个右值引用类型的变量上!!

int &&r1 = 40;//r1是一个右值引用类型的变量,它绑定了一个字面值
int &&r2 = r1;//非法!,右值引用不能绑定变量

二、标准库move函数

虽然不能将一个右值引用直接绑定到一个左值上,但可以显式地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值的右值引用,此函数定义在头文件utility中。

int &&rr3 = std::move(rr1)//rr1可以是任何值(右值、左值、引用),返回一个右值。

调用了move就意味着承诺:除了对rr1赋值或者销毁它外,我们将不再使用它。在调用move之后,我们不能对移后的源对象的值做任何假设。

Note:使用move的代码应该使用std::move,而不是用因为有了using namespace std 就不写,这可以避免潜在的名字冲突。

三、移动构造函数

StrVec::StrVec(StrVec &&s) noexcept:elements(s.elements),first_free(s.first_free),cap(s.cap)
{
    s.elements = s.first_free = s.cap =nullptr;
}

noexcept的作用就是告诉标准库我们的构造函数不抛出任何异常。
这个移动构造函数会先接管给定StrVec中的内存,在接管内存后,将给定对象中的指针都置为nullptr,避免移动后,旧对象被销毁析构、指针被delete。

NOTE: 不会抛出异常移动构造函数和移动赋值运算符必须标记为noexcept !!
因为标准库会因为是否有noexcept而使用或避免使用它。假设我们有一个装类对象的vector,当我们push_back的时候引起了重新分配内存,这时候,标准库有两个选择,拷贝构造函数和移动构造函数。使用拷贝构造函数相对来说更安全,因为如果在将旧空间上的对象一一拷贝到新空间的过程中,出现了异常,标准库可停止使用新分配的内存空间,这样即使出现异常,vector的内容还是没有改变,顶多就是最新的push_back没成功。而使用移动构造函数时,因为移动的过程会把旧空间的对象析构,一旦在这个过程中出现问题,旧空间的内容被破坏而新空间的内容也异常。所以如果不声明移动构造函数为noexcept,标准库会优先使用拷贝构造。

四、移动赋值运算符

StrVec &StrVec::operator= (StrVec &&rhs) noexcept
{
    if (this!=&rhs)//排除自赋值
    {
        free();//释放已有元素
        elements = rhs.elements;
        first_free = rhs.first_free;
        cap = rhs.cap;
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
    return *this;
}

先检查this指针与rhs的地址是否相同。如果相同,右侧和左侧运算对象指向相同的对象,我们可直接返回。如果不同,我们释放左侧运算对象所使用的内存,并接管给定对象的内存,最后将给定对象的指针置为nullptr。

五、移动构造和拷贝构造

一个类如果定义了拷贝构造函数拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数移动赋值运算符

同理,如果类定义了一个移动构造函数、移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的

当一个类既有移动构造函数,也有拷贝构造函数,编译器会通过普通函数匹配规则来确定使用哪个

StrVec v1,v2;
v1 = v2;//v2是左值,使用拷贝构造函数
v2 = std::move(v1);//std::move返回的是右值,虽然实参转换为const后能匹配上拷贝构造函数,但移动构造形参是右值,能直接匹配上,为最佳匹配

如果StrVec

六、移动构造实例

class Test_contruct
 {
 public:
     int data_int;
     int* p;
     Test_contruct()
     {
         data_int = 0;
         p = new int(0);
         cout << "执行了默认构造" << endl;
     } //无参构造
     Test_contruct(int var)
     {
         data_int = var;
         p = new int(0);
         cout << "执行了有参构造" << endl;
     } //有参构造
     Test_contruct(const Test_contruct& point)
     {
         data_int = point.data_int;
         p = new int(*(point.p)); 
        cout << "执行了拷贝构造" << endl;
     }//拷贝构造
     Test_contruct(Test_contruct&& point) noexcept:data_int(point.data_int) , p(point.p)
     {
         cout << "执行了移动构造" << endl;
         point.p = nullptr;
     }//移动构造
      Test_contruct operator=(const Test_contruct& point) //=运算符重载
     {
         Test_contruct temp;
         temp.data_int = point.data_int;
         temp.p = new int(*(point.p));
         cout << "执行了赋值拷贝运算符" << endl;
         return temp;
     }
     ~Test_contruct()
     {
         delete p;
     }
 };

例一:

Test_contruct Test01()
 {
     auto b = Test_contruct(2);
     return b;
 }
int main()
{
    cout << "Test01()  :" << endl;
    auto t01 = Test01();
}
/*输出为:
Test01()  :
 执行了有参构造
 执行了移动构造
*/

就是先在函数Test01内执行了有参构造函数,然后在函数返回的时候执行移动构造函数,构造t01。

例二:

Test_contruct Test02()
 {
     return Test_contruct(2);
 }
int main()
{
    cout << "Test02()  :" << endl;
    auto t02 = Test02();
}
/*输出结果
Test02()  :
 执行了有参构造
*/

如果在return的时候才构造的话,就相当于直接在main函数里执行有参构造,无移动构造

例三:

void Test03()
 {
     vector arr;
     arr.reserve(3);
     for (int i = 0; i < 4; ++i)
     {
         auto temp = Test_contruct(i);
         arr.push_back(temp);
     }
 }
int main()
{
    cout << "Test03()  :" << endl;
    Test03();
}
/*输出结果
Test03()  :
 执行了有参构造
 执行了拷贝构造
 执行了有参构造
 执行了拷贝构造
 执行了有参构造
 执行了拷贝构造
 执行了有参构造
 执行了拷贝构造
 执行了移动构造
 执行了移动构造
 执行了移动构造
*/

我们先创建了一个vertor并预留3个位置的空间,然后再循环里,先给temp执行一次有参构造函数,然后用push_back的方式加到vector中,此时会执行拷贝构造函数。当循环到了第四次后,此时push_back时,必须先将原vector的3个元素移动到一个新的空间(因为原空间只能容纳三个元素),故有了三次移动构造。

例四:

void Test04()
 {
     vector arr;
     arr.reserve(3);
     for (int i = 0; i < 4; ++i)
     {
         arr.push_back(Test_contruct(i));
         /*
         相当于:
         auto temp = Test_contruct(i);
         arr.push_back(std::move(temp));
         */
     }
 }
int main()
{
    cout << "Test04()  :" << endl;
    Test04();
}
/*输出结果
Test04()  :
 执行了有参构造
 执行了移动构造
 执行了有参构造
 执行了移动构造
 执行了有参构造
 执行了移动构造
 执行了有参构造
 执行了移动构造
 执行了移动构造
 执行了移动构造
 执行了移动构造
*/

相比于例三,执行了移动构造而不是拷贝构造,提升了一定效率

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注