第十三章 拷贝控制

最好有自定义的拷贝控制,因为编译器定义的版本的行为可能并非我们所想。

拷贝构造函数第一个参数必须是一个引用,否则,为了调用拷贝构造函数,我们必须拷贝它的实参,为了拷贝实参,我们又要调用拷贝构造函数,陷入无限循环。

拷贝初始化与直接初始化的区别:http://blog.csdn.net/qq_38216239/article/details/78233813

在拷贝初始化的过程中,编译器可以(但不是必须)略过拷贝/移动构造函数,直接创建对象。

重载运算符本质上是函数。某些运算符,包括赋值运算符,必须定义为成员函数,其左侧运算对象绑定到隐式参数this。

赋值运算符通常返回一个指向其左侧运算对象的引用。

析构函数:无返回值,不接受参数,不能被重载。析构函数体自身并不直接销毁成员,销毁成员是在函数体之后的隐含的析构阶段中进行的。销毁类类型成员调用成员自己的析构函数,内置类型则什么也不需要做。

class E
{
  public:
  ~E();//析构函数
  //...
 };

构造函数:成员初始化在函数体执行之前,按类中出现顺序初始化。
析构函数:先执行函数体,再销毁成员,按初始化逆序销毁。

A、只有真实存在的对象离开其作用域时才会调用析构函数,对象的引用,指向对象的指针离开其作用域时,不会调用析构函数。这是为了安全起见,因为很多时候可能对象的引用,指向对象的指针离开作用域时,对象还在其作用域。为了减少程序的bug,建议当对象离开其作用域后,我们让对象的引用,指向对象的指针失效,或者干脆就不再使用它。
B、使用new运算符创建的对象的资源,只有使用delete运算符删除指向它的指针时,才会调用它的析构函数,释放它的资源。这点要特别注意,当我们在类中显式定义析构函数时,函数体中通常就包含delete语句。
C、类中的静态成员属于类,不属于类的对象,它们的资源不会被析构函数释放。析构函数的调用与构造函数的调用有明显不同:析构函数可以被显式调用,而构造函数不能。显式调用析构函数和调用类的其它成员函数没什么不同。当析构函数被显式调用时,只执行它的函数体,而不删除对象的资源。也就是说,当析构函数被显式调用时,它就是一个普通的成员函数,没有析构功能。(注:引用自http://blog.csdn.net/szchtx/article/details/6898326

更新三/五法则:如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。

阻止拷贝:定义为删除的函数 =delete ,而不应该将拷贝控制成员声明为private的。
析构函数不能是删除的成员。
当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就会被定义为删除的。

管理类外资源的类:行为像值——原对象和副本独立:标准库容器和string类等。
行为像指针——原对象和副本使用相同的底层数据:shared_ptr类等。
(I/O类和unique_ptr类不允许拷贝或赋值,既不像值也不像指针)

拷贝赋值运算符要考虑到自赋值操作:

class HasPtr //类值行为的类
{
  public:
    //相关控制成员
    HasPtr& operator=(const HasPtr&);//拷贝赋值运算符
  private:
    string* ps;
    int i;  
};
HasPtr& HasPtr::operator=(const HasPtr& rhs)
{
    auto newps = new string(*rhs.ps);//拷贝string,独立副本
    /*在销毁左侧运算对象之前要先拷贝右侧运算对象。否则,如果是自赋值操
    作,即rhs和左侧对象是同一个,我们delete ps时,rhs对象的资源也被
    释放*/                           
    delete ps;
    ps = newps;
    i = rhs.i;
    return *this;
}
class HasPtr //类指针行为的类,用内置指针直接管理资源
{
  public:
    //相关控制成员
    HasPtr(const HasPtr& h):
      ps(h.ps), i(h.i), use(h.use) {++*use;}//拷贝构造函数,递增计数器
    HasPtr& operator=(const HasPtr&);//拷贝赋值运算符
    ~HasPtr()
    {
      if (--*use == 0) //析构函数要检查引用计数,才能决定是否释放资源
      {  delete ps;
         delete use; }
    }
  private:
    string* ps;
    int i;  
    size_t* use;//自定义引用计数
};
HasPtr& HasPtr::operator=(const HasPtr& rhs)
{
  ++*rhs.use; //递增右侧对象引用计数
  if (--*use == 0)//递减左侧对象引用计数
  {delete ps; delete use;}
  ps = rhs.ps;
  i = rhs.i;
  use = rhs.use;
  return *this;
}

对于分配了资源的类,定义类的swap可能是一种很重要的优化手段。
拷贝并交换技术,在赋值运算符中使用swap:

inline void swap(HasPtr& lhs, HasPtr& rhs)
{
  //操作
}
HasPtr& HasPtr::operator=(HasPtr rhs) //注意,参数为值传递,不能定义为const的
{
  swap(*this, rhs);//调用HasPtr版本的swap,而非std::swap
  return *this;
} //优势:天然是异常安全的,且能处理自赋值

关于左值、右值、左值引用和右值引用:
http://blog.csdn.net/qq_38216239/article/details/78193098
使用move的代码应尽量使用std::move而不是move,避免潜在的名字冲突。

定义移动构造函数和移动赋值运算符来避免不必要的拷贝。移动构造函数必须确保移后源对象(moved_from object)是可析构的。

class StrVec
{
public:
  StrVec(StrVec&&) noexcept;//告诉标准库可以使用move操作,而不是默认的copy操作,会提升性能,前提是确保真的不会抛出异常。
  //...
private:
  //成员s1,s2,s3都是指针  
};
StrVec::StrVec(StrVec&& s) noexcept//声明、定义处都必须标记noexcept
: s1(s.s1), s2(s.s2), s3(s.s3)
{
  s.s1 = s.s2 =s.s3 = nullptr;//确保s可析构
}  

只有当一个类没有定义任何自己版本的拷贝控制成员,且所有非static成员都能移动时,才会合成默认移动构造函数和移动赋值运算符。
当类既有拷贝构造函数又有移动构造函数时:参数为右值移动,为左值则拷贝。但是没有定义移动构造函数时,右值也可调用拷贝,且是安全的。

移动迭代器(适配器):make_move_iterator(普通迭代器),返回一个移动迭代器,是通过改变给定迭代器的解引用运算符的行为来适配的,移动迭代器的解引用运算符生成一个右值引用,可以用于uninitialized_copy的参数,实现通过移动来“拷贝”。

有时对成员函数定义移动和拷贝两个版本也有好处。

使用引用限定符指出引用成员的对象是左值还是右值。
注意:如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值