“复制赋值”和“移动赋值”的思考

Posted axianzZ

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了“复制赋值”和“移动赋值”的思考相关的知识,希望对你有一定的参考价值。

概述

从 C++ 11 中开始,该语言支持两种类型的分配:复制赋值移动赋值。其中的内部细节是咋样的呢?今天跟踪了一下,是个蛮有趣的过程。下面我们以一个简单的类来做个分析。

#ifndef HASPTR_H
#define HASPTR_H


#include <string>

class HasPtr {
public:
    friend void swap(HasPtr&, HasPtr&);
    HasPtr(const std::string& s = std::string());
    HasPtr(const HasPtr& hp);
    HasPtr(HasPtr&& p) noexcept;
    HasPtr& operator=(HasPtr rhs);
    // HasPtr& operator=(const HasPtr &rhs);
    // HasPtr& operator=(HasPtr &&rhs) noexcept;
    ~HasPtr();

private:
    std::string* ps;
    int i;
};

#endif // HASPTR_H


#include "hasptr.h"
#include <iostream>

inline void swap(HasPtr& lhs, HasPtr& rhs)
{
    using std::swap;
    swap(lhs.ps, rhs.ps);
    swap(lhs.i, rhs.i);
    std::cout << "call swap" << std::endl;
}

HasPtr::HasPtr(const std::string& s) : ps(new std::string(s)), i()
{
    std::cout << "call constructor" << std::endl;
}

//这里的i+1只是为了方便调试的时候看过程,实际是不用加1的
HasPtr::HasPtr(const HasPtr& hp) : ps(new std::string(*hp.ps)), i(hp.i + 1)
{
    std::cout << "call copy constructor" << std::endl;
}

HasPtr::HasPtr(HasPtr&& p) noexcept : ps(p.ps), i(p.i)
{
    p.ps = 0;
    std::cout << "call move constructor" << std::endl;
}

HasPtr& HasPtr::operator=(HasPtr rhs)
{
    swap(*this, rhs);
    return *this;
}

HasPtr::~HasPtr()
{
    std::cout << "call destructor" << std::endl;
    delete ps;
}

主函数

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    HasPtr hp1("hello"), hp2("World"), *pH = new HasPtr("World");
    hp1 = hp2;
    hp1 = std::move(*pH);

    return a.exec();
}

下面我们开始调试:

输出:

我们通过构造函数构造了三个变量,他们的值和

  address ps i
hp1 0x28fe64 "hello" 0
hp2 0x28fe5c "World" 0
pH 0x28fe9c "World" 0

 

 

 

复制赋值

我们接着单步走:

可以发现首先调用了复制构造函数,构造了一个和hp2一样的临时变量

  address ps i
this 0x28fe2c "World" 1
hp2 0x28fe5c "World" 0

 

 

 

下一步:

到这里才开始进行赋值运算,我们对比一下数据:

  address ps i
this 0x28fe2c "hello" 0
rhs 0x28fe8c "World" 1

 

 

 

这里的rhs就是我们刚刚分配的临时变量,那么this就是hp1,所以最终是我们的临时变量和hp1交换,我们接着走:

这里lhs的地址就是:0x28fe64,就是hp1的地址,交换之后:

到此hp1和临时变量的值就完全交换过来了,也就是说hp1 = hp1了。

  address ps i
lhs 0x28fe64 "World" 1
rhs 0x28fe8c "hello" 0

 

 

 

 

可是我们接着运行,发现进入了一个析构函数:

 

看一下地址是0x28fe8c以及其值,这是临时变量,临时变量不用了,所以被销毁了,至此我们的复制赋值运算就结束了。

移动赋值

我们看一下移动赋值赋值:

 

首先进入移动函数,这里只是使指针指向了pH的数据,并未构造新的数据,变量右值引用了pH,只是相当于换了个名字。

接下来开始进入赋值运算:

 

这里两个交换的值是hp1和pH,和复制赋值不同,它是和临时变量交换数据,

 

后面进入析构函数:

它释放掉了pH的数据。

 

可以看出来,pH的值被释放掉了。

总结

调试过后,我们发现,赋值运算的过程并非像想象中那么简单,是不是?复制赋值还是开辟一个临时变量用于转化,这个耗费了额外的空间资源。

移动赋值就可以避免这个问题,但是需要注意的是,移动赋值使用的是右值,用完之后就被销毁了,所以,如果想把一个左值当做右值来用,必须确保这个左值在这之后不需要使用了。

 参考:

  1.   《C++ primer》

以上是关于“复制赋值”和“移动赋值”的思考的主要内容,如果未能解决你的问题,请参考以下文章

对象的赋值运算符

5 规则(用于构造函数和析构函数)过时了吗?

字符串类设计与应用

编译器何时为类的特殊成员提供定义?

第10课 面向对象的增强(default/deleteoverride/final)

c++ 接口必须遵守五法则吗?