Effective C++ 读书笔记 Item12 完整地拷贝对象(拷贝构造函数、复制运算符)

C++有两种拷贝函数(copying function): 拷贝构造函数(copy constructor)和拷贝赋值操作符(copy assignment operator)。在第五章我们讲到过,如果在自己定义的类中不声明这些拷贝函数,编译器会自动为你生成。如果我们声明了自己的拷贝函数,程序将会执行我们自己的拷贝函数。我们来看一个栗子:

void logCall(const std::string& funcName);
class Customer{
  public:
    ...
    Customer(const Customer& rhs);
    Customer& operator=(const Customer& rhs);
  private:
    std::string name;
};

Customer::Customer(const Customer& rhs):name(rhs.name){   //使用初始化列表
  logCall("Customer copy constructor");
}

Customer& Customer::operator=(const Customer& rhs){
  logCall("Customer copy assignment operator");
  name = rhs.name;    //拷贝数据
  return *this;       //返回*this,见第10章
}

以上我们定义了Customer类的构造函数(使用了初始化列表)和拷贝赋值操作符,这些代码是没有问题的。可是直到我们新增了一个数据成员:

class Date{...};
class Cutomer{
  ...
  private:
    std::string name;
    Data lastTransaction;     //新增了一个交易日期的数据成员
};

如果我们依然使用相同的两种拷贝函数,那么我们只能得到一个部分拷贝的对象(partial copy),我们只能拷贝到name而不能拷贝到lastTransaction。

解决方法:

很简单很直接,当我们的类新增数据成员时,要保证拷贝函数也要照顾到新来的同志们。


部分拷贝更可能潜在发生的地方是继承层级中,假设我们在普通用户之上定义一个VIP用户:

class PriorityCustomer : public Customer{
  public:
    ...
    PriorityCustomer(const PriorityCustomer& rhs);
    PriorityCustomer& operator=(const PriorityCustomer& rhs);
  private;
    int priority;
};

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
  :priority(rhs.prority){           //使用初始化列表来构造该类的数据成员
  logCall("PriorityCustomer copy constructor");  
}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs){
  logCall("PriorityCustomer copy assignment operator");
  priority = rhs.priority;          //拷贝该类的数据成员
  return *this;
}

以上代码看起来像是拷贝了所有的数据成员,但是却忘记了它的基类部分!如果我们不传入基类对象作为子类构造函数的参数,当构造这个子类的时候,它的基类的默认构造函数将会被调用,结果就是,基类Customer的name等数据成员被设定为了默认值,那么全体VIP用户的数据就丢失了(人家充了钱反而号被你搞丢了)

我们再来看赋值操作符,赋值操作符同样没有把基类作为传进来的参数,因此当拷贝某对象时,它的基类部分也不会被拷贝进来,所以这样的代码同样会导致一个部分拷贝。

解决方法:

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
  : priority(rhs.prority), Customer(rhs){     //要把基类部分也添加进初始化列表
  logCall("PriorityCustomer copy constructor"); 
}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs){
  logCall("PriorityCustomer copy assignment operator");
  Customer::operator=(rhs);         //要使用基类的拷贝操作符
  priority = rhs.priority; 
  return *this;
}

这一章讨论的是完整拷贝的问题,但一般我们也要考虑到操作符的异常安全性(exception safety),所以需要结合上一章讨论的的异常安全性,例如使用先拷贝再调换(copy and swap)的思路,来实现万全的拷贝赋值功能。

可能大家也会发现,C++的这两种拷贝函数有相似的功能和代码,那么我们能不能避免代码重复,让其中一个拷贝函数调用另一个呢?答案是不能

使用拷贝赋值操作符调用拷贝构造函数,或者使用拷贝构造函数调用拷贝赋值操作符,都是没有意义的。拷贝赋值操作符适用于已经构造好的对象,而拷贝构造函数适用于还没有构造好的对象,所以这种做法在语义上是错误的。

如果我们真的想要节省代码,比如某个类有特别多的数据成员,我们可以写另一个函数用来给每个成员赋值,两个拷贝函数都可以调用,这个函数一般叫init()。

总结:

  • 拷贝函数要照顾到类的所有部分,包括所有的数据成员和它的基类部分
  • 不要用一个拷贝函数来实现另一个拷贝函数,两种拷贝函数的语义不同。如果要节省代码,可以另写一个init()函数让两个拷贝函数来调用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值