C 箴言:拷贝一个对象的任何组成部分

2008-02-23 05:40:37来源:互联网 阅读 ()

新老客户大回馈,云服务器低至5折

  在设计良好的面向对象系统中,为了压缩其对象内部的空间,仅留两个函数用于对象的拷贝:一般称为拷贝构造函数(copy constructor)和拷贝赋值运算符(copy assignment operator)。我们将他们统称为拷贝函数(copying functions)。假如需要,编译器会生成拷贝函数,而且阐明了编译器生成的版本正象您所期望的:他们拷贝被拷贝对象的全部数据。

  当您声明了您自己的拷贝函数,您就是在告诉编译器您不喜欢缺省实现中的某些东西。编译器对此似乎怒发冲冠,而且他们会用一种古怪的方式报复:当您的实现存在一些几乎能够确定错误时,他偏偏不告诉您。

  考虑一个象征消费者(customers)的类,这里的拷贝函数是手写的,以便将对他们的调用记入日志:

void logCall(const std::string& funcName); // make a log entry

class Customer {
 public:
  ...
  Customer(const Customer& rhs);
  Customer& operator=(const Customer& rhs);
  ...

 private:
  std::string name;
};
Customer::Customer(const Customer& rhs)
: name(rhs.name) // copy rhs’s data
{
 logCall("Customer copy constructor");
}

Customer& Customer::operator=(const Customer& rhs)
{
 logCall("Customer copy assignment operator");
 name = rhs.name; // copy rhs’s data
 return *this; // see Item 10
}

  这里的每一件事看起来都不错,实际上也确实不错——直到 Customer 中加入了另外的数据成员:

class Date { ... }; // for dates in time

class Customer {
public:
 ... // as before

private:
 std::string name;
 Date lastTransaction;
};

  在这里,已有的拷贝函数只进行了部分拷贝:他们拷贝了 Customer 的 name,但没有拷贝他的 lastTransaction。然而,大部分编译器对此毫不在意,即使是在最高的警告级别(maximal warning level)。这是他们在对您写自己的拷贝函数进行报复。您拒绝了他们写的拷贝函数,所以假如您的代码是不完善的,他们也不告诉您。结论显而易见:假如您为一个类增加了一个数据成员,您务必要做到更新拷贝函数。(您还需要更新类中的全部的构造函数连同任何非标准形式的 operator=。这个问题最为迷惑人的情形之一是他会通过继承发生。考虑:

class PriorityCustomer: public Customer { // a derived class
 public:
  ...
  PriorityCustomer(const PriorityCustomer& rhs);
  PriorityCustomer& operator=(const PriorityCustomer& rhs);
  ...

 private:
  int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: priority(rhs.priority)
{
 logCall("PriorityCustomer copy constructor");
}

PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
 logCall("PriorityCustomer copy assignment operator");
 priority = rhs.priority;
 return *this;
}

  PriorityCustomer 的拷贝函数看上去似乎拷贝了 PriorityCustomer 中的每相同东西,但是再看一下。是的,他确实拷贝了 PriorityCustomer 声明的数据成员,但是每个 PriorityCustomer 还包括一份他从 Customer 继承来的数据成员的副本,而那些数据成员根本没有被拷贝!PriorityCustomer 的拷贝构造函数没有指定传递给他的基类构造函数的参数(也就是说,在他的成员初始化列表中没有提及 Customer),所以,PriorityCustomer 对象的 Customer 部分被 Customer 的构造函数在无参数的情况下初始化——使用缺省构造函数。(假设他有,假如没有,代码将无法编译。)那个构造函数为 name 和 lastTransaction 进行一次缺省的初始化。

  对于 PriorityCustomer 的拷贝赋值运算符,情况有些微的不同。他不会试图用任何方法改变他的基类的数据成员,所以他们将保持不变。

  无论何时,您打算自己为一个派生类写拷贝函数时,您必须注意同时拷贝基类部分。那些地方的典型特征当然是 private,所以您不能直接访问他们。派生类的拷贝函数必须调用和他们对应的基类函数:

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), // invoke base class copy ctor
priority(rhs.priority)
{
 logCall("PriorityCustomer copy constructor");
}

PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
 logCall("PriorityCustomer copy assignment operator");

 Customer::operator=(rhs); // assign base class parts
 priority = rhs.priority;
 return *this;
}

  本文中的 "copy all parts" 的含义现在应该清楚了。当您写一个拷贝函数,需要确保拷贝任何本地数据成员连同调用任何基类中的适当的拷贝函数。

  在实际中,两个拷贝函数经常有相似的函数体,而这一点可能吸引您试图通过用一个函数调用另一个来避免代码重复。您希望避免代码重复的想法值得肯定,但是用一个拷贝函数调用另一个来做到这一点是错误的。

  用拷贝赋值运算符调用拷贝构造函数是没有意义的,因为您这样做就是试图去构造一个已存在的对象。这太荒谬了,甚至没有一种语法来支持他。有一种语法看起来似乎能让您这样做,但实际上您做不到,更有一种语法采用迂回的方法这样做,但他们在某种条件下会对破坏您的对象。所以我不打算给您看任何那样的语法。无条件地接受这个观点:不要用拷贝赋值运算符调用拷贝构造函数。 尝试一下另一种相反的方法——用拷贝构造函数调用拷贝赋值运算符——这同样是荒谬的。一个构造函数初始化新的对象,而一个赋值运算符只能用于已初始化过的对象。借助构造过程给一个对象赋值将意味着对一个尚未初始化的对象做一些事,而这些事只有用于已初始化对象才有意义。简直是胡搞!不要做这种尝试。

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇: 二级C 重点难点分析:数据类型、表达式和基本运算

下一篇: C 箴言:只要有可能就推迟变量定义