设计良好的面向对象软件系统会将对象的内部封装起来,只留两个函数负责对象的拷贝,那便是拷贝构造函数和拷贝赋值运算符。在条款5中,我们已经指出编译器会在必要的时候为我们的类生成这两个函数,在其中对每个成员变量进行浅拷贝。
一旦我们声明了自己的拷贝构造函数或者拷贝赋值运算符,编译器不仅不再负责对应函数的生成,甚至连检查都完全不进行。这在某些时候将会导致问题。
例如,我们写了一个类Customer
来表示顾客,并且手工书写了拷贝构造函数和拷贝赋值运算符,用于打印日志:
class Customer
{
public:
Customer(const Customer &other);
Customer &operator=(const Customer &other);
private:
std::string name;
};
Customer::Customer(const Customer &other)
: name(other.name)
{
Logger.info("Customer copy constructor");
//...
}
Customer &Customer::operator=(cosnt Customer &other)
{
Logger.info("Customer copy-assign operator");
name = other.name;
return *this;
}
目前为止一切正常;现在,我们需要向Customer
类中添加另一个表示日期的成员变量lastTransaction
:
class Date { /* ... */ };
class Customer
{
public:
// ...
private:
std::string name;
Date lastTransaction;
};
此时,既有的拷贝构造函数和拷贝赋值运算符仅仅拷贝了成员变量name
,而成员变量lastTransaction
却忘记添加复制逻辑了,这将导致“局部赋值”的问题;大多数编译器不会对此做出警告。
另一方面,一旦既有的类被继承,那么就存在了另一个潜在的危机:
class PriorityCustomer : public Customer
{
public:
//...
PriorityCustomer(const PriorityCustomer &other);
PriorityCustomer &operator=(const PriorityCustomer &other);
//..
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer &other) : priority(other.priority)
{
Logger.info("PriorityCustomer copy constructor");
}
PriorityCustomer &operator=(const PriorityCustomer &other)
{
Logger.info("PriorityCustomer copy-assign operator");
priority = other.priority;
return *this;
}
PriorityCustomer
的拷贝构造函数和拷贝赋值运算符看起来的确复制了自己的每个成员变量,但是每个PriorityCustomer
对象中都还包含它所继承的Customer
成员变量的副本,但是这一部分成员变量却没有被复制;因此,副本对象也同样会处于一种“局部复制”的状态。
任何时候只要我们自己承担起了“为子类手写拷贝构造函数和拷贝赋值运算符”的重责大任,就必须要非常小心地也复制其基类部分,方法是在构造函数的初始化列表中调用父类的拷贝构造函数、在拷贝赋值运算符中显示地调用父类的拷贝赋值运算符。
PriorityCustomer::PriorityCustomer(const PriorityCustomer &other) : Customer(other), priority(other.priority)
{
Logger.info("PriorityCustomer copy constructor");
}
PriorityCustomer &operator=(const PriorityCustomer &other)
{
Logger.info("PriorityCustomer copy-assign operator");
Customer::operator=(other);
priority = other.priority;
return *this;
}
本条款所说的“复制每一个成分”现在看起来很明确了:
- 复制本类中的所有成员变量。
- 调用基类中的拷贝构造函数和拷贝赋值运算符。
最后要补充的是:不论是令拷贝构造函数调用拷贝赋值运算符,还是令拷贝赋值运算符调用拷贝构造函数,这二者都是不合理的,最本质的原因是它们的用途和语义不同;如果这样做的目的仅仅是为了复用相同的代码,那么应该将这部分代码抽取为一个私有成员函数(例如常用的init
)。
【注意】
- 拷贝构造函数和拷贝赋值运算符应该确保复制对象的所有成员变量,以及由基类中继承而来的成员变量。
- 可以将拷贝构造函数和拷贝赋值运算符中通用的部分抽取为一个单独的私有成员函数。