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对象矛盾。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。