公有继承

来源:互联网 发布:qq三国js技能 编辑:程序博客网 时间:2024/03/29 19:25

C++面向对象编程中一条重要的规则是:公有继承意味着 "是一个" 。一定要牢牢记住这条规则。

  当写下类D("Derived" )从类B("Base")公有继承时,实际上是在告诉编译器:类型D的每一个对象也是类型B的一个对象,但反之不成立;即:B表示一个比D更广泛的概念,D表示一个比B更特定概念;就是在声明:任何可以使用类型B的对象的地方,类型D的对象也可以使用,因为每个类型D的对象是一个类型B的对象。相反,如果需要一个类型D的对象,类型B的对象就不行:每个D "是一个" B, 但反之不成立。

C++采用了公有继承的上述解释。看这个例子:
class Person { ... };
class Student: public Person { ... };

  每个学生是人,但并非每个人是学生。这正是上面的层次结构所声明的。我们希望,任何对 "人" 成立的事实 ---- 如都有生日 ----也对 "学生" 成立;但不希望,任何对 "学生" 成立的事实 ---- 如都在某一学校上学 ----也对 "人" 成立。人的概念比学生的概念更广泛;学生是一种特定类型的人。

  在C++世界中,任何一个其参数为Person类型的函数(或Person的指针或Person的引用)可以实际取一个Student对象(或Student的指针或Student的引用):
void dance(const Person& p);        // 任何人可以跳舞
void study(const Student& s);       // 只有学生才学习
Person p;                           // p是一个人
Student s;                          // s是一个学生
dance(p);                           // 正确,p是一个人
dance(s);                           // 正确,s是一个学生,一个学生"是一个"人
study(s);                           // 正确
study(p);                           // 错误! p不是学生

  只是公有继承才会这样。就是说,只是Student公有继承于Person时,C++的行为才会象所描述的那样。私有继承则是完全另外一回事,至于保护继承,好象没有人知道它是什么含义。另外,Student "是一个" Person的事实并不说明Student的数组 "是一个" Person数组。

  公有继承和 "是一个" 的等价关系听起来简单,但在实际应用中,可能不会总是那么直观。有时直觉会误导。例如,有这样一个事实:企鹅是鸟;还有这样一个事实:鸟会飞。如果想简单地在C++中表达这些事实,我们会这样做:
class Bird {
public:
  virtual void fly();               // 鸟会飞
  ...
};
class Penguin:public Bird {      // 企鹅是鸟
  ...
};

  突然间陷入困惑,因为这种层次关系意味着企鹅会飞,而我们知道这不是事实。造成这种情况,是因为使用的语言(汉语)不严密。说鸟会飞,并不是说所有的鸟会飞,通常,只有那些有飞行能力的鸟才会飞。如果更精确一点,我们都知道,实际上有很多种不会飞的鸟,所以我们会提供下面这样的层次结构,它更好地反映了现实:
class Bird {
  ...                   // 没有声明fly函数
};
class FlyingBird: public Bird {
public:
  virtual void fly();
  ...
};
class NonFlyingBird: public Bird {
  ...                  //  没有声明fly函数
};
class Penguin: public NonFlyingBird {
  ...                  //  没有声明fly函数
};

  这种层次就比最初的设计更忠于我们所知道的现实。但关于鸟类问题的讨论,现在还不能完全结束。因为在有的软件系统中,说企鹅是鸟是完全合适的。比如说,如果程序只和鸟的嘴、翅膀有关系而不涉及到飞,最初的设计就很合适。这看起来可能很令人恼火,但它反映了一个简单的事实:没有任何一种设计可以理想到适用于任何软件。好的设计是和软件系统现在和将来所要完成的功能密不可分的。如果程序不涉及到飞,并且将来也不会,那么让Penguin派生于Bird就会是非常合理的设计。实际上,它会比那个区分会飞和不会飞的设计还要好,因为设计中不会用到这种区分。在设计层次中增加多余的类是一种很糟糕的设计,就象在类之间制定了错误的继承关系一样。

  对于 "所有鸟都会飞,企鹅是鸟,企鹅不会飞" 这一问题,还可以考虑用另外一种方法来处理。也就是对penguin重新定义fly函数,使之产生一个运行时错误:
void error(const string& msg);      // 在别处定义
class Penguin: public Bird {
public:
  virtual void fly() { error("Penguins can't fly!"); }
  ...
};

  解释型语言如Smalltalk喜欢采用这种方法,但这里要认识到的重要一点是,上面的代码所说的可能和所想的是完全不同的两回事。它不是说,"企鹅不会飞",而是说,"企鹅会飞,但让它们飞是一种错误"。怎么区分二者的不同?这可以从检测到错误发生的时间来区分。"企鹅不会飞" 的指令是由编译器发出的,"让企鹅飞是一种错误" 只能在运行时检测到。为了表示 "企鹅不会飞" 这一事实,就不要在Penguin对象中定义fly函数:
class Bird {
  ...                          // 没有声明fly函数
                             
};
class NonFlyingBird: public Bird {
  ...                          // 没有声明fly函数
                              
};
class Penguin: public NonFlyingBird {
  ...                          // 没有声明fly函数
                              
};
如果想使企鹅飞,编译器就会谴责你的违规行为:
Penguin p;
p.fly();                       // 错误!

  用Smalltalk的方法得到的行为和这完全不同。用那种方法,编译器连半句话都不会说。

  C++的处理方法和Smalltalk的处理方法有着根本的不同,所以只要是在用C++编程,就要采用C++的方法做事。另外,在编译时检测错误比在运行时检测错误有某些技术上的优点。

类Square(正方形)可以从类Rectangle(矩形)公有继承吗?
        Rectangle
              ^
              | ?
          Square
每个人都知道一个正方形是一个矩形,但反过来通常不成立。 确实如此吗?看看下面的代码:
class Rectangle {
public:
  virtual void setHeight(int newHeight);
  virtual void setWidth(int newWidth);
  virtual int height() const;          // 返回当前值
  virtual int width() const;           // 返回当前值

  ...
};
void makeBigger(Rectangle& r)          // 增加r面积的函数
{                                     
  int oldHeight = r.height();
  r.setWidth(r.width() + 10);          // 对r的宽度增加10
  assert(r.height() == oldHeight);     // 断言r的高度未变
}                                    
很明显,断言永远不会失败。makeBigger只是改变了r的宽度,高度从没被修改过。
现在看下面的代码,它采用了公有继承,使得正方形可以被当作矩形来处理:
class Square: public Rectangle { ... };
Square s;
...
assert(s.width() == s.height());      // 这对所有正方形都成立

makeBigger(s);                        // 通过继承,s "是一个" 矩形
                                      // 所以可以增加它的面积
                                     
assert(s.width() == s.height());      // 这还是对所有正方形成立

  很明显,和前面的断言一样,后面的这个断言也永远不会失败。因为根据定义,正方形的宽和高应该相等。那么现在有一个问题。我们怎么协调下面的断言呢?
· 调用makeBigger前,s的宽和高相等;
· makeBigger内部,s的宽度被改变,高度未变;
· 从makeBigger返回后,s的高度又和宽度相等。(注意s是通过引用传给makeBigger的,所以makeBigger修改了s本身,而不是s的拷贝)
  最根本的问题在于:对矩形适用的规则(宽度的改变和高度没关系)不适用于正方形(宽度和高度必须相同)。但公有继承声称:对基类对象适用的任何东西 ---- 任何!---- 也适用于派生类对象。在矩形和正方形的例子中,所声称的原则不适用,所以用公有继承来表示它们的关系只会是错误。当然,编译器不会阻拦这样做,但正如我们所看到的,它不能保证程序可以工作正常。正如每个程序员都知道的,代码通过编译并不说明它能正常工作。

   "是一个" 的关系不是存在于类之间的唯一关系。类之间常见的另两个关系是 "有一个" 和 "用...来实现"。