1.问题的引入
在C++中拷贝函数有两类:拷贝构造函数和拷贝赋值运算符。在文章C++类中默认生成的函数中我提到过,如果我们在类中没有声明拷贝函数,C++中的编译器会默认自动为你生成。如果我们在类中自己声明了拷贝函数,意思就是告诉编译器你并不喜欢它默认生成的拷贝函数。当编译器感觉到自己被"冒犯"时,它就会对你进行"报复",即在你实现的代码出错时,并不会报错来通知你。如下例所示:
1void logCall(const std::string &funcName); 2 3class Customer 4{ 5public: 6 // ... 7 Customer(const Customer &rhs); // 拷贝构造函数 8 Customer &operator=(const Customer &rhs); // 拷贝赋值运算符函数 9private: 10 std::string name; 11}; 12 13Customer::Customer(const Customer &rhs) : name(rhs.name) 14{ 15 logCall("Customer类的拷贝构造函数"); 16} 17 18Customer &Customer::operator=(const Customer &rhs) 19{ 20 logCall("Customer类的拷贝赋值运算符函数"); 21 name = rhs.name; 22 return *this; 23}
上面的Customer类中,使用的是自定义的拷贝函数。出现的问题:当在类中加入一个新的成员变量lastTransaction时,拷贝函数执行的是局部拷贝。如下例所示:
1void logCall(const std::string &funcName); 2 3class Date{ 4 // ... 5}; 6class Customer 7{ 8public: 9 // ... 10 Customer(const Customer &rhs); // 拷贝构造函数 11 Customer &operator=(const Customer &rhs); // 拷贝赋值运算符函数 12private: 13 std::string name; 14 Date lastTransaction; // 新加入的成员变量 15};
上例中,拷贝函数的确复制了成员变量name,但是并没有复制新增加的成员变量lastTransaction。大多数编译器对此种情况也不会报错,这是编译器对你因不使用它为你默认生成的拷贝函数,作出的复仇行为。解决方法:如果你为类添加了一个成员变量,你必须同时修改你的拷贝函数。
2.隐藏在继承中的局部拷贝现象
局部拷贝现象更可能潜在发生的地方是继承层级中,假设我们在普通用户之上定义一个VIP用户。如下例所示:
1void logCall(const std::string &funcName); 2 3class Customer 4{ 5public: 6 // ... 7 Customer(const Customer &rhs); // 拷贝构造函数 8 Customer &operator=(const Customer &rhs); // 拷贝赋值运算符函数 9private: 10 std::string name; 11}; 12 13Customer::Customer(const Customer &rhs) : name(rhs.name) 14{ 15 logCall("Customer类的拷贝构造函数"); 16} 17 18Customer &Customer::operator=(const Customer &rhs) 19{ 20 logCall("Customer类的拷贝赋值运算符函数"); 21 name = rhs.name; 22 return *this; 23} 24 25class PriorityCustomer: public Customer{ 26public: 27 // ... 28 PriorityCustomer(const PriorityCustomer& rhs); 29 PriorityCustomer& operator=(const PriorityCustomer& rhs); 30 // ... 31private: 32 int priority; 33}; 34 35PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): priority(rhs.priority){ 36 logCall("子类PriorityCustomer的拷贝构造函数"); 37} 38 39PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs){ 40 logCall("子类PriorityCustomer的拷贝赋值运算符函数"); 41 priority = rhs.priority; 42 return *this; 43}
看起来,子类PriorityCustomer的拷贝函数像是复制了PriorityCustomer类中的每一个成员。但是,注意:子类PriorityCustomer的拷贝函数仅仅复制了PriorityCustomer声明的成员变量,但子类PriorityCustomer中还包含了它从Customer中所继承过来的基类的成员变量副本,这些成员变量是没有被复制的。子类PriorityCustomer的拷贝构造函数并没有指定实参传递基类Customer的拷贝构造函数(即子类PriorityCustomer拷贝构造函数的初始化列表中没有提及Customer)。因此,子类PriorityCustomer对象中的Customer成分会被不带实参的Customer默认的拷贝构造函数来初始化。默认拷贝构造函数将对name和lastTransaction成员变量进行默认的初始化操作。但是,上述情况在子类PriorityCustomer的拷贝赋值运算符函数中有些许不同。子类PriorityCustomer的拷贝赋值运算符函数不会企图去修改基类中的成员变量,所以那些成员变量会保持不变。
3.局部拷贝现象的解决方法
解决方法:任何时候,只要你为子类自定义了拷贝函数,你必须很小心地也复制其基类的成员。那些成员往往是private的,所以你的子类无法直接访问这些成员,你应该让子类的拷贝函数调用相应的基类拷贝函数。如下例所示:
1PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): Customer(rhs), priority(rhs.priority){ // 让子类的拷贝构造函数调用基类的拷贝构造函数 2 logCall("子类PriorityCustomer的拷贝构造函数"); 3} 4 5PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs){ 6 logCall("子类PriorityCustomer的拷贝赋值运算符函数"); 7 Customer::operator=(rhs); // 让子类的拷贝赋值运算符函数调用基类的拷贝赋值运算符函数 8 priority = rhs.priority; 9 return *this; 10}
4.代码优化
可能大家也会发现,上面各例中的拷贝构造函数与拷贝赋值运算符函数有相似的功能和代码,那么我们能不能为了避免代码重复,让其中一个函数调用另一个呢?答案是不能!因为使用拷贝赋值运算符函数调用拷贝构造函数,或者使用拷贝构造函数调用拷贝赋值运算符函数,都是没有意义的。拷贝赋值运算符函数适用于已经构造好的对象,而拷贝构造函数适用于还没有构造好的对象,所以这种做法在语义上是错误的。
如果我们真的想要节省代码,比如某个类有特别多的数据成员,我们可以写另一个第三方函数来给每个成员赋值,拷贝构造函数和拷贝赋值运算符函数都可以调用它,这个第三方函数一般叫init()。
5.总结
(1)拷贝函数应该确保拷贝对象内的所有成员变量以及所有子类中的基类成分。
(2)不要试图以拷贝构造函数或拷贝赋值运算符函数去调用拷贝赋值运算符函数或拷贝构造函数,正确的做法是可以另写一个第三方函数init()让两个函数来调用它。