11.3.2 拷贝构造函数

出现上述问题是因为编译器对如何从现有的对象产生新的对象进行了假定。当通过按值传递的方式传递一个对象时,就创立了一个新对象,函数体内的对象是由函数体外的原来存在的对象传递的。从函数返回对象也是同样的道理。在表达式中:

11.3.2 拷贝构造函数 - 图1

先前未创立的对象h2是由函数f()的返回值创建的,所以又从一个现有的对象中创建了一个新对象。

编译器假定我们想使用位拷贝来创建对象。在许多情况下,这是可行的。但在HowMany类中就行不通,因为初始化不是简单的拷贝。如果类中含有指针又将出现另一个问题:它们指向什么内容,是否拷贝它们或它们是否与一些新的内存块相连?

幸运的是,可以介入这个过程,并可以防止编译器进行位拷贝。每当编译器需要从现有的对象创建新对象时,可以通过定义自己的函数做这些事。因为是在创建新对象,所以,这个函数应该是构造函数,并且传递给这个函数的单一参数必须是创立的对象的源对象。但是这个对象不能通过按值传递方式传入构造函数,因为正在试图定义的函数就是为了处理按值传递方式的,而且按句法传递一个指针是没有意义的,毕竟我们正在从现有的对象创建新对象。这里,引用就起作用了,可以使用源对象的引用。这个函数被称为拷贝构造函数,它经常被称为X(X&)(它叫做类X的外在表现)。

如果设计了拷贝构造函数,当从现有的对象创建新对象时,编译器将不使用位拷贝。编译器总是调用我们的拷贝构造函数。所以,如果没有设计拷贝构造函数,编译器将做一些判断,但可以选择完全接管这个过程的控制。

现在可以解决HowMany.cpp中的问题。

11.3.2 拷贝构造函数 - 图2

11.3.2 拷贝构造函数 - 图3

这儿加入一些新的方法,使我们能很好地理解发生过程。首先,当对象的信息被打印出来时,string name起着对象识别作用。在构造函数内,可以设置一个标识符字串(通常是对象的名字),它通过string构造函数拷贝至name中。默认值""构造了一个空字符串。同样,构造函数将增加而析构函数减少objectCount的值。

其次是拷贝构造函数HowMany2(const HowMany2&)。拷贝构造函数可以仅从现有的对象创立新对象,所以,现有的对象的名字被拷贝给name,这样就能了解它是从哪里拷贝来的。如果深入了解,将会看到在构造函数的初始化表上对name(h.name)的调用事实上就是调用了string拷贝构造函数。

在拷贝构造函数内部,对象数目会像普通构造函数一样的增加。这意味着当参数通过按值传递方式传递和返回时,我们能得到准确的对象数目。

print()函数已经被修改,用于打印消息、对象标识符和对象数目。现在print()函数必须访问具体对象的name数据,所以不再是静态成员函数。

在main()函数内部,可以看到又增加了一次函数f()的调用。但这次使用了普通的C语言调用方式,且忽略了函数的返回值。既然现在知道了值是如何返回的(即在函数体内,代码处理返回过程并把结果放在目的地,目的地的地址作为一个隐藏的参数传递)。返回值被忽略将会发生什么,程序的输出将对此作出解释。

在显示输出之前,这里有一个小程序,它使用了iostreams可为任何文件加入行号。

11.3.2 拷贝构造函数 - 图4

整个文件被读入vector<string>,这使用了本书前面同样的代码。当打印行号时,我们希望所有的行都能彼此对齐,这就要求在文件中调整行的数目,以使得各行号所允许的宽度是一致的。我们可以轻松地通用vector:size()决定行的数目,但我们真正所需要知道的是它们是否超过了10行、100行、1000行等。如果对文件的行数取以10为底的对数,把它转为整型并再加1,这样就可得到行的最大宽度。

我们将会注意到,在for循环的内部有两个特殊的调用:setf()和width()。在这方面,ostream调用允许控制对齐方式和输出的宽度。但是它们必须在每一行被输出时都要调用,这也就是为什么它们被置于for循环内的原因。在本书的第2卷有一章是说明输出流的,它将介绍更多有关控制输出流的调用和其他的一些方法。

当Linenum.cpp被应用于HowMany2.out时,结果如下:

11.3.2 拷贝构造函数 - 图5

11.3.2 拷贝构造函数 - 图6

正如所希望的,第一件发生的事是为h调用普通的构造函数,对象数增加为1。但在进入函数f()时,拷贝构造函数被编译器调用,完成传值过程。在f()内创建了一个新对象,它是h的拷贝(因此被称为“h拷贝”),所以对象数变成2,这是拷贝构造函数的作用结果。

第8行显示了从f()返回的开始情况。但在局部变量“h拷贝”销毁以前(在函数结尾这个局部变量便出了范围),它必须被拷入返回值,也就是h2。先前未创建的对象(h2)是从现有的对象(在函数f()内的局部变量)创建的,所以在第9行拷贝构造函数当然又被使用。现在,对于h2的标识符,名字变成了“h拷贝的拷贝”。因为它是从拷贝拷过来的,这个拷贝是函数f()内部对象。在对象返回之后,函数结束之前,对象数暂时变为3,但此后内部对象“h拷贝”被销毁。在13行完成对f()的调用后,仅有2个对象h和h2。这时可以看到h2最终是“h拷贝的拷贝”。

11.3.2.1 临时对象

第15行开始调用f(h),这次调用忽略了返回值。在16行可以看到恰好在参数传入之前,拷贝构造函数被调用。和前面一样,21行显示了为了返回值而调用拷贝构造函数。但是,拷贝构造函数必须有一个作为它的目的地(this指针)的工作地址。但这个地址从哪里获得呢?

每当编译器为了正确地计算一个表达式而需要一个临时对象时,编译器可以创建一个。在这种情况下,编译器创建一个看不见的对象作为函数f()忽略了的返回值的目标地址。这个临时对象的生存期应尽可能的短,这样,空间就不会被这些等待被销毁且占用珍贵资源的临时对象搞乱。在一些情况下,临时对象可能立即传递给另外的函数。但在现在这种情况下,临时对象在函数调用之后不再需要,所以一旦函数调用完结就对内部对象调用析构函数(23和24行),这个临时对象就被销毁(25和26行)。

在28-31行,对象h2被销毁了,接着对象h被销毁。对象记数非常正确地回到了0。