移动构造函数和移动赋值运算符的坑

本文深入探讨C++中移动语义的四大常见陷阱,包括隐式声明的移动构造函数和赋值运算符、显式声明移动构造函数的正确方式、不调用成员的移动构造及自我移动的问题。通过实例解析,帮助读者理解并避免这些潜在错误。

第一坑,隐式声明的移动构造函数和移动赋值运算符

  class A{
      int * p;

  public:
    A(int value):
      p(new int(value))
      {}

    ~A(){
      std::cout<<"delete pointer:"<<(void *)p<<std::endl;
      //查看析够函数删除了哪个指针
  	  delete p;
    }

  };

  int main(){
    A a(1);
    A b(2);

    std::swap(a,b);
    /*首先我们来了解一下std::swap的一些特性
      1.大概全貌如下:
        template<class T>
        void swap(T & a ,T & b);
      2.要求T必须满足MoveAssignable和MoveConstructible
        通俗讲就是有移动构造函数和移动赋值函数
    */
  }

结果如下:

  delete pointer:0x558527e3be70
  delete pointer:0x558527e3be70
  delete pointer:0x558527e3be90

你们会发现一二条发生了重复删除了两次,对于这种小范围new的来说,运行期间并没有报错,但到了大范围new数组和delete数组的时候,就会出现double free的现象。
没错,这是系统自动声明会带来的一些后果,如果你们在类里面new一个对象,我们要显示声明移动构造函数或者把移动构造函数删除掉。

第二坑,显式声明移动构造函数

你是不是和我一样把他们写成这样子?

  A(const A && );
  A & operator=(const A && );

如果你跟我一样把移动构造函数和复制构造函数混淆了形式,那你必然会写成这样:

  A(const A && a):
    p(a.p)
    {}

  A & operator=(const A && a){
    p=a.p;
    return *this;
  }

你完了,你再次引发double free。必须要着重说一下,移动构造函数形式如下:

  A (A &&);
  A & operator=(A &&);

注意,并没有const,并没有const,并没有const!!!而且必须要把右值的内容更改掉!!!

  A (A && a):
    p(a.p)
    {
      a.p=nullptr;
    }

  A & operator=(A && a){
    p=a.p;
    a.p=nullptr;
    return *this;
  }

&&的特性,右值引用就是对一个右值进行引用的类型。因为右值不具名,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去,通过右值引用来延长临时右值的生命周期。

第三坑,竟不调用成员的移动构造。

  class A{
    int * p;

  public:
    A(int value):
      p(new int(value))
      {}

    A(A & a):
      p(new int(*a.p))
    {
      std::cout<<"copy c"<<std::endl;
      //如果调用A的复制构造函数,则打印 "copy c"
    }

    A(A && a):
      p(a.p)
      {
        std::cout<<"move c"<<std::endl;
        //如果调用A的移动构造函数,则打印 "move c"
        a.p=nullptr;
      }

    A & operator=(A & a){
      std::cout<<"copy"<<std::endl;
      //如果调用A的复制赋值运算符,则打印 "copy "
      p=new int(*a.p);
      return *this;
    }

    A & operator=(A && a){
      std::cout<<"move"<<std::endl;
      //如果调用A的移动赋值运算符,则打印 "move
      p=a.p;
      a.p=nullptr;
      return *this;
    }

    ~A(){
      delete p;
    }

  };

  class B{
    A a;
  public:
    B(int value):
      a(value)
      {}

    B(B && b):
      a(b.a)
      //这里是一个标志1,标志着原来的我对c++移动构造函数的一个憧憬:
      //我原本以为A有移动构造函数了,那么B的移动构造函数应该会自动
      //调用A的移动构造函数,果然我还是太年轻了。
      {}

    B & operator=(B && b){
      a=b.a;
      //这又是一个标志2,标志着原来的我对c++移动赋值运算符的一个憧憬
      return *this;
    }
  };

  int main(){
    B b1(1);
    B b2(std::move(b1));
    b1=std::move(b2);
  }

结果如下:

  copy c
  copy

所以各位孩子们,如果你们还被移动构造函数给迷惑了,不要担心,记得在每个类的移动构造函数和移动赋值运算符加几个std::move()就行了.

  //修改标志1的内容:
  a(std::move(b.a))

  //修改标志2的内容:
  a=std::move(b.a);

需要注意的是如果是基本类型,或者不存在内存分配的类,可以不用调用std::move()因为他们都一样,都是复制构造,不存在移动构造。如果有调用内存分配的类,那你需要注意了。这三个坑,切忌。

第四坑,我自己移动我自己

比起前三个坑,第四个坑按理来说应该必须要知道的,因为这我们在复制赋值运算符就知道了,有一条语句叫做

    if(& name == this)
      return *this;

没错,我们移动赋值运算符也应该加上这一句。他的抉择比复制赋值运算符的抉择更重要,甚至可以说是刚需,但是如果我们没有或者忘了添加会有什么下场,我们继续上代码:

struct A{
  //为了方便检查指针 p 的值,我把A声明为struct
    int * p;

    A(int value):
      p(new int(value))
      {}

    A(A & a):
      p(new int(*a.p))
      {
        std::cout<<"copy c"<<std::endl;
      }

    A(A && a):
        p(a.p)
    {
        a.p=nullptr;
    }

    A & operator=(A & a){
      p=new int(*a.p);
      return *this;
    }

    A & operator=(A && a){
      p=a.p;
      a.p=nullptr;
      return *this;
    }

    ~A(){
      delete p;
    }

  };

  int main(){
    A a1(10);
    a1=std::move(a1);
    std::cout<<(void *)a.p<<std::endl;
    //打印指针p
  }

结果如下:

0

没错,你会发现a1.p直接变成了nullptr,最关键是因为在移动函数中允许他们自己移动他们自己,间接造成了内存泄漏,比起复制函数,移动函数这部分非常不显眼!!!所以在移动函数中我们应该做到阻止他们复制他们自己。

//正确的移动函数写法
  A & operator=(A && a){
    if(&a == this)
      return *this;

      p=a.p;
      a.p=nullptr;
      return *this;
  }

这段代码其实在复制复制运算符中并不是必要的,effective c++ 就讨论过这个问题。(想知道可以自己去看书)。但是对于移动构造函数,我觉得这段代码是刚需的,因为他修改了参数的值,如果参数是本身,那么就间接修改了自己,这是很烦恼的事,对于内存泄漏的排查,这些隐藏在构造函数的代码是最容易忽略和也是最难排查的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值