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()函数让两个拷贝函数来调用。