1.默认构造函数介绍
在设计良好的面向对象系统中,会将对象的内部进行封装,只有两个函数可以拷贝对象:拷贝构造函数和拷贝赋值运算符。我们把这两个函数统一叫做拷贝函数。从Item5中,我们得知,如果需要的话编译器会为你生成这两个拷贝函数,并且编译器生成的版本能够精确的做到你想做的:它们拷贝了对象的所有数据。
2.自己实现构造函数有可能出现问题
当你声明自己的拷贝函数的时候,你就会向编译器表示,你对编译器生成版本的拷贝函数有些地方不是很喜欢。你的这种做法会让编译器以一种奇怪的方式进行报复:如果你自己实现的拷贝函数出现了问题,编译器不会告诉你。
2.1 问题出现场景一
考虑一个表示消费者的类,类中的拷贝函数已经被手动实现了,所以调用它们会被记入日志:
1 void logCall(const std::string& funcName); // make a log entry
2
3 class Customer {
4
5 public:
6
7 ...
8
9 Customer(const Customer& rhs);
10
11 Customer& operator=(const Customer& rhs);
12
13 ...
14
15 private:
16
17 std::string name;
18
19 };
20
21 Customer::Customer(const Customer& rhs)
22
23 : name(rhs.name) // copy rhs’s data
24
25 {
26
27 logCall("Customer copy constructor");
28
29 }
30
31 Customer& Customer::operator=(const Customer& rhs)
32
33 {
34
35 logCall("Customer copy assignment operator");
36
37 name = rhs.name; // copy rhs’s data
38
39 return *this; // see Item 10
40
41 }
这里的一切看上去都是好的,也确实如此,直到另外一个数据成员加到Customer类中:、
1 class Date { ... }; // for dates in time
2
3 class Customer {
4
5 public:
6
7 ... // as before
8
9 private:
10
11 std::string name;
12
13 Date lastTransaction;
14
15 };
这时候,当前的拷贝函数就会执行一个部分拷贝,它们拷贝了Customer的name成员变量,却没有拷贝lastTransaction.但大多数编译器会对这种实现默不发声,甚至一个警告级别的信息也不会发出来(看Item 53)。编译器对你自己写的拷贝函数进行了报复。你拒绝使用它们提供的拷贝函数,于是它们不会告诉你代码是否是完整的。结论很明显:如果你向类中添加一个数据成员,你需要确保同时对拷贝函数进行更新。(你同时需要更新类中所有的构造函数(Item4和Item45)和任何非标准形式的operator=(Item 10给出了一个例子)),如果你忘记了,编译器不会提醒你。
2.2 更加阴险的方式-场景二
使这个问题出现的最阴险的方式是通过继承。看下面的例子:
1 class PriorityCustomer: public Customer { // a derived class
2
3 public:
4
5 ...
6
7 PriorityCustomer(const PriorityCustomer& rhs);
8
9 PriorityCustomer& operator=(const PriorityCustomer& rhs);
10
11 ...
12
13 private:
14
15 int priority;
16
17 };
18
19 PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
20
21 : priority(rhs.priority)
22
23 {
24
25 logCall("PriorityCustomer copy constructor");
26
27 }
28
29 PriorityCustomer&
30
31 PriorityCustomer::operator=(const PriorityCustomer& rhs)
32
33 {
34
35 logCall("PriorityCustomer copy assignment operator");
36
37 priority = rhs.priority;
38
39 return *this;
40
41 }
PriorityCustomer的拷贝函数看上去拷贝了类中的所有东西,但请再看一遍。是的,它们拷贝了PriorityCustomer的所有数据成员,但是PriorityCustomer的每个对象同时包含了从Customer继承过来的数据成员,这部分数据没有被拷贝!PriorityCustomer的拷贝构造函数没有指定传到基类构造函数的参数(也就是说没有在成员初始化列表中列出Customer),所以PriorityCustomer对象的Customer部分会被Customer的无参构造函数进行初始化。(肯定会有一个,不然编译会出错。)这个构造函数会为name 和 lastTransaction执行一个默认初始化。
对于PriorityCustomer的拷贝构造运算符来说情形有些不同。它并没有以任何方式去尝试修改基类的数据成员,因此它们可以保持不变。
3.如何才能避免上面的问题
在任何时候你自己去为一个派生类实现拷贝构造函数的时候,你必须注意需要同时拷贝基类部分。这些部分当然有可能是Private的(见Item22),所以你不能直接访问它们。但是,派生类的拷贝函数必须调用对应的基类构造函数:
1 PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
2
3 : Customer(rhs), // invoke base class copy ctor
4
5 priority(rhs.priority)
6
7 {
8
9 logCall("PriorityCustomer copy constructor");
10
11 }
12
13 PriorityCustomer&
14
15 PriorityCustomer::operator=(const PriorityCustomer& rhs)
16
17 {
18
19 logCall("PriorityCustomer copy assignment operator");
20
21 Customer::operator=(rhs); // assign base class parts
22
23 priority = rhs.priority;
24
25 return *this;
26
27 }
在这个条款的标题中,“拷贝所有部分”的意思现在应该明了了。当你实现一个拷贝函数的时候,确保(1)拷贝所有本地的数据成员。(2)同时调用所有基类的合适的拷贝函数。
4.如何才能解决构造函数中的代码重复问题
在实际应用中,这两个拷贝函数通常有着类似的函数体,这会让你尝试通过一个函数调用另一个函数以达到避免代码重复的目的。你的这种想避免代码重复的愿望是值得赞赏的,但为了达到避免代码重复,用一个拷贝函数调用另外一个是错误的方法。
4.1 用赋值运算符调用拷贝构造函数-错误!
用拷贝赋值运算符来调用拷贝构造函数是没有意义的,因为你正在尝试构建一个已经存在的对象。这是荒谬的,也没有这样的语法。看上去有一些能够到达你要求的语法,但实际上不是。有一些语法确实能够做到,但在一些情况下会破坏你的对象。所以我不会向你展示这些语法的任何部分。你不想通过拷贝赋值运算符去调用拷贝构造函数,接受这个想法就可以了。
4.2 用拷贝构造函数调用赋值运算符-错误!
相反,使用拷贝函数调用拷贝赋值运算符也同样是没有意义的。一个拷贝构造函数是初始化新的对象的,但是一个赋值运算符只能够应用在已经被初始化的对象上面。在一个对象上通过构造函数来执行赋值就意味着,你正在对一个未初始化的对象做某些事情,但这件事情对初始化的对象才有意义。没有意义,不要去尝试!
4.3 正确的做法-将相同代码提炼成第三个函数
想反,如果你发现你的拷贝构造函数和拷贝赋值运算符有着看上去类似的函数体,通过创建可以同时被两个构造函数调用的第三个成员函数来消除代码重复。这样的函数应该被声明为Private并且通常叫做Init.这个策略是安全的,可以达到消除重复的目的。