***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************
六、继承与面向对象设计
six、Inheritance and Object-Oriented Design
面向对象编程(OOP)几乎流行了两个年代,即使你过去只用C编程,现在也没办法逃脱这个趋势。
本章主要看 C++的OOP与你可能习惯的OOP的不同:
> "继承" 可以使单一继承或多重继承
> 每一个继承连接(link)可以是public,protected or private,也可以是 virtual or non-virtual
> 成员函数的各个选项:virtual?non-virtual?pure virtual?以及成员函数和其他语言特性的交互影响:缺省参数值与virtual函数有什么交互影响。
> 还有一些其他的,比如:继承如何影响C++的名称查找规则?设计选项有哪些?如果class的行为需要修改,virtual函数是最佳选择吗?
除了这些,本章还会解释C++各种不同特性的真正意义。比如:
> "public继承" 意味 "is-a"
> virtual函数 意味 "接口必须被继承";non-virtual函数 意味 "接口和实现都必须被继承"
等等
条款 32:确定你的public继承塑模出is-a关系
Rule 32:Make sure public inheritance models "is-a"
1.public继承 与 is-a
以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味"is-a"的关系
如果你令 class D("Derived")以public形式继承 class B("Base"),你便是告诉C++编译器(亦或是看你代码的人)——每一个类型为D的对象同时也是一个类型为B的对象,反之则不成立。
例如:
class Person { ... }; class Student : public Person { ... };
上面的代码说明——每个学生都是人,但并非每个人都是学生。这就是is-a。
在C++领域中,任何函数如果期望获得一个类型为Person(或pointer-to-Person或reference-to-Person)的实参,都也愿意接受一个Student对象(或pointer-to-Student或reference-to-Student):
void eat(const Person& p); // 任何人都会吃 void study(const Student& s); // 只有学生才到校学习 Person p; // p是人 Student s; // s是学生 eat(p); // ok,p是人,可以执行吃这个动作 eat(s); // ok,s是学生,根据is-a,可以执行吃这个动作 study(s); // ok,s是学生,可以执行到校学习这个动作 study(p); // no! p是人,并非每个人都可以执行到校学习这个动作
>这个论点只对public继承才成立<
对于private继承,会完全不同,详见条款39,至于protected继承,作者也没搞太明白。。。
2.产生的各种问题
>1 public继承和is-a之间的等价关系听起来非常简单,但有时候可能被误导,比如:企鹅是一种鸟,鸟可以飞,但如果我们用这样的形式来描述这种关系:
class Bird { public: virtual void fly(); // 鸟可以飞 ... }; class Penguin : public Bird { // 企鹅是一种鸟 ... };
Boom!发生了什么事情?我们的三观何在。。。
显然,这样是错误的,我们不得不承认有数种鸟不会飞。
但如何反应我们的意思呢?
① 就像下面这样的继承体系,更能准确反映出我们的意思:
class Bird { ... // 没有声明fly函数 }; class FlyingBird : public Bird { public: virtual void fly(); ... }; class Penguin : public Bird { ... };
但是,此时这件事就结束了吗?No no no,因为对某些软件系统而言,可能不需要区分会飞的鸟和不会飞的鸟。假如,你的程序对鸟喙和鸟翅更加感兴趣,完全不在乎飞行,那么刚开始的"双class继承体系"或许就可以满足了。
> 这实际上是说明一个事实——世界上并不存在一个"适用于所有软件"的完美设计;所谓的最佳设计,取决于系统希望做什么事。 <
② 对于更加准确反映我们的想法,还有一种方法。为企鹅重新定义fly函数,令它产生一个运行期错误。
void error(const std::string& msg); class Penguin : public Bird { public: virtual void fly() { error("Attempt to make a penguin fly!"); } ... };
这里所说的某些东西可能和你所想的不同,这里并不是说“企鹅不会飞”,而是说“企鹅会飞,但尝试那么做是一种错误”!
③两者的差异,从错误被侦测出来的时间点来看:“企鹅不会飞”这一限制可由编译期强制实施,但若违反了“企鹅会飞,但尝试那么做是错误的”这条规则,只有运行期才能检测出来。
>2 class Square 应该以 public继承 class Rectangle 吗?
当然,学校是这么说的——正方形是一种特殊的长方形
但是,看看下面这段代码:
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) { int oldHeight = r.height(); r.setWidth(r.width()+10); // 为r的宽度加10 assert(r.height()==oldHeight); // 判断r的高度是否未曾改变 }
显然,上面的 assert结果永远为真。因为makeBigger只改变了r的宽度,r的高度从未被改变。
然后:
class Square : public Rectangle { ... }; Square s; ... assert(s.width()==s.height()); // 这对于所有的正方形一定为真 makeBigger(s); // 由于public继承,s是一种(is-a)矩形 assert(s.width()==s.height()); // 对所有的正方形应该仍然为真
这也很明显,第二个assert结果也应该永远为真。因为根据定义,正方形的高度和宽度相同。
But,我们如何调节下面这些assert判断式?
? 调用makeBigger之前,s的高度和宽度相同
? 在makeBigger函数内,s的宽度改变,但高度不变
? makeBigger返回之后,s的高度再度和其宽度相同。(注意,s是以by reference方式传给makeBigger,所以makeBigger修改的是s自身,不是s的副本)
其实,本例的根本困难是,某些可施行与矩形身上的事(例如宽度可独立于其高度被外界修改)却不可实行于正方形身上(宽度总应该与高度一样)。
但是,public继承主张,能够实行于base class对象身上的每件事情,每件事情也同时可以实行于derived class身上。
还有最最重要的一点:☆ 代码通过编译并不代表可以正常的运作 ☆
3.最后
is-a并非是唯一存在于class之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-in-terms-of(根据实物实现出)。
将上述这些重要的相互关系中的任何一个误塑为is-a而造成的错误设计,在C++中并不罕见,所以你应该确定你确实了解这些个"class相互关系"之间的差异,并知道如何在C++中最好的塑造它们。
★请记住★
? "public继承"意味着is-a。适用于base class身上的每一件事情一定也适用于derived class身上,因为每一个derived class对象也都是一个base class对象。
***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************