Item 12:完整地拷贝对象
拷贝函数
在设计良好的面向对象系统中,封装了对象内部的配件,仅留两个函数用于对象的拷贝,它们统称为拷贝函数:拷贝构造函数和拷贝赋值运算符。
考虑一个象征消费者的类,这里的拷贝函数是手写的,以便将对它们的调用记入日志:
void logCall(const std::string& funcName); //@ make a log entry
class Customer {
public:
...
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
...
private:
std::string name;
};
Customer::Customer(const Customer& rhs)
: name(rhs.name) //@ copy rhs's data
{
logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer& rhs)
{
logCall("Customer copy assignment operator");
name = rhs.name; //@ copy rhs's data
return *this;
}
这里的每一件事看起来都不错,实际上也确实不错——直到 Customer 中加入了另外的数据成员:
class Date { ... }; //@ for dates in time
class Customer {
public:
... //@ as before
private:
std::string name;
Date lastTransaction;
};
在这里,已有的拷贝函数只进行了部分拷贝:它们拷贝了 Customer 的 name,但没有拷贝它的 lastTransaction。然而,大部分编译器对此毫不在意,即使是在最高的警告级别。
这个问题最为迷惑人的情形之一是它会通过继承发生。考虑:
class PriorityCustomer: public Customer { //@ a derived class
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
...
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
return *this;
}
PriorityCustomer 的拷贝函数看上去好像拷贝了 PriorityCustomer 中的每一样东西,但是每个 PriorityCustomer 还包括一份它从 Customer 继承来的数据成员的副本,而那些数据成员根本没有被拷贝!
PriorityCustomer 的拷贝构造函数没有指定传递给它的基类构造函数的参数,所以,PriorityCustomer 对象的 Customer 部分被 Customer 的构造函数在无参数的情况下初始化——使用缺省构造函数。那个构造函数为 name 和 lastTransaction 进行一次缺省的初始化。
对于 PriorityCustomer 的拷贝赋值运算符,情况有些微的不同。它不会试图用任何方法改变它的基类的数据成员,所以它们将保持不变。
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), //@ invoke base class copy ctor
priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs); //@ assign base class parts
priority = rhs.priority;
return *this;
}
当你写一个拷贝函数,需要保证:拷贝所有本地数据成员以及调用所有基类中的适当的拷贝函数。
-
用拷贝赋值运算符调用拷贝构造函数是没有意义的,因为你这样做就是试图去构造一个已经存在的对象。
-
用拷贝构造函数调用拷贝赋值运算符同样是荒谬的。一个构造函数初始化新的对象,而一个赋值运算符只能用于已经初始化过的对象。借助构造过程给一个对象赋值将意味着对一个尚未初始化的对象做一些事,而这些事只有用于已初始化对象才有意义。
作为一种代替,如果你发现你的拷贝构造函数和拷贝赋值运算符有相似的代码,通过创建第三个供两者调用的成员函数来消除重复。这样的函数当然是 private 的,而且经常叫做 init。
总结
- 拷贝函数应该保证拷贝一个对象的所有数据成员以及所有的基类部分。
- 不要试图依据一个拷贝函数实现拷贝赋值,反之亦不可以。作为代替,将通用功能放入第三个供双方调用的函数。