总结:
1. 拷贝函数应该保证拷贝一个对象的所有数据成员以及所有的基类部分。
2. 不要试图依据一个拷贝函数实现另一个。作为代替,将通用功能放入第三个供双方调用的函数。
设计良好的面向对象系统中,封装了对象内部,仅留两个函数用于对象的拷贝:拷贝构造函数和拷贝赋值运算符,统称为拷贝函数。编译器生成版的copy函数会拷贝被拷贝对象的所以成员变量。
考虑一个表现顾客的类,这里的拷贝函数是手工写成的,以便将对它们的调用志记下来:
void logCall(const std::string&funcName); // 制造一个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) // 复制rhs的数据
{logCall("Customer copy constructor");}
Customer& Customer::operator=(constCustomer& rhs)
{
logCall("Customer copy assignment operator");
name= rhs.name; //复制rhs的数据
return*this;
}
这里的每一件事看起来都不错,实际上也确实不错——直到 Customer 中加入了另外的数据成员:
class Date { ... }; // 日期
class Customer {
public:
... // 同前
private:
std::string name;
Date lastTransaction;
};
在这里,已有的拷贝函数只进行了部分拷贝:它们拷贝了 Customer 的 name,但没有拷贝它的 lastTransaction。然而,大部分编译器即使是在最高的警告级别也不出任何警告。结论显而易见:如果你为一个类增加了一个数据成员,你务必要做到更新拷贝函数,你还需要更新类中的全部的构造函数以及任何非标准形式的 operator=。
一旦发生继承,可能会造成此主题最暗中肆虐的一个暗藏危机。考虑:
PriorityCustomer::PriorityCustomer(constPriorityCustomer& rhs)
: Customer(rhs), // 调用基类的copy构造函数
priority(rhs.priority)
{logCall("PriorityCustomer copy constructor");}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs); // 对基类成分进行赋值动作
priority = rhs.priority;
return*this;
}
无论何时,你打算自己为一个派生类写拷贝函数时,必须注意同时拷贝基类部分。那些成分往往是private,所以你不能直接访问它们,应该让派生类的拷贝函数调用相应的基类函数。当你写一个拷贝函数,需要保证(1)拷贝所有本地数据成员以及(2)调用所有基类中的适当的拷贝函数。
· 拷贝函数应该保证拷贝一个对象的所有数据成员以及所有的基类部分。
在实际中,两个拷贝函数经常有相似的函数体,而这一点可能吸引你试图通过用一个函数调用另一个来避免代码重复。你希望避免代码重复的想法值得肯定,但是用一个拷贝函数调用另一个来做到这一点是错误的。
“用拷贝赋值运算符调用拷贝构造函数”和“用拷贝构造函数调用拷贝赋值运算符”都是没有意义的。如果发现你的拷贝构造函数和拷贝赋值运算符有相似的代码,通过创建第三个供两者调用的成员函数来消除重复。这样的函数当然是 private 的,而且经常叫做 init。这一策略可以消除拷贝构造函数和拷贝赋值运算符中的代码重复,安全且被证实过。
· 不要试图依据一个拷贝函数实现另一个。作为代替,将通用功能放入第三个供双方调用的函数。