条款12:复制对象时勿忘其每一个成分
Copy all parts of an object.
本章内容较长,分两部分进行学习。
copying函数
设计良好的面向对象的系统,会将对象的内部封装起来,只留下两个函数来负责对象的拷贝(复制):
- copy构造函数
- copy assignment操作符
我们将它们一起成为copying函数。
假如我们要声明自己的copying函数,即告诉编译器自己不会去使用缺省实现的某些行为,此时编译器却是会在代码几乎必然出错的情况下,却不去告诉你。
举个例子,考虑一个class,用来表示顾客,其中人为地书写copying函数(而非由编译器去创建),使得外界对它们的调用都会记录下来(logged):
void logCall(const std::string& funcName); //制作一个log entry
class Customer {
public:
...
Customer(const Customer& rhs); //copy构造函数
Customer& operator=(const Customer& rhs); //copy assignment操作符
...
private:
std::string name;
};
Customer::Customer(const Customer& rhs) : name(rhs.name) //复制rhs的数据
{
logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer& rhs)
{
logCall("Customer copy assignment operator);
name = rhs.name; //复制rhs的数据
return *this;
}
虽然上面的代码看起来并没有什么问题,但是当另一个变量加入到其中时:
class Date { ... }; //日期
class Customer {
public: //定义与前面相同
...
private:
std::string name;
Date lastTransaction;
};
此时,既有的copying函数执行的是局部拷贝(partial copy):
- 它们只复制了顾客的name,而没有复制新添加的lastTransaction。
这明显是个错误,但是编译器却并不会报错。
因此,如果我们为class添加一个新的成员变量时,就必须同时修改copying函数。(同时也需要修改class的所有构造函数以及任何非标准形式的operator=)
不完全复制
另外,一旦发生继承,则会造成一个更严重的危机:
class PriorityCustomer: public Customer { //定义Derived class
public:
...
PriorityCustomer(const PriorityCustomer& rhs); //
PriorityCustomer& operator=(const PriorityCustomer& rhs);
...
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: priority(rhs.priority) //复制rhs的数据
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
return *this;
}
上面的代码中,PriorityCustomer的copying函数看起来好像是复制了PriorityCustomer内的每一样东西,但是实际上,它们只是复制了PriorityCustomer声明的成员变量,但是每个PriorityCustomer还内含了它所继承的Customer成员变量复件,而那些成员变量并没有被复制。即:
- Derived class没有连同base class中的字段一起复制。
PriorityCustomer的copy构造函数并没有指定实参传给其base class构造函数。(也即说它在它的成员初值列(member initialization list)中没有提到Customer)。
因此,PriorityCustomer对象的Customer成分会被不带有Customer构造函数(即default构造函数)初始化。
default构造函数将会针对name和lastTransaction执行缺省的初始化动作。
因此,对于PriorityCustomer,它不曾企图修改其base class的成员变量,因此那些成员变量保持不变。