移动复制构造函数与移动赋值构造函数

引言

对象移动是C++11中一个重要的特性,在C++的以前版本,在很多地方在逻辑上并不需要拷贝,但实则必须拷贝,比如说vector的分配空间,所以在合适的情况下使用对象移动会使程序性能大幅度提升,下面就来简单的介绍下移动构造函数与std::move函数

浅复制与深复制问题

说起移动构造函数就不得不提起拷贝构造函数中的浅复制与深复制问题,这在C++ Primer Plus 与 C++ Primer中均有提到(个人认为C++ Primer Plus 在这个问题的解释上更加的引人入胜),其实问题也非常简单,就是在成员中有指针时默认的拷贝构造函数是浅复制,就会导致两个指针指向同一块内存,这样在第一个对象在析构时就会释放指针,从而导致拷贝的对象中的指针成了空悬指针,且第二个对象析构时其指针已经被释放,所以便有可能出现delete两次的情况,这是当然是一个巨大的错误。

其实我们的移动构造函数就比较像这个问题,不过移动构造函数省去了其他成员的复制,直接把所有成员的管理权转交给了复制的对象,也就是说不仅是指针,被复制对象的所有成员操作权转移到了复制对象的身上,这样就省去了拷贝的时间与资源,从而达到提升效率的问题,这就是移动构造函数的初衷。

这里拷贝大佬一份代码,侵删,链接在文末。

#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中
    vector<Str> vs;
    vs.push_back(std::move(s));//左值转换成右值
    //vs.push_back(s);
    cout<<vs[0].str<<endl;
    if(s.str != NULL)
        cout<<s.str<<endl;
    return 0;
} 

这是个非常棒的例子,值得学习这个知识点的每个人仔细考虑 我们先来看下输出

普通构造函数...
移动构造函数...
I love zx
析构函数
析构函数

我们可以清楚的看到在对象被初始化时调用了一次普通的构造函数,而在进入vector时则是调用了移动构造函数且其值在调用std::move后丢失 因为move会返回调用对象的亡值,所以在函数调用后vs中值被"盗",这样做有什么好处呢,我们把std::move换成一般对象来看看

普通构造函数...
拷贝构造函数...
I love zx
I love zx
析构函数
析构函数

我们可以看到函数调用变成了拷贝构造函数 且原对象中值仍旧存在 所以在我们确保不需要对象中的值时使用移动构造函数提高性能,否则请使用拷贝构造函数

拷贝构造函数与移动构造函数的参数问题

我们知道在参数是右值时应使用移动构造函数,参数是左值时应使用赋值构造函数,但我们知道当没有移动构造函数时右值也是可以匹配拷贝构造函数的,那意味着参数不正确会导致二义性,从而无法编译成功,所以我们这样来设计拷贝构造函数与移动构造函数

StrVec(const StrVec&);
StrVec &operator=(const StrVec &);
StrVec(StrVec&&) noexcept;//移动构造函数
StrVec &operator=(StrVec&&)noexcept;//移动赋值构造函数

通过在拷贝构造函数上加上const限定符而是右值的最佳匹配成为 && 而不是 const &,这样就完美的解决了二义性的问题

copy and swap (拷贝交换技术)

这个东西很有意思 我们先来看一段代码

StrVec &operator=(StrVec vec){
	swap(*this,vec);
	return *this;
}

在我们编写构造函数时不仅要考虑效率,还要去考虑异常安全,这段代码完美的解决了异常安全与自赋值问题,且代码十分简介美观,为什么解决了异常安全问题呢,因为就算出现问题,也会在参数拷贝是出现错误,而不会影响this对象,自赋值问题当然
也是随之解决,但我们能否用这个函数来代替赋值构造函数与移动赋值构造函数呢,其实在我的理解下各有好处,

拷贝交换技术

优点

  • 异常安全且解决自赋值
  • 代码清晰 语义简洁

缺点

  • 效率不理想(对象被拷贝了两次)

赋值构造函数

优点

  • 可根据不同参数调用不同函数 效率高(对象移动不需要拷贝)
  • 正确的代码也可保证异常安全与防止自赋值

缺点

  • 代码较冗杂
  • 代码过程中需小心使用move来操作移动构造函数以提升效率

拷贝交换技术是一个还需要在考虑的地方,需要在进行学习进行再深一步的考虑

移动构造函数大量应用于STL

我们在平时的代码工作中会大量的使用STL库,如果给我们的类编写移动复制构造函数与移动赋值构造函数的话,可以显而易见的减少拷贝以提升我们的代码的效率。

这是C++ Primer 中一个简单的Vector模型,可以直观的看出移动构造函数中效率的差别 重点在alloc_n_copy 和 push_back中对于右值的处理 大家可仔细考虑

#include<memory>
#include<string>
#include<utility>
#include<iostream>
#include<algorithm>
using namespace std;

//简易Vector
class StrVec
{
    public:
        StrVec():
            elements(nullptr),first_free(nullptr),cap(nullptr){}
        StrVec(const StrVec&);
        StrVec &operator=(const StrVec &);
        StrVec(StrVec&&) noexcept;//移动构造函数
        StrVec &operator=(StrVec&&)noexcept;//移动赋值构造函数
        ~StrVec();
        void push_back(const string &); //两个版本的push_back会根据参数的不同选择重载函数
        void push_back(string &&);      //避免在参数为右值的情况下进行不必要的拷贝
        size_t size() const {return first_free-elements;}
        size_t capacity() const {return cap-elements;}
        string* begin() const {return elements;}
        string* end() const {return cap;}
        
    private:
        static allocator<string> alloc;
        void chk_n_alloc(){
            if(size()==capacity())
            reallocate();//当初始分配的空间不够用时从新分配内存
        }
        pair<string*,string*> alloc_n_copy
        (const string *,const string *);
        void free();
        void reallocate();
        string *elements;//起始点
        string *first_free;//首元素位置
        string *cap;//结束点
};
allocator<string> StrVec::alloc;//静态变量类外初始化

StrVec::StrVec(StrVec&&tmp) noexcept
:elements(tmp.elements),first_free(tmp.first_free),cap(tmp.cap)
{
    cout << "yidonggouzao\n";
    tmp.first_free=tmp.elements=tmp.cap=nullptr;
}

StrVec &StrVec::operator=(StrVec&&tmp) noexcept
{
    cout << "yidongfuzhi\n";
    if(this!=&tmp)//防止自赋值
    {
        free();
        elements = tmp.elements;
        first_free = tmp.first_free;
        cap = tmp.cap;
        tmp.cap=tmp.first_free=tmp.elements=nullptr;
    }
    return *this;
}


StrVec::~StrVec()
{
    cout << "hello\n";
    free();
}

/* void StrVec::push_back(const string & str)
{
    chk_n_alloc();
    alloc.construct(first_free++,std::move(str));//这样效率会更高
    //以上写法不好说效率                //上面是我没学函数的移动版本时写的 错误显然
}
 */

void StrVec::push_back(const string &str)
{
    chk_n_alloc();
    alloc.construct(first_free++,str);
}

//对于函数同时提供拷贝版本与移动版本 会使效率提高
void StrVec::push_back(string &&str)
{
    chk_n_alloc();
    alloc.construct(first_free++,std::move(str));
}

pair<string*,string*> 
StrVec::alloc_n_copy(const string *a,const string *b)
{
    //分配一段原始的内存
    auto data = alloc.allocate(b-a);
    //把a 到 b 的元素拷贝到data
    return {data,uninitialized_copy(make_move_iterator(a),make_move_iterator(b),data)};
    //利用移动迭代器 提高效率
    //没有移动构造函数会很浪费资源
}

void StrVec::free()
{
/*     if(elements)
    {
        for(auto x = first_free ;x!=elements ;x--){
            alloc.destroy(x);
        }
        alloc.deallocate(elements,cap-elements);
        //deallocate只有对其中每一个元素执行destory才能调用
    } */

    if(elements)
    {
        for_each(elements,first_free,[](string &rhs){alloc.destroy(&rhs);});
        //利用for_each 与 lambda 可以使代码清晰不少
        alloc.deallocate(elements,cap-elements);
    }//语义更加明显
}

StrVec::StrVec(const StrVec&tmp)
{
    auto data = alloc_n_copy(tmp.begin(),tmp.end());
    elements = data.first;
    first_free = cap = data.second;
    //有多少元素便拷贝多少元素
}

StrVec & StrVec::operator=(const StrVec & tmp)
{
    auto data = alloc_n_copy(tmp.begin(),tmp.end());
    free();//销毁自身内存
    elements = data.first;
    first_free = cap = data.second;
    return *this; //返回本身
}

//重新分配内存 并保留以前的部分 拷贝赋值并销毁以前的数据这样会显得效率十分低下
//所以使用  移动构造函数
void StrVec::reallocate()
{
    auto newcapacity = size()?2*size():1;
    auto newdata = alloc.allocate(newcapacity);//分配空间
    auto dest = newdata;
    auto elem = elements;
    for(size_t i=0;i!=size();i++){
        alloc.construct(dest++,std::move(*elem++));
        //把左值通过move转化为右值 然后就满足移动构造函数的条件 移动后elem其中就没有值了
    }
    free();
    elements = newdata;
    first_free = dest;
    cap = elements + newcapacity;
}

int main()
{
    StrVec temp;
    temp.push_back("hello");
    cout << temp.capacity() << endl;
    temp.push_back("world");
    temp.push_back("nihao");
    cout << temp.capacity() << endl;
    //StrVec tmp=std::move(temp);
    //StrVec tmp=temp;
    StrVec tmp;
    tmp = std::move(temp);//并没有调用析构函数
    cout << "up\n";
    cout << tmp.capacity() << endl;
    cout << temp.capacity() << endl;
    return 0;
    //移动构造函数的优缺点到这里就很清楚了
    //优:效率高,避免了无意义的拷贝
    //缺:会使移后源对象进行析构函数 
    //结论:在确保不需要等号后面的数据时 使用移动拷贝控制函数 得到效率的提升
}

这就是我对于移动复制构造函数与移动赋值构造函数的理解,希望对有同样的你有有所帮助。

参考:
青儿哥哥
拷贝交换技术

posted @ 2022-07-02 13:18  李兆龙的博客  阅读(445)  评论(1编辑  收藏  举报