C++箴言:拷贝一个对象的所有组成部分

2016-02-19 12:17 8 1 收藏

清醒时做事,糊涂时读书,大怒时睡觉,无聊时关注图老师为大家准备的精彩内容。下面为大家推荐C++箴言:拷贝一个对象的所有组成部分,无聊中的都看过来。

【 tulaoshi.com - 编程语言 】

在设计良好的面向对象系统中,为了压缩其对象内部的空间,仅留两个函数用于对象的拷贝:一般称为拷贝构造函数(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" 的含义现在应该清楚了。当你写一个拷贝函数,需要保证(1)拷贝所有本地数据成员以及(2)调用所有基类中的适当的拷贝函数。

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

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

  作为一种代替,如果你发现你的拷贝构造函数和拷贝赋值运算符有相似的代码,通过创建第三个供两者调用的成员函数来消除重复。这样的函数当然是 private 的,而且经常叫做 init。这一策略是在拷贝构造函数和拷贝赋值运算符中消除代码重复的安全的,被证实过的方法。

  Things to Remember

  ·拷贝函数应该保证拷贝一个对象的所有数据成员以及所有的基类部分。

  ·不要试图依据一个拷贝函数实现另一个。作为代替,将通用功能放入第三个供双方调用的函数。

来源:http://www.tulaoshi.com/n/20160219/1600477.html

延伸阅读
一旦程序员抓住对象传值的效率隐忧,很多人就会成为狂热的圣战分子,誓要根除传值的罪恶,无论它隐藏多深。他们不屈不挠地追求传引用的纯度,但他们全都犯了一个致命的错误:他们开始传递并不存在的对象的引用。这可不是什么好事。 考虑一个代表有理数的类,包含一个将两个有理数相乘的函数: class Rational...
在上一篇文章中介绍了作为资源管理类支柱的 Resource Acquisition Is Initialization (RAII) 原则,并描述了 auto_ptr 和 tr1::shared_ptr 在基于堆的资源上运用这一原则的表现。并非所有的资源都是基于堆的,然而,对于这样的资源,像 auto_ptr 和 tr1::shared_ptr 这样的智能指针通常就不像 resource handlers(资源管理者)那样合适。在这种...
背景:人像摄影中的重要组成部分 以人物为主体的照片,背景的作用十分重要,通过人物与背景的相互关系可揭示人物的内心世界。在拍摄人像上背景是最重要的元素之一,可以衬托主题营造情境增加美感等。 从人像摄影的实践来看,背景可能是多种多样的,有平面的,有纵深的,有室内的,也有室外的如何借背景刻画被摄人物的性格特征,可从...
近年来,广告已成为很多网站的主要收入来源。不久前,在线广告往往遭到访客的拒绝,广告客户也不确定它的价值和效力。今天,大多数访客期望在商业网站上看到广告,广告客户已经认识到各种在线广告的潜在机会。长期以来广告一直是印刷出版物的一部分,如杂志和报纸,现在它们已经在网上期刊和出版物扮演同样的角色。 网站所有者或者发布的产品...
一个 C++ 日期类(第一部分) 原著 Chuck Allison 翻译:孟谨 原文出处:Code Capsules:A C++ Date Class, Part 1 本文适合初级读者 Chuck Allison 是盐湖城圣 Latter Day 教堂总部下耶稣教堂家族历史研究处的软件体系设计师。他拥有数学...

经验教程

988

收藏

100
微博分享 QQ分享 QQ空间 手机页面 收藏网站 回到头部