C++11之 Move semantics(移动语义)(转)

转https://blog.csdn.net/wangshubo1989/article/details/49748703

按值传递的意义是什么? 
当一个函数的参数按值传递时,这就会进行拷贝。当然,编译器懂得如何去拷贝。 
而对于我们自定义的类型,我们也许需要提供拷贝构造函数。

但是不得不说,拷贝的代价是昂贵的。

所以我们需要寻找一个避免不必要拷贝的方法,即C++11提供的移动语义。 
上一篇博客中有一个句话用到了:

#include <iostream>

void f(int& i) { std::cout << "lvalue ref: " << i << "n"; }
void f(int&& i) { std::cout << "rvalue ref: " << i << "n"; }

int main()
{
    int i = 77;
    f(i);    // lvalue ref called
    f(99);   // rvalue ref called

    f(std::move(i));  // 稍后介绍

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

实际上,右值引用注意用于创建移动构造函数和移动赋值运算。

移动构造函数类似于拷贝构造函数,把类的实例对象作为参数,并创建一个新的实例对象。 
但是 移动构造函数可以避免内存的重新分配,因为我们知道右值引用提供了一个暂时的对象,而不是进行copy,所以我们可以进行移动。

换言之,在设计到关于临时对象时,右值引用和移动语义允许我们避免不必要的拷贝。我们不想拷贝将要消失的临时对象,所以这个临时对象的资源可以被我们用作于其他的对象。

右值就是典型的临时变量,并且他们可以被修改。如果我们知道一个函数的参数是一个右值,我们可以把它当做一个临时存储。这就意味着我们要移动而不是拷贝右值参数的内容。这就会节省很多的空间。

 

下面这个图,很好地说明了拷贝构造函数和移动构造函数的区别。

 C++11之 Move semantics(移动语义)(转)

 

看明白了吗?

通俗一点的解释就是,拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。

但是上面提到,指针的浅层复制是非常危险的呀。没错,确实很危险,而且通过上面的例子,我们也可以看出,浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间(同时也是b->value指向的空间)

所以我们可以把上面的拷贝构造函数的代码修改一下:

复制代码
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>

using namespace std;

class Str{
    public:
    char *value;
    Str(char s[])
    {
        cout<<"调用构造函数..."<<endl;
        int len = strlen(s);
        value = new char[len + 1];
        memset(value,0,len + 1);
        strcpy(value,s);
    }
    Str(Str &v)
    {
        cout<<"调用拷贝构造函数..."<<endl;
        this->value = v.value;
        v.value = NULL;
    }
    ~Str()
    {
        cout<<"调用析构函数..."<<endl;
        if(value != NULL)
            delete[] value;
    }
};

int main()
{

    char s[] = "I love BIT";
    Str *a = new Str(s);
    Str *b = new Str(*a);
    delete a;
    cout<<"b对象中的字符串为:"<<b->value<<endl;
    delete b;
    return 0;
}
复制代码

 

结果为:

 C++11之 Move semantics(移动语义)(转)

 

修改后的拷贝构造函数,采用了浅层复制,但是结果仍能够达到我们想要的效果,关键在于在拷贝构造函数中,最后我们将v.value置为了NULL,这样在析构a的时候,就不会回收a->value指向的内存空间。

 

这样用a初始化b的过程中,实际上我们就减少了开辟内存,构造成本就降低了。

 

但要注意,我们这样使用有一个前提是:用a初始化b后,a我们就不需要了,最好是初始化完成后就将a析构。如果说,我们用a初始化了b后,仍要对a进行操作,用这种浅层复制的方法就不合适了。

所以C++引入了移动构造函数,专门处理这种,用a初始化b后,就将a析构的情况。

 

*************************************************************

**移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。(关于右值引用大家可以看我之前的文章,或者查找其他资料)。这意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。

 

移动构造函数应用最多的地方就是STL中

给出一个代码,大家自行验证使用move和不适用move的区别吧

复制代码
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <vector>
using namespace std;

class Str{
    public:
        char *str;
        Str(char value[])
        {
            cout<<"普通构造函数..."<<endl;
            str = NULL;
            int len = strlen(value);
            str = (char *)malloc(len + 1);
            memset(str,0,len + 1);
            strcpy(str,value);
        }
        Str(const Str &s)
        {
            cout<<"拷贝构造函数..."<<endl;
            str = NULL;
            int len = strlen(s.str);
            str = (char *)malloc(len + 1);
            memset(str,0,len + 1);
            strcpy(str,s.str);
        }
        Str(Str &&s)
        {
            cout<<"移动构造函数..."<<endl;
            str = NULL;
            str = s.str;
            s.str = NULL;
        }
        ~Str()
        {
            cout<<"析构函数"<<endl;
            if(str != NULL)
            {
                free(str);
                str = NULL;
            }
        }
};
int main()
{
    char value[] = "I love zx";
    Str s(value);
    vector<Str> vs;
    //vs.push_back(move(s));
    vs.push_back(s);
    cout<<vs[0].str<<endl;
    if(s.str != NULL)
        cout<<s.str<<endl;
    return 0;
} 
复制代码

说多无语,看代码:

#include <iostream>
#include <algorithm>

class A
{
public:

    // Simple constructor that initializes the resource.
    explicit A(size_t length)
        : mLength(length), mData(new int[length])
    {
        std::cout << "A(size_t). length = "
        << mLength << "." << std::endl;
    }

    // Destructor.
    ~A()
    {
    std::cout << "~A(). length = " << mLength << ".";

    if (mData != NULL) {
            std::cout << " Deleting resource.";
        delete[] mData;  // Delete the resource.
    }

    std::cout << std::endl;
    }

    // Copy constructor.
    A(const A& other)
        : mLength(other.mLength), mData(new int[other.mLength])
    {
    std::cout << "A(const A&). length = "
        << other.mLength << ". Copying resource." << std::endl;

    std::copy(other.mData, other.mData + mLength, mData);
    }

    // Copy assignment operator.
    A& operator=(const A& other)
    {
    std::cout << "operator=(const A&). length = "
             << other.mLength << ". Copying resource." << std::endl;

    if (this != &other) {
        delete[] mData;  // Free the existing resource.
        mLength = other.mLength;
            mData = new int[mLength];
            std::copy(other.mData, other.mData + mLength, mData);
    }
    return *this;
    }

    // Move constructor.
    A(A&& other) : mData(NULL), mLength(0)
    {
        std::cout << "A(A&&). length = " 
             << other.mLength << ". Moving resource.n";

        // Copy the data pointer and its length from the 
        // source object.
        mData = other.mData;
        mLength = other.mLength;

        // Release the data pointer from the source object so that
        // the destructor does not free the memory multiple times.
        other.mData = NULL;
        other.mLength = 0;
    }

    // Move assignment operator.
    A& operator=(A&& other)
    {
        std::cout << "operator=(A&&). length = " 
             << other.mLength << "." << std::endl;

        if (this != &other) {
          // Free the existing resource.
          delete[] mData;

          // Copy the data pointer and its length from the 
          // source object.
          mData = other.mData;
          mLength = other.mLength;

          // Release the data pointer from the source object so that
          // the destructor does not free the memory multiple times.
          other.mData = NULL;
          other.mLength = 0;
       }
       return *this;
    }

    // Retrieves the length of the data resource.
    size_t Length() const
    {
        return mLength;
    }

private:
    size_t mLength; // The length of the resource.
    int* mData;     // The resource.
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103

移动构造函数 
语法:

A(A&& other) noexcept    // C++11 - specifying non-exception throwing functions
{
  mData =  other.mData;  // shallow copy or referential copy
  other.mData = nullptr;
}
  • 1
  • 2
  • 3
  • 4
  • 5

最主要的是没有用到新的资源,是移动而不是拷贝。 
假设一个地址指向了一个有一百万个int元素的数组,使用move构造函数,我们没有创造什么,所以代价很低。

// Move constructor.
A(A&& other) : mData(NULL), mLength(0)
{
    // Copy the data pointer and its length from the 
    // source object.
    mData = other.mData;
    mLength = other.mLength;

    // Release the data pointer from the source object so that
    // the destructor does not free the memory multiple times.
    other.mData = NULL;
    other.mLength = 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

移动比拷贝更快!!!

移动赋值运算符 
语法:

A& operator=(A&& other) noexcept
{
  mData =  other.mData;
  other.mData = nullptr;
  return *this;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

工作流程这样的:Google上这么说的:

Release any resources that *this currently owns. 
Pilfer other’s resource. 
Set other to a default state. 
Return *this.

// Move assignment operator.
A& operator=(A&& other)
{
    std::cout << "operator=(A&&). length = " 
             << other.mLength << "." << std::endl;

    if (this != &other) {
      // Free the existing resource.
      delete[] mData;

      // Copy the data pointer and its length from the 
      // source object.
      mData = other.mData;
      mLength = other.mLength;

      // Release the data pointer from the source object so that
      // the destructor does not free the memory multiple times.
      other.mData = NULL;
      other.mLength = 0;
   }
   return *this;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

让我们看几个move带来的好处吧! 
vector众所周知,C++11后对vector也进行了一些优化。例如vector::push_back()被定义为了两种版本的重载,一个是cosnt T&左值作为参数,一个是T&&右值作为参数。例如下面的代码:

std::vector<A> v;
v.push_back(A(25));
v.push_back(A(75));
  • 1
  • 2
  • 3

上面两个push_back()都会调用push_back(T&&)版本,因为他们的参数为右值。这样提高了效率。

而 当参数为左值的时候,会调用push_back(const T&) 。

#include <vector>

int main()
{
    std::vector<A> v;
    A aObj(25);       // lvalue
    v.push_back(aObj);  // push_back(const T&)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

但事实我们可以使用 static_cast进行强制:

// calls push_back(T&&)
v.push_back(static_cast<A&&>(aObj));
  • 1
  • 2

我们可以使用std::move完成上面的任务:

v.push_back(std::move(aObj));  //calls push_back(T&&)
  • 1

似乎push_back(T&&)永远是最佳选择,但是一定要记住: 
push_back(T&&) 使得参数为空。如果我们想要保留参数的值,我们这个时候需要使用拷贝,而不是移动。

最后写一个例子,看看如何使用move来交换两个对象:

#include <iostream>
using namespace std;

class A
{
  public:
    // constructor
    explicit A(size_t length)
        : mLength(length), mData(new int[length]) {}

    // move constructor
    A(A&& other)
    {
      mData = other.mData;
      mLength = other.mLength;
      other.mData = nullptr;
      other.mLength = 0;
    }

    // move assignment
    A& operator=(A&& other) noexcept
    {
      mData =  other.mData;
      mLength = other.mLength;
      other.mData = nullptr;
      other.mLength = 0;
      return *this;
    }

    size_t getLength() { return mLength; }


    void swap(A& other)
    {
      A temp = move(other);
      other = move(*this);
      *this = move(temp);
    }

    int* get_mData() { return mData; }

  private:
    int *mData;
    size_t mLength;
};

int main()
{
  A a(11), b(22);
  cout << a.getLength() << ' ' << b.getLength() << endl;
  cout << a.get_mData() << ' ' << b.get_mData() << endl;
  swap(a,b);
  cout << a.getLength() << ' ' << b.getLength() << endl;
  cout << a.get_mData() << ' ' << b.get_mData() << endl;
  return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

原文链接: https://www.cnblogs.com/wangshaowei/p/8880394.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    C++11之 Move semantics(移动语义)(转)

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/391934

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年3月31日 上午10:50
下一篇 2023年3月31日 上午10:50

相关推荐