C++的继承(一): 让蟋蟀继承蚱蜢

Scott Mayer 的那本《Effective C++》第二版刚出来时,我碰巧在书店看到了它。这是我看到的第一本讲C++讲的比较清楚的书。买回来了之后,我很欣喜,随着学习的深入,完全掌握C++似乎指日可待。

弹指20年过去了。奇怪的是对于这个C++语言我一直没有真正领会。"形"是知道很多了,但"神"是什么,一直不得要领。直到有一天开始怀疑《Effective C++》这本书。

《Effective C++》第二版写了一个蟋蟀和蚱蜢的例子。在条款40,谨慎的使用多重继承,他说,
… …
然而,必须当心诱惑。有时你会掉进这样的陷阱中:对某个需要改动的继承层次结构来说,本来用一个更基本的重新设计可以更好,但你却为了追求速度而去使用MI。例如,假设为可以活动的卡通角色设计一个类层次结构。至少从概念上来说,让各种角色能跳舞唱歌将很有意义,但每一种角色执行这些动作时方式都不一样。另外,跳舞唱歌的缺省行为是什么也不做。

所有这些用C++来表示就象这样:

class CartoonCharacter {
public:
virtual void dance() {}
virtual void sing() {}
};

虚函数自然地体现了这样的约束:唱歌跳舞对所有CartoonCharacter对象都有意义。什么也不做的缺省行为通过类中那些函数的空定义来表示(参见技巧36)。假设有一个特殊类型的卡通角色是蚱蜢,它以自己特殊的方式跳舞唱歌:

class Grasshopper: public CartoonCharacter {
public:
virtual void dance(); // 定义在别的什么地方
virtual void sing(); // 定义在别的什么地方
};

现在假设,在实现了Grasshopper类后,你又想为蟋蟀增加一个类:

class Cricket: public CartoonCharacter {
public:
virtual void dance();
virtual void sing();
};

当坐下来实现Cricket类时,你意识到,为Grasshopper类所写的很多代码可以重复使用。但这需要费点神,因为要到各处去找出蚱蜢和蟋蟀唱歌跳舞的不同之处。你猛然间想出了一个代码复用的好办法:你准备用Grasshopper类来实现Cricket类,你还准备使用虚函数以使Cricket类可以定制Grasshopper的行为。

你立即认识到这两个要求 ---- “用…来实现” 的关系,以及重新定义虚函数的能力 ---- 意味着Cricket必须从Grasshopper私有继承,但蟋蟀当然还是一个卡通角色,所以你通过同时从Grasshopper和CartoonCharacter继承来重新定义Cricket:

class Cricket: public CartoonCharacter,
private Grasshopper {
public:
virtual void dance();
virtual void sing();
};

然后准备对Grasshopper类做必要的修改。特别是,需要声明一些新的虚函数让Cricket重新定义:

class Grasshopper: public CartoonCharacter {
public:
virtual void dance();
virtual void sing();
protected:
virtual void danceCustomization1();
virtual void danceCustomization2();
virtual void singCustomization();
};

蚱蜢跳舞现在被定义成象这样:

void Grasshopper::dance()
{
执行共同的跳舞动作;
danceCustomization1();
执行更多共同的跳舞动作;
danceCustomization2();
执行最后共同的跳舞动作;
}

蚱蜢唱歌的设计与此类似。
很明显,Cricket类必须修改一下,因为它必须重新定义新的虚函数:

class Cricket:public CartoonCharacter,
private Grasshopper {
public:
virtual void dance() { Grasshopper::dance(); }
virtual void sing() { Grasshopper::sing(); }
protected:
virtual void danceCustomization1();
virtual void danceCustomization2();
virtual void singCustomization();
};

这看来很不错。当需要Cricket对象去跳舞时,它执行Grasshopper类中共同的dance代码,然后执行Cricket类中定制的dance代码,接着继续执行Grasshopper::dance中的代码,等等。

然而,这个设计中有个严重的缺陷,这就是,你不小心撞上了 “奥卡姆剃刀” ---- 任何一种奥卡姆剃刀都是有害的思想,William of Occam的尤其如此。奥卡姆者鼓吹:如果没有必要,就不要增加实体。现在的情况下,实体就是指的继承关系。如果你相信多继承比单继承更复杂的话(我希望你相信),Cricket类的设计就没必要复杂。
… …

这个例子深深地印在我的脑子里,影响了我很多年。当某一天自己忽然有所领悟的时候,再去找这本书,遍寻不得。原来的那本书早就送了人了。而网上几乎搜不到这个例子。《Effective C++》早已出了第三版。而在那个版本里,已经删了这个例子。就是说,让蟋蟀继承蚱蜢,这样用是可以的!

计算编程世界是不同的。每一个问题的解决,都需要工程师付出巨大的努力。把现实世界的问题,搬进计算编程世界的时候,势必要剪裁,提取最本质的特性,而使编程的开销最小化。这就是说编程世界中的对象,和现实世界的对象,已经大相径庭。

如果从一个先做好的class 开始,先做了一点扩展,而后又作了一点扩展,看起来是这样子,

class Base { … };

class SomeExtend: public Base { … };
class MoreExtend: public Some { …};

计算机程序里的名字不过是标识符,取个有意义的名字不过是提醒一下阅读程序的人。它们不是现实世界的对象。如果一味纠结究竟是class MoreExtend 继承 class SomeExtend,还是 class SomeExtend 继承 class MoreExtend 那就是十足的傻瓜。因为都可以。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值