天天看点

《C++面向对象高效编程(第2版)》——4.5 对象复制的语义

本节书摘来自异步社区出版社《c++面向对象高效编程(第2版)》一书中的第4章,第4.5节,作者: 【美】kayshav dattatri,更多章节内容可以访问云栖社区“异步社区”公众号查看。

c++面向对象高效编程(第2版)

复制对象是oop中的一个很普通的操作。既然在我们的世界中,一切皆是对象,我们肯定会遇到需要某个对象的多个副本的情况。

如第3章所述,在许多不同的情况中都需要复制对象。例如,当按值传递(和按值返回)参数给函数时,就需要制作对象的副本。当函数被调用时,复制操作由语言(编译器)发起,这是一个隐式进行的操作。

当然,对象的副本也可由程序员通过声明显式创建。我们可以编写如下代码:

void foo(tperson theperson)

   // foo函数按值接受一个tperson参数

{

   // 此处代码不重要,已略去。

}

main()

   tperson bar(“foo bar”, “unknown”, 414235056, “6-6-99”);

   // ...

   foo(bar); //调用以对象“bar”为参数的foo函数

}<code>`</code>

调用foo()时,将制作对象bar的副本。在c++中,用复制构造函数完成这样的复制操作,即通过现有对象制作一个该对象的副本。我们在tperson类中尚未实现复制构造函数,所以,编译器会生成默认复制构造函数(default copy constructor):

`

tperson::tperson(const tperson&amp; source)`

该复制构造函数将执行数据成员的逐个成员复制(memberwise copy)。复制构造函数中的代码类似图4-8。

class tpoint2d {

  public:

    tpoint2d(double x = 0.0, double y = 0.0);

    distanceto(const tpoint2d&amp; otherpoint);

    // 复制构造函数和赋值操作符省略 – 由编译器提供

    // ... 其他细节省略

  private:

    double _xcoordinate;

    double _ycoordinate;

};<code>`</code>

编译器生成的简单复制构造函数可以完全满足需要。该类的对象包含两个双精度数,只需复制它们的值即可。

回到tperson类,我们并不希望复制_name和_address数据成员的值,因为它们是指针。我们希望为它们所指向的内容分配足够的内存,然后复制那些内容。在复制操作完成后,源对象和目的对象之间不会共享任何东西。这就是深复制。

smalltalk:

smalltalk为所有类都提供了两种复制对象的方法shallowcopy和deepcopy。在smalltalk系统中,所有对象都可以使用这些方法。shallowcopy方法创建一个新对象,但该对象与原始对象共享状态;deepcopy方法复制对象及其状态,而且这种复制将为对象内所包含的所有对象进行递归复制。因此,deepcopy的结果是生成一个与源对象完全一样,但互相独立的对象,生成的对象与源对象不共享任何东西。在smalltalk中,每个类都获得copy方法,而且copy的默认实现就是shallowcopy。需要组合使用shallowcopy和deepcopy的类(很多情况都需要这样)应该实现自己的copy方法。smalltalk按值传递对象的语义和c++中传递引用几乎等价。然而需要注意的是,一些最新的smalltalk实现(如visualworks)使用了略为不同的复制方案。

eiffel:

eiffel在复制对象时,遵循组合的引用—值(reference-value)语义。复制对象的方法称为clone(),在默认情况下,所有类都可以使用(与create类似)。该成员函数对基本类型(如整数和字符)进行真正地复制,即复制它们的值。然而,如果原始对象包含对另一个对象的引用,则只复制引用,而不是复制被引用的对象,这与浅复制十分类似。在本书其他章节提到过,在eiffel中,对象只能包含基本类型或对其他对象的引用。对象不能按值包含另一个对象它只能包含对其他对象的引用2(为了方便共享)。如果类的实现者需要一个不同的复制语义,必须在类中为其他成员函数也提供相应的复制语义。eiffel并不真正支持按值调用。在eiffel中,调用函数(或过程)将把形参与实参相关联,这和c++中的引用类似。但不同的是,被调函数不能直接修改实参。换言之,对于任何arg参数,被调函数都不能修改它的值。如果arg是一个基本类型参数,则被调函数不能修改arg的值(它所引用的内容);如果arg是类类型,则被调函数不能将arg与新对象相关联,或将其与void引用。但是,eiffel允许函数通过arg调用方法来修改arg。(不能把这种策略和c++的c<code>onst</code>成员函数混淆。在任何情况下,无论是直接还是间接,都不能修改<code>const</code>成员函数的对象。)鉴于eiffel的这个特性,函数只可通过预定义的保留名称result,才能向主调函数返回值,result可以在函数内部(而不是在过程中)使用。

理解复制对象的另一种方法是将一个对象想象为树的根节点。该对象(节点)可以包含任意数量的对其他对象(节点)的引用(在c++中,也包括任意数量的指向其他对象(节点)的指针)。当我们以树的形式描绘对象图时,它是一棵非常大(深)的树。深复

《C++面向对象高效编程(第2版)》——4.5 对象复制的语义

图4-9

制从树的根节点开始,然后递归遍历整棵树,复制每一个节点。复制操作结束后,我们获得一棵新树,它和原树完全一样。而浅复制只能复制根节点,其他所有节点在原树和副本树之间共享,复制的结果只是一棵带共享节点的树(见图4-9)。

现在,我们来看看具有深复制语义的复制构造函数的实现:

继续阅读