假设你正在从事一个软件项目,它处理动物。在这个软件里,大多数动物能被抽象得非常类似,但两种动物--晰蜴和小鸡--需要特别处理。显然,晰蜴和小鸡与动物类的联系是这样的:

动物类处理所有动物共有的特性,晰蜴类和小鸡类特别化动物类以适用这两种动物的特有行为。

这是它们的简化定义:

class Animal {

public:

Animal& operator=(const Animal& rhs);

...

};

class Lizard: public Animal {

public:

Lizard& operator=(const Lizard& rhs);

...

};

class Chicken: public Animal {

public:

Chicken& operator=(const Chicken& rhs);

...

};

这里只写出了赋值运算函数,但已经够我们忙乎一阵了。看这样的代码:

Lizard liz1;

Lizard liz2;

Animal *pAnimal1 = &liz1;

Animal *pAnimal2 = &liz2;

...

*pAnimal1 = *pAnimal2;

这里有两个问题。第一,最后一行的赋值运算调用的是Animal类的,虽然相关对象的类型是Lizard。结果,只有liz1的Animal部分被修改。这是部分赋值。在赋值后,liz1的Animal成员有了来自于liz2的值,但其Lizard成员部分没被改变。

第二个问题是真的有程序员把代码写成这样。用指针来给对象赋值并不少见,特别是那些对C有丰富经验而转移到C++的程序员。所以,我们应该将赋值设计得更合理的。如Item M32指出的,我们的类应该容易被正确适用而不容易被用错,而上面这个类层次是容易被用错。

一个解决方法是将赋值运算申明为虚函数。如果Animal::operator=是虚函数,那句赋值语句将调用Lizard的赋值操作(应该被调用的版本)。然而,看一下申明它为虚后会发生什么:

class Animal {

public:

virtual Animal& operator=(const Animal& rhs);

...

};

class Lizard: public Animal {

public:

virtual Lizard& operator=(const Animal& rhs);

...

};

class Chicken: public Animal {

public:

virtual Chicken& operator=(const Animal& rhs);

...

};

基于C++语言最近作出的修改,我们可以修改返回值的类型(于是每个都返回正确的类的引用),但C++的规则强迫我们申明相同的参数类型。这意味着Lizard类和Chicken类的赋值操作必须准备接受任意类型的Animal对象。也就是说,这意味着我们必须面对这样的事实:下面的代码是合法的:

Lizard liz;

Chicken chick;

Animal *pAnimal1 = &liz;

Animal *pAnimal2 = &chick;

...

*pAnimal1 = *pAnimal2;                 // assign a chicken to

// a lizard!

这是一个混合类型赋值:左边是一个Lizard,右边是一个Chicken。混合类型赋值在C++中通常不是问题,因为C++的强类型原则将评定它们非法。然而,通过将Animal的赋值操作设为虚函数,我们打开了混合类型操作的门。

这使得我们处境艰难。我们应该允许通过指针进行同类型赋值,而禁止通过同样的指针进行混合类型赋值。换句话说,我们想允许这样:

Animal *pAnimal1 = &liz1;

Animal *pAnimal2 = &liz2;

...

*pAnimal1 = *pAnimal2;                 // assign a lizard to a lizard

而想禁止这样:

Animal *pAnimal1 = &liz;

Animal *pAnimal2 = &chick;

...

*pAnimal1 = *pAnimal2;                 // assign a chicken to a lizard

只能在运行期区分它们,因为将*pAnimal2赋给*pAnimal1有时是正确的,有时不是。我们于是陷入了基类型运行期错误的黑暗世界中。尤其是,我们需要在混合类型赋值时指出在operator=内部发生了错误,而类型相同时,我们期望按通常的方式完成赋值。

我们可以使用dynamic_cast(见Item M2)来实现。下面是怎么实现Lizard的赋值操作:

Lizard& Lizard::operator=(const Animal& rhs)

{

// make sure rhs is really a lizard

const Lizard& rhs_liz = dynamic_cast<const Lizard&>(rhs);

proceed with a normal assignment of rhs_liz to *this;

}

这个函数只在rhs确实是Lizard类型时将它赋给*this。如果rhs不是Lizard类型,函数传递出dynamic_cast转换失败时抛的bad_cast类型的异常。(实际上,异常的类型是std::bad_cast,因为标准运行库的组成部分,包括它们抛出的异常,都位于命名空间std中。对于标准运行库的概述,见Item E49和Item M35)。

即使不在乎有异常,这个函数看起来也是没必要的复杂和昂贵--dynamic_cast必要引用一个type_info结构;见Item M24--因为通常情况下都是一个Lizard对象赋给另一个:

Lizard liz1, liz2;

...

liz1 = liz2;                           // no need to perform a

// dynamic_cast: this

// assignment must be valid

我们可以处理这种情况而无需增加复杂度或花费dynamic_cast,只要在Lizard中增加一个通常形式的赋值操作:

class Lizard: public Animal {

public:

virtual Lizard& operator=(const Animal& rhs);

Lizard& operator=(const Lizard& rhs);           // add this

...

};

Lizard liz1, liz2;

...

liz1 = liz2;                                     // calls operator= taking

// a const Lizard&

Animal *pAnimal1 = &liz1;

Animal *pAnimal2 = &liz2;

...

*pAnimal1 = *pAnimal2;                          // calls operator= taking

// a const Animal&

实际上,给出了后面那个的operator=,也就简化了前者的实现:

Lizard& Lizard::operator=(const Animal& rhs)

{

return operator

}

现在这个函数试图将rhs转换为一个Lizard。如果转换成功,通常的赋值操作被调用;否则,一个bad_cast异常被抛出。

说实话,在运行期使用dynamic_cast进行类型检测,这令我很紧张。有一件事要注意,一些编译器仍然没有支持dynamic_cast,所以使用它的代码虽然理论上具有可移植性,实际上不一定。更重要的是,它要求使用Lizard和Chicken的用户必须在每次赋值操作时都准备好捕获bad_cast异常并作相应处理。如果他们没有这么做的话,那么不清楚我们得到的好处是否超过最初的方案。

指出了这个关于虚赋值操作的令人非常不满意的状态后,在最开始的地方重新整理以试图找到一个方法来阻止用户写出有问题的赋值语句是有必要的。如果这样的赋值语句在编译期被拒绝,我们就不用担心它们做错事了。

最容易的方法是在Animal中将operator=置为private。于是,Lizard对象可以赋值给Lizard对象,Chicken对象可以赋值给Chicken对象,但部分或混合类型赋值被禁止:

class Animal {

private:

Animal& operator=(const Animal& rhs);               // this is now

...                                                 // private

};

class Lizard: public Animal {

public:

Lizard& operator=(const Lizard& rhs);

...

};

class Chicken: public Animal {

public:

Chicken& operator=(const Chicken& rhs);

...

};

Lizard liz1, liz2;

...

liz1 = liz2;                                    // fine

Chicken chick1, chick2;

...

chick1 = chick2;                                // also fine

Animal *pAnimal1 = &liz1;

Animal *pAnimal2 = &chick1;

...

*pAnimal1 = *pAnimal2;                          // error! attempt to call

// private Animal::operator=

不幸的是,Animal也是实体类,这个方法同时将Animal对象间的赋值评定为非法了:

Animal animal1, animal2;

...

animal1 = animal2;                              // error! attempt to call

// private Animal::operator=

而且,它也使得不可能正确实现Lizard和Chicken类的赋值操作,因为派生类的赋值操作函数有责任调用其基类的赋值操作函数:

Lizard& Lizard::operator=(const Lizard& rhs)

{

if (this == &rhs) return *this;

Animal::operator=(rhs);                       // error! attempt to call

// private function. But

// Lizard::operator= must

// call this function to

...                                           // assign the Animal parts

}                                               // of *this!

后面这个问题可以通过将Animal::operator=申明为protected来解决,但“允许Animal对象间的赋值而阻止Lizard和Chicken对象通过Animal的指针进行部分赋值”的两难问题仍然存在。程序该怎么办?

最容易的事情是排除Animal对象间赋值的需求,其最容易的实现方法是将Animal设计为抽象类。作为抽象类,Animal不能被实例化,所以也就没有了Animal对象间赋值的需求了。当然,这导致了一个新问题,因为我们最初的设计表明Animal对象是必须的。有一个很容易的解决方法:不用将Animal设为抽象类,我们创一个新类--叫AbstractAnimal--来包含Animal、Lizard、Chikcen的共有属性,并把它设为抽象类。然后将每个实体类从AbstractAnimal继承。修改后的继承体系是这样的:

类的定义是:

class AbstractAnimal {

protected:

AbstractAnimal& operator=(const AbstractAnimal& rhs);

public:

virtual ~AbstractAnimal() = 0;                     // see below

...

};

class Animal: public AbstractAnimal {

public:

Animal& operator=(const Animal& rhs);

...

};

class Lizard: public AbstractAnimal {

public:

Lizard& operator=(const Lizard& rhs);

...

};

class Chicken: public AbstractAnimal {

public:

Chicken& operator=(const Chicken& rhs);

...

};

这个设计给你所以你需要的东西。同类型间的赋值被允许,部分赋值或不同类型间的赋值被禁止;派生类的赋值操作函数可以调用基类的赋值操作函数。此外,所有涉及Aniaml、Lizard或Chicken类的代码都不需要修改,因为这些类仍然操作,其行为与引入AbstractAnimal前保持了一致。肯定,这些代码需要重新编译,但这是为获得“确保了编译通过的赋值语句的行为是正确的而行为可能不正确的赋值语句不能编译通过”所付出的很小的代价。

要使得这一切工作,AbstractAnimal类必须是抽象类--它必须至少有一个纯虚函数。大部分情况下,带一个这样的函数是没问题的,但在极少见的情况下,你会发现需要创一个如AbstractAnimal这样的类,没有哪个成员函数是自然的纯虚函数。此时,传统方法是将析构函数申明为纯虚函数;这也是上面所采用的。为了支持多态,基类总需要虚析构函数(见Item 14),将它再多设为纯虚的唯一麻烦就是必须在类的定义之外实现它(例子见P195,Item M29)。

(如果实现一个纯虚函数的想法冲击了你,你只是知识不够开阔。申明一个函数为虚并不意味着它没有实现,它意味着:

l         当前类是抽象类

l         任何从此类派生的实体类必须将此函数申明为一个“普通”的虚函数(也就是说,不能带“= 0”)

是的,绝大部分纯虚函数都没有实现,但纯虚析构函数是个特例。它们必须被实现,因为它们在派生类析构函数被调用时也将被调用。而且,它们经常执行有用的任务,诸如释放资源(见Item M9)或纪录消息。实现纯虚函数一般不常见,但对纯虚析构函数,它不只是常见,它是必须。)

你可能已经注意到这里讨论的通过基类指针进行赋值的问题是基于假设实体类(如Animal)有数据成员。如果它们没有数据成员,你可能指出,那么就不会有问题,从一个无数据的实体类派生新的实体类是安全的。

无数据而可以成为实体类的基类会两种可能:在将来,或者它可能有数据成员,或者它仍然没有。如果它将来可能有数据成员,你现在做的只是推迟问题的发生(直到数据成员被加入),你在用短利换长痛(参见Item M32)。如果这个基类真的不会有数据成员,那么它现在就该是抽象类,没有数据的实体类有什么用处?

用如AbstractAnimal这样的抽象基类替换如Animal这样的实体基类,其好处远比简单地使得operator=的行为易于了解。它也减少了你试图对数组使用多态的可能,这种行为的令人不愉快的后果解释于Item M3。然而,这个技巧最大的好处发生在设计的层次上,因为这种替换强迫你明确地认可有用处的抽象行为的实体。也就是说,它使得你为有用的原型(concept)创造了新的抽象类,即使你并不知道这个有用的原型的存在。

如果你有两个实体类C1和C2并且你喜欢C2公有继承自C1,你应该将两个类的继承层次改为三个类的继承层次,通过创造一个新的抽象类A并将C1和C2都从它继承:

这种修改的重要价值是强迫你确定抽象类A。很清楚,C1和C2有共性;这就是为什么它们用公有继承联系在一起的原因(见Item E35)。修改后,你必须确定这些共性到底是什么。而且,你必须用C++的类将这些共性组织起来,它将不再是模糊不清的东西了,它到达了一个抽象类型的层次,有明确定义的成员函数和明确定义的语义。

这一切导致了一些令人不安的思考。毕竟,每个类都完成了某些类型的抽象,我们不应该在此继承体系中创造两个类来针对每个原型吗(一个是抽象类来表示其抽象部分(to embody the abstract part of the abstraction) ,一个是实体类来表示对象生成部分(to embody the object-generation part of the abstraction))?不应该。如果你这么做了,将使得继承体系中有太多的类。这样的继承体系是难以理解的,难以维护的,编译的代价很昂贵。这不是面向对象设计的目的。

其目的是:确认有用的抽象,并强迫它们(并且只有它们)放入如抽象类这样的实体。但怎么确认有用的抽象?谁知道什么抽象在将来被证明有用?谁能预知他将来要从什么进行继承?

好了,我不知道怎么预知一个继承体系将来的用处,但我知道一件事:在一个地方需要的抽象可能只是凑巧,但多处地方需要的抽象通常是有意义的。那么,有用的抽象就是那些被多处需要的抽象。也就是说,它们相当于是这样的类:就它们自己而言是有用的(比如,有这种类型的对象是用处的),并且它们对于一个或多个派生类也是有用处的。

在一个原型第一次被需要时,我们无法证明同时创造一个抽象类(为了这个原型)和一个实体类(为了原型对应的对象)是正确的,但第二次需要时,我们就能够这么做是正确的。我描述过的修改简单地实现了这个过程,并且在这么做的过程中强迫设计着和程序员明确表达那些有用的抽象,即使他们不知道那些有用的原型。 这也碰巧使得构建正确的赋值行为很容易。

让我们看一下一个简单的例子。假设你正在编制一个程序以处理局域网上计算机间的移动信息,通过将它拆为数据包并根据某种协议进行传输。我们认为应该用类来表示这些数据数据包,并且这些数据包是程序的核心。

假设你处理的只有一种传输协议,也只有一种包。也许你听说了其它协议和数据包类型的存在,但还从未支持它们,也没有任何计划以在未来支持它们。你会为数据包(for the concept that a packet represents) 既设计一个抽象类吗,又设计一个你实际使用的实体类?如果你这么做了,你可以在以后增加新的数据包而不用改变基类。这使得你增加新的数据包类型时程序不用重新编译。但这种设计需要两个类,而你现在只需要一个(针对于你现在使用的特殊数据包类型)。这值得吗,增加设计的复杂度以允许扩充特性,而这种扩充可能从不发生?

这儿没有肯定正确的选择,但经验显示:为我们还不完全了解的原型设计优秀的类几乎是不可能的。如果你为数据包设计了抽象类,你怎么保证它正确,尤其是在你的经验只局限于这唯一的数据包类型时?记住,只有在设计出的类能被将来的类从它继承而不需要它作任何修改时,你才能从数据包的抽象类中获得好处。(如果它需要被修改,你不得不重新编译所有使用数据包类的代码,你没得到任何好处。)

看起来不太能够设计出一个领人满意的抽象设计包类,除非你精通各种数据包的区别以及它们相应的使用环境。鉴于你有限的经验,我建议不要定义抽象类,等到以后需要从实体类继承时再加。

我所说的转换方法是一个判断是否需要抽象类的方法,但不是唯有的方法。还有很多其它的好方法;讲述面向对象分析的书籍上满是这类方法。“当发现需求从一个实体类派生出另外一个实体类时”,这也不是唯一需要引入抽象类的地方。不管怎么说啦,需要通过公有继承将两个实体类联系起来,通常表示需要一个新的抽象类。

这种情况是如此常见,所以引起了我们的深思。第三方的C++类库越来越多,当发现你需要从类库中的实体类派生出一个新的实体类,而这个库你只有只读权时,你要怎么做?

你不能修改类库以加入一个新的抽象类,所以你的选择将很有限、很无趣:

l         从已存在的实体类派生出你的实体类,并容忍我们在本Item开始时说到的赋值问题。你还要注意在Item M3中说过的数组问题。

l         试图在类库的继承树的更高处找到一个完成了你所需的大部分功能的抽象类,从它进行继承。当然,可能没有合适的类;即使有,你可能不得不重复很多已经在(你试图扩展的)实体类中实现了的东西。

l         用包容你试图继承的类的方法来实现你的新类(见Item E40和Item E42)。例下例,你将一个类库中的类的对象为数据成员,并在你的类中重实现它的接口:

class Window {                      // this is the library class

public:

virtual void resize(int newWidth, int newHeight);

virtual void repaint() const;

int width() const;

int height() const;

};

class SpecialWindow {               // this is the class you

public:                             // wanted to have inherit

...                               // from Window

// pass-through implementations of nonvirtual functions

int width() const { return w.width(); }

int height() const { return w.height(); }

// new implementations of "inherited" virtual functions

virtual void resize(int newWidth, int newHeight);

virtual void repaint() const;

private:

Window w;

};

这种方法需要你在类库每次升级时也要更新你自己的类。它还需要你放弃重定义类库中的类的虚函数的能力,因为你用的不是继承。

l         使用你得到。使用类库中的类,而将你自己的程序修改得那个类适用。用非成员函数来提供扩展功能(那些你想加入那个类而没有做到的)。结果,程序将不如你所期望中的清晰、高效、可维护、可扩展,但至少它完成了你所需要的功能。

这些选择都不怎么吸引人,所以你不得不作出判断并选择最轻的毒药。这不怎么有趣,但生活有时就是这样。想让事情在以后对你自己(和我们其它人)容易些,将问题反馈给类库生产商。靠运气(以及大量的用户反馈),随时间的流逝,那些设计可能被改进。

最后,一般的规则是:非尾端类应该是抽象类。在处理外来的类库时,你可能需要违背这个规则;但对于你能控制的代码,遵守它可以提高程序的可靠性、健壮性、可读性、可扩展性。

More Effective C++之 Item M33:将非尾端类设计为抽象类相关推荐

  1. 条款M33:将非尾端类设计为抽象类:将抽象类赋值运算符函数设为protected

    1.常遇到的代码问题: class Animal { //抽象类 public: Animal& operator=(const Animal& rhs); virtual ~Anim ...

  2. 【M33】将非尾端类设计为抽象类

    1.考虑下面的需求,软件处理动物,Cat与Dog需要特殊处理,因此,设计Cat和Dog继承Animal.Animal有copy赋值(不是虚方法),Cat和Dog也有copy赋值.考虑下面的情况: Ca ...

  3. 建议收藏!全面梳理非交易类平台产品设计原则

    琳琅满目,光彩照人.这是现在商品世界的写照. 产能过剩,超额满足.这是当下社会现状的真实. 平台型产品,作为互联网的中坚力量,是我们普通用户透过网络触摸商品和社会的媒介. 可以说,每家公司都有一颗做平 ...

  4. Effective Java (3rd Editin) 读书笔记:3 类和接口

    3 类和接口 Item 15:最小化类和成员的访问权限 一个设计优秀的类应该隐藏它的所有实现细节,将它的 API 和内部实现干净地分离开.这种软件设计的基本准则被称为"封装"(en ...

  5. c语言 如何读多种数据类型 非类,c语言程序设计教学大纲(非电气类)文档.doc

    c语言程序设计教学大纲(非电气类)文档 <C语言程序设计>课程教学大纲 主任 教研室主任 大纲执笔人 姜长洪 王海荣 C语言备课组 一.课程基本信息 课程编号:×××× 课程名称:C语言程 ...

  6. 非标自动化企业前十名_非标自动化设计:非标自动化是如何被称做企业里的血液?...

    非标机械设计,就是根据客户提供的样板或者提出的要求来订做设计的.相信还有很多人对这个词感到很陌生,提起来也只是大概知道它是一种什么东西,那么接下来,小编就来为您简单的科普一下,非标机械设计都有哪些特点 ...

  7. 2021年 第13届 全国大学生数学竞赛 初赛(非数学类)试题详细解答

    [2020年第12届全国大学生数学竞赛--资源分享 ][1~11届省赛决赛考题及题解(数学类.非数学类).推荐学习网址.复习备考书籍推荐] 2019年 第11届 全国大学生数学竞赛 初赛(非数学类)试 ...

  8. 试验设计茆诗松电子版_非标机械设计有哪些设计过程?

    推荐阅读:机械设计工程师技术成长之路(连载9)外企机械工程师的二十年职业感悟机械设计工程师--设计能力从何而来?完整版<机械工程师生存现状解析>看懂机械设计流程,你也可以成为一名合格的机械 ...

  9. bmp怎么编辑底色_非标机械设计这个行业前景怎么样

    今天就不分享技术点了,主要和大家谈谈非标机械设计这个行业的前景怎么样,非标机械设 计,就是根据客户提供的样板或者提出的要求来订做设计的.相信还有很多人对这个词感到很 陌生,提起来也只是大概知道它是一种 ...

最新文章

  1. jQuery插件定义
  2. 从零开始学前端:列表标签 --- 今天你学习了吗?(CSS:Day06)
  3. oracle数据库6月之后的数据,Oracle数据库SCN存在可能在2019年6月导致宕机问题
  4. 通过命令行创建MAVEN多模块项目
  5. 一级计算机2016难度,2016年计算机等级一级考前必看
  6. [转]从网页Web上调用本地应用程序(.jar、.exe)的主流处理方法
  7. Hibernate 验证版本不兼容问题
  8. 2017-2018-2 20165314 实验三《 敏捷开发与XP实践》实验报告
  9. 手机app系统软件开发报价单及方案:费用明细
  10. 360os比android,手机系统比拼360OS、Flyme究竟哪个好?
  11. 网络安全工程师毕业答辩杂记
  12. 出租车计费程序php,出租车计价器VHDL程序
  13. 甲骨文裁员是在为云业务转型太慢埋单
  14. [项目管理] BOT运作模式
  15. 爬虫--雪球网爬取(requests 和 request 的两种方法)
  16. 公司新加了一台友宝自动售货机引发的思考-适配器模式
  17. 华为新平板鸿蒙,华为新平板将发布,搭载鸿蒙2.0系统
  18. html颜色参考 速查 在线取色,Color by Fardos - 配色/取色插件
  19. c语言库函数大全文库,c语言常用的库函数_相关文章专题_写写帮文库
  20. 启用数据空间:让VirtualBox虚拟机中的Ubuntu 10.10和XP主机互通有无

热门文章

  1. Unity3D 1D动画行为混合树 第三人称人物控制器
  2. moment安装以及基本用法
  3. FFmpeg倒放视频
  4. 2017软考信息安全工程师通过了,立贴小庆贺下
  5. 信息搜集:SHODAN API 参考
  6. c语言.jpg图片转成数组_PDF文件转JPG等图片格式的小工具
  7. 计算机初级证书怎么考
  8. 快讯:每日优鲜回应3天关闭9城业务;名创优品将在港交所上市
  9. 使用Python的tkinter模块实现界面化的批量修改文件名(续)
  10. 趣头条——前区块链时代一次不成功的实验