《C++代码设计与重用》——2.5 浅拷贝和深拷贝

2.5 浅拷贝和深拷贝

C++代码设计与重用
2.5 浅拷贝和深拷贝
有两个操作,尽管它们具有某些不合乎需要的特性,但因为它们的使用范围很广,进而博得一定的注意,所以这两个操作在这里有必要特别提及一下,这两个操作就是浅拷贝操作和深拷贝操作。x对象的浅拷贝是指:另一个和x相同类型的,并且它的数据成员和x相对应的数据成员具有相同值的对象。x对象的深拷贝是指:另一个和x类型相同的对象,它具有x直接或间接指向的对象的一份拷贝,并且在拷贝里,所有共享和循环的联系依旧保留。考虑下面3个类:

class Z {
          //没有数据成员
          //...
};
class Y {
private::
     Z* z;
     //没有其他数据成员
     //...
};
class x {
private:
     int i
     Y* y1;
     Y* y2;
     //没有其他数据成员
     //...
};

在图2.1中,x2是X类型对象x1的浅拷贝,x3是x1的深拷贝。

但是,对一个设计得很好,并且正确实现的程序库(几乎没有异常产生),它的用户应该可以请求创建某个对象的拷贝—通过拷贝构造函数—并且由程序库适当实现某种对象类型的拷贝。除了一些特殊的类之外,用户是不需要了解拷贝函数的实现机制的,也不应该指定用某种特殊的方式来拷贝一个对象。

让我们还是回到讨论的话题,对一个给定的类,我们很少用浅拷贝操作或深拷贝操作来实现它的拷贝构造函数。假设我们用下面代码来实现2.1节中的Rational类:

class Rational {
   private:
        Rational_rep* rep;
        //...
   };
   class Rational_rep {
   private:
        int num;
        int denom;
        //...
   };

在这里,我们只是简单地把成员变量num和denom移到一个单独的类里面。(我们将在8.2.4节看到,这种实现方法为类Rational的用户提供了链接兼容性。)下面是类Rational和类Rational_rep用深拷贝来实现的拷贝构造函数:

class Rational {
    public:
         Rational(const Rational& r) {
              Rep = new Rational_rep(*r.rep);
         }
         //...
    };
    class Rational_rep {
    public:
         Rational_rep(Rational_rep& rep) :
              num(rep.num),
              denom(rep.denom) {
         }
         //...
    );

当用浅拷贝或者深拷贝来正确实现拷贝构造函数的时候,我们应该理所当然地类似(浅拷贝还有很大区别)上面那样来实现这个构造函数。但是,上面代码之所以可以实现,也仅仅是某种巧合;我们并不向用户建议这种巧合。如果类的实现改变了,那么拷贝构造函数就可能不再实现浅拷贝或者深拷贝了。实际上,用户也不应该委托拷贝构造函数实现这两种拷贝。此外,这种(巧合)现象—指通过浅拷贝或者深拷贝来实现类的拷贝构造函数—发生的概率要比程序员所认为的少很多。因为对大部分类来说,浅拷贝和深拷贝都不能用来实现类的拷贝构造函数;并且对一些特殊的类,浅拷贝和深拷贝往往带有不适合需要的属性:它们不能保持程序的不变性。例如,为了尽可能地共享类Rational_rep,我们可以这样来改变类Rational的实现:

Struct Rational_rep {
         int refcnt;    //引用计数。
         //...
    };
    class Rational {
    public:
         Rational(const Rational& r) {
              red = r.rep;
              ++rep->refcnt;
         }
         //...
    };

一般当我们要共享某个对象的时候,我们必须增加引用计数,用它来决定在什么时候可以删除一个共享对象。对于任何使用类Rational这个版本的程序,它的不变性是指类Rational_rep里面的引用计数等于指向这个共享Rational_rep的类Rational的数目。读者容易看出,创建类Rational的浅拷贝将会违背这个不变性。

类Rational的问题也并不局限于浅拷贝。考虑下面的转型,它在Rational不为零的情况下返回真值。

class Rational {
    public:
         operator bool() const {
                  return rep->num != 0;
         }
         //...
    };

假设用户经常调用这个函数。为了优化这个函数的实现,让我们改变类Rational的实现,来使所有值为零的Rational对象都指向同一个Rational_rep对象:

class Rational {
    public:
        operator bool() const {
                 return rep == rep_of_zero;
        }
        //...
    private:
        static Rational_rep* rep_of_zero;
        //...
    };

这个bool转型函数避免了一个间接的调用(很显然获得了一些效率的优化,但这仅仅是一个例子)。读者容易核实,创建一个值为零的Rational对象的深拷贝,将会破坏不变性,这里的不变性是指,所有值为零的Rational对象都指向同一个Rational_rep对象1。

如果浅拷贝或者深拷贝操作有可能破坏某个不变性定义的话,那么类的实现者当然可以通过插入代码以恢复这个不变性,从而避免破坏。然而,保存不变性这个做法并没有改变浅拷贝或深拷贝破坏不变性的这个事实。况且,在更加复杂的类里面,浅拷贝或者深拷贝操作也照样破坏不变性,并且这种破坏性是很难甚至不可能得到修复的(见练习2.5c)。

假设类X的浅拷贝和深拷贝操作不会破坏任何不变性,那么类X是否应该提供这些操作呢?见下面例子:

class X {
    public:
         X* shallow_copy();  //应该提供这个函数吗?
         X* deep_copy();   //应该提供这个函数吗?
         //...
    };

当然不应该,除非X是那些非常特殊的类,并且允许用户指定它的拷贝实现方式。另一方面,如果我们想改变类X的实现,又不得破坏不变性,并且提供浅拷贝和深拷贝操作,那么这种改变也是不可能实现的。因此,我们可以这样认为:类一般不应该提供浅拷贝和深拷贝操作(见练习2.5d)。

1译注:由深拷贝的定义可知,如果执行深拷贝操作,那么也将拷贝出一个新的Rational rep对象,这与要求只有同一个Rational rep对象矛盾。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

时间: 2025-01-23 18:47:59

《C++代码设计与重用》——2.5 浅拷贝和深拷贝的相关文章

《C++代码设计与重用》导读

前言 C++代码设计与重用 一切事物都将得到检验并因此被称为问题. Edith Hamilton 这本书的主要目的在于:展示如何以C++编程语言编写可重用代码-就是说,根据不同的需要,在不经过修改,或者经过很少修改的前提下,可重用代码可以很容易地应用到5个.50个甚至500个程序当中,而且这些程序往往是不同程序员编写的,可能运行在不同的系统上.在整个阐述的过程中,我们的目的并不在于争论是否所有的代码都是可重用的,也不在于说明可重用代码能够解决所有的程序问题.显然,不论是对程序员而言,还是对可重用

《C++代码设计与重用》——2.9 总结

2.9 总结 C++代码设计与重用2.9 总结正规函数-拷贝构造函奴.析构函数.基本赋值运算符.相等运算符和不等运算符-在所有的类中都应该实现相同的语义. 尽管没有最小标准接口,但是nice函数-缺省构造函数.拷贝构造函数.赋值运算符和相等运算符-应该是大多数类都提供的函数.没有任何函数是所有的类都应该提供的函数:而且,绝大多数类都不应该提供浅拷贝和深拷贝操作. 对程序库中类的接口一致性,我们应该给予充分的重视.但是当一致性使类的接口变得很不适当或者不直观时,我们就不能一味顽固地坚持这种一致性.

《C++代码设计与重用》——2.4 存在最小标准接口吗

2.4 存在最小标准接口吗 C++代码设计与重用2.4 存在最小标准接口吗一些专家(如[RC90]里的Riel和Carter)在对类进行深入研究之后,主张所有的类都应该提供某个最小标准接口.但究竟应该提供哪种最小标准接口呢?不同专家的建议往往又大相径庭.所有建议的标准接口除了包含nice函数(指nice类提供的函数)外,还包含诸如输入输出函数.哈希函数.以字符串返回类名的函数.浅拷贝和深拷贝操作等. 即使提供某个最小标准接口的动机是好的,但如果试图对所有的类都定义这个最小标准接口,那就是很不可取

《C++代码设计与重用》——2.10 练习

2.10 练习 C++代码设计与重用2.10 练习2.1 给出下面被建议为最小标准接口函数的反例: a.输入函数: b.输出函数: c.用字符串返回外层类类名的函数. 2.2 考虑类WORM_Pool,它和2.4.1节的Pool类很相似,但这一点除外,它在只能写一次但可读多次的内存区域分配内存块.那么,类WORD_Pool是析构函数的反例吗?请说明是或不是的原因. 2.3 假设我们为用户提供一个类Buf,它描述一个缓冲区: class Buf { public: Buf(size_t sz);

《C++代码设计与重用》——2.3 Nice类

2.3 Nice类 C++代码设计与重用 2.3 Nice类 我们都知道类会提供某些函数,这些函数要么是在类的代码中被显式声明为公共的(public)或保护的(protected),要么是由编译器在程序需要这些代码时隐式生成的.例如,下面这个类: class X{ public: X(); void f(); }; 它提供了一个缺省构造函数.函数f.一个拷贝构造函数.一个赋值运算符和一个析构函数.而且最后3个函数会在程序需要它们的时候由编译器自动生成. 请考虑下面这个通常有用的函数: templ

《C++代码设计与重用》——1.5 这本书能给我们带来什么

1.5 这本书能给我们带来什么 C++代码设计与重用 1.5 这本书能给我们带来什么 编写可重用代码可以使复杂的问题变得比较简单,但编码过程是非常困难的.这本书不会也不能让这困难的过程变得格外简单,这本书也没有提供能让每个C++程序员都可以很轻松地编写出可重用代码的锦囊妙计. 针对每个希望编写出可重用代码的C++程序员,这本书的每一章都讨论了一个或者多个他们必须理解的问题.理解了这些问题虽然不能使编写可重用代码变得相当简单,但可以让编写出可重用代码成为一种可能. 这本书的其余部分的结构如下: 当

《C++代码设计与重用》——1.2 重用的神话

1.2 重用的神话 C++代码设计与重用1.2 重用的神话关于代码重用出现了许多神话(荒诞的说法),这一节我们来反驳几个比较普遍的说法. 神话1:重用可以解决软件危机 软件危机是指程序设计团体现今没有能力做到以下几点:编写解决复杂问题的程序,快速生成解决复杂问题的程序,正确编写这些程序并使这些程序的维护相当容易. 软件开发进步的迹象是显而易见的.一个很显然的迹象就是随着时间的推移,所谓的复杂问题的范围发生了改变.在20世纪60年代,编写一个FORTRAN-66编译器就被认为是一个非常复杂的问题:

《C++代码设计与重用》——1.1 什么是重用性

1.1 什么是重用性 C++代码设计与重用 1.1 什么是重用性 许多相同操作都会在多个计算机程序里重复实现,例如: 对数组元素进行排序:解答线性方程组:实现一个从X类型到Y类型的映射:解析C++代码:从数据库检索数据:和其他程序进行通信.与其在每个程序里都设计和实现上面每个操作的相同代码,我们更愿意采用的方法是:只设计和实现这些操作的代码一次,然后再把这些代码重用手不同程序里.显然,已有的可重用代码,使每个应用程序不必从头写起,因为它(可重用代码)大大加速了应用程序的开发,并且减少了编写和维护

《C++代码设计与重用》——2.7 转型

2.7 转型 C++代码设计与重用2.7 转型程序库设计者必须充分重视隐式转型(implicit conversion).在C++中,有两种方法可以用来定义从类型From到类型To的隐式转型.第一种,我们可以在类To中定义一个只含一个参数的构造函数(并且没有其他的缺省参数): class To { public: To(const From&); //或者是To(From) //... }; 或者,我们可以在类From中定义一个转型操作: class From { public: operato