一、“is-a”的概念
- 以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味“is-a”(是一种)的关系
- 如果你令class D以public形式继承class B,你便是告诉编译器:
- 每一个类型为D的对象同时也是一个类型为B的对象。反之不是
- B对象可使用的地方,D对象一样可以使用。反之不是
演示案例
- 下面Student类public继承于Person
class Person {}; class Student :public Person {};
- 任何获得类型为Person(pointer-to-Person或reference-to-Person)的实参,都可以接受一个Student(pointer-to-Student或reference-to-Student)对象
- 例如:
void eat(const Person& p); void study(const Student& s); int main() { Person p; Student s; eat(p); //正确 eat(s); //正确 study(s); //正确 study(p); //错误 return 0; }
- 上面的规则只对public继承才成立。private、protected不成立
二、设计正确的继承模型
- 鸟可以飞,企鹅也是一种鸟。于是我们可能设计下面错误的继承模型:
- 企鹅虽然属于鸟类,但是企鹅不会飞
- 设计中,我们错误的将鸟类中的fly()虚函数派生给了Penguin类
//鸟类
class Bird {
public:
virtual void fly();
};
//企鹅,也继承了fly()虚函数
class Penguin :public Bird {};
- 我们应该修改上面的代码,下面才是合适的模型:
//鸟类
class Bird {
//无fly()函数
};
//会飞的鸟类
class FlyingBird :public Bird {
public:
virtual void fly();
};
//企鹅不会飞
class Penguin :public Bird {
};
三、以“编译期”确认关系代替“运行期”确认关系
- 紧接着上面鸟类与企鹅的问题
- 企鹅不会飞,但是我们仍然让Bird定义fly()函数,然后让Penguin继承于Bird,与上面不同的是,我们让Penguin在执行fly()函数的时候报出一个错误(运行期执行)。代码如下:
class Bird {
public:
virtual void fly();
};
void error(const std::string& msg);
class Penguin :public Bird {
public:
virtual void fly() {
error("Attempt to make a penguin fly!");
}
};
- 上面的代码是在运行期检查这种错误的
- 下面我们设计让编译器在编译的时候检查出企鹅不会飞这种错误。代码如下:
class Bird {
//无fly()函数
};
class Penguin :public Bird {
//...
};
Penguin p;
p.fly();
- 总结:
- 上面我们介绍了检测“企鹅不会飞”这种错误的两种方式。一种为在运行期检测,一种为在编译期检测
- 当然,我们希望在编译期的时候就确定企鹅不会飞这种关系,因此更希望以第二种方式(编译期)代替第一种方式(运行期)来设计继承关系
四、is-a模型的一些例外
考虑这样一个演示案例
- 我们让正方形类(Square)public继承于矩形类(Rectangle)。如下所示:
- 矩形类的代码定义如下:
class Rectangle { public: virtual void setHeight(int newHeight); //设置高 virtual void setWidth(int newWidth); //设置宽 virtual void height()const; //返回高 virtual void width()const; //返回宽 };
- 现在有下面这个函数,下面函数中的assert永远为真,因为函数只改变了宽度,而没有改变高度:
//这个函数用来增加r的面积 void makeBigger(Rectangle& r) { int oldHeight = r.height(); //取得旧高度 r.setWidth(r.width() + 10); //设置新宽度 assert(r.height() == oldHeight); //判断高度是否改变 }
- 正方形类的代码定义如下:
class Square :public Rectangle { //... };
- 现在有下面的代码:
Square s; //正方形类 //... assert(s.width() == s.height()); //永远为真,因为正方形的宽和高相同 makeBigger(s); //由于继承,我们可以增加正方形的面积 //... assert(s.width() == s.height()); //对所有正方形来说,应该还是为真
- 现在考虑上面的代码:
- 第一步,我们判断正方形的宽和高,根据原理,assert应该返回真
- 第二步,调用makeBigger()函数,这个函数改变了宽度,而没有改变高度
- 第三步,再次调用assert应该还是返回真,因为此处的s为正方形
- 现在我们可以看到:
- 前面我们虽然提到过,作用于基类的代码,使用派生类也可以执行
- 但是此处我们可以看到,某些施行于矩形类中的代码(例如只改变宽度而不改变高度),在正方形中却不可以实施(因为正方形的宽度和高度应该保持一致)
- is-a并非是唯一存在于classes之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-terms-of(根据某物实现出)。这些关系将在条款38和39介绍。在这些相互关系的塑造为is-a会造成错误设计
五、总结
-
“public继承”意味着is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象