Thinking in C++ (1-4)

 

富有多态机制的交互性对象

 

当你使用了层次化类型时,你经常需要通过基本类型层面考虑一个对象,而不是它的具体的类型。这一机制使得你的代码中的对象并不依赖于它的具体类型。在前面提到的形状的例子中,在没有指明这些形状是圆,正方形,还是三角形的前提下,函数可以针对泛化的形状进行操作。所有的形状可以被画出,擦除,或者移动,所以这些函数只是简单的对形状对象发送一个消息,这些函数不关心对象如何响应这些消息。

 

在这一机制下,添加新的类型并不会影响到原来的代码,而添加新类型便是面向对象编程中最为普遍的解决新问题的途径。比如说,你可以在不修改对泛化类型进行操作的函数的前提下,派生一个新的形状的子类型——五角星。这种通过派生新类来扩展程序的方法非常的重要,因为它显著的改进了设计的过程,从而降低了软件维护成本。

 

但是这里还存在一个问题,当尝试将派生类型的对象看作它们的基类型对象时(将圆看为形状,将自行车看为交通工具,将鹈鹕看为鸟类,等等),如果一个函数试图告诉一个一般的形状把他自己画出来,告诉一个一般的交通工具向前进,或者一只一般的鸟移动一下,此时编译器在编译时不可能精确地知道哪段代码会被运行。这就是问题所在——当消息发送出去了,程序员并不渴望知道哪段代码应该被执行,画图函数对圆,正方形,或者三角形同样适用,对象将会针对其具体类型运行适当的代码。如果你不需要知道哪段代码会被执行,那么当你添加一个新的类型时,在不对所调用的函数进行修改的前提下,这个新类型所运行的代码将可能与以前有所不同。因此我们说编译器不可能精确地知道哪段代码会被运行,那么该怎么办呢?举例说,在下边这一图表中的“鸟类控制器”这一对象只能针对一般的“鸟类”对象正常的工作,它不知道它们的精确的类型。从“鸟类控制器”的角度而言这样做是方便的,因为它不需要编写代码来确定鸟类的具体类型,或者是这只鸟的行为。当move() 函数被调用时,即便忽略鸟类的具体类型,对这只鸟仍能产生正确的行为(鹅会跑,会飞或者游泳,企鹅只会跑或者游泳),那么,这一现象是怎么发生的呢?

 

这个问题的答案是面向对象编程语言中最微妙的地方了:编译器不会以传统的方式进行函数调用。非OOP编译器所产生的函数调用会导致“前期绑定”,(这个术语也许你在之前没有听说过,因为那时候也没有别的绑定方法)“前期绑定”意味着编译器生成一个针对一个具体函数名称的调用,然后连接器把这一调用映射到需要执行的代码的绝对地址处。在OOP中,在程序运行之前,它不能确定要执行的代码的地址,所以在对一个泛化的对象发送消息时,必须存在另一个有效的方案。

 

为了解决这一问题,面向对象编程语言使用了“后期绑定(动态绑定)”的方法。当你对一个对象发送消息时,直到运行时才会决定调用哪一段代码。编译器可以确保函数存在,并且通过对参数和返回值的检查确保类型合法。(在弱类型语言中这一检查是不存在的),但是编译器并不知道当前哪段代码该运行。

 

为了进行后期绑定,C++ 编译器用一小段特殊的代码来代替直接定址表。这一小段代码使用对象中储存的信息计算出函数体的地址。(这一过程将会在15章详细讲到)于是,在这段代码中的目录中,每一个对象可以拥有不同的行为。当你对一个对象发出消息,这个对象将会找出针对这一消息它将做些什么。

 

你可以通过virtual关键字来说明你需要一个包含有后期绑定特性的函数。使用virtual时你并不需要了解它的机理,但是没有了它你便不能在C++ 中进行面向对象编程了。在C++ 中,你必须在适当的时候使用virtual因为,在默认情况下,成员函数并不是动态绑定的。虚函数使你可以描述在同一系列的类的行为之间的不同点。这些不同点便是多态行为的起源。

 

我们回到形状的例子。类的系列(所有基于同样的统一接口的类)曾在这一章的前边用图表说明过。为了演示多态性,我们需要编写一段独立的代码,这段代码应忽略类型具体的细节并且只与基类存在关系。这段代码与具体类的信息之间是分离的,于是编写和理解变得更加简单。同时,如果通过集成添加了一个新的类型(比如说六边形),你编写的那段代码仍然可以对六边形类型(它也是形状类型的)正常的工作。这样的程序便存在“可扩展性”。

 

如果你在C++ 中写了这样一个函数:(很快你就会知道该怎么做了)

 

  void doStuff(Shape&s) {

    s.erase();

    // ...

    s.draw();

  }

 

这个函数可以与任何形状类型(Shape)对象对话,所以说它独立于具体对象类型的画出和擦出操作(’&’的意思是取传给doStuff() 函数的对象的地址,但现在你可以不理解它的细节。这暂时还不重要)如果你在程序的其他什么地方使用doStuff() 函数:

 

  Circle c;

  Triangle t;

  Line l;

  doStuff(c);

  doStuff(t);

  doStuff(l);

 

doStuff() 函数的调用将会自动的正确,不管对象的具体类型是什么。

 

这的确是一个奇妙的魔术般的调用。看看这行代码:

 

  doStuff(c);

 

当一个圆(Circle)传入一个本来期望得到一个形状(Shape)类型的参数的函数时会发生些什么呢?由于一个圆“是一个”形状,所以它可以被duStuff() 识别为一个形状。也就是说,所有doStuff() 可以发送给形状的消息,圆都可以接受。所以说这样的代码是绝对安全的并且完全符合逻辑。

 

我们把“把一个派生类型看作它的基类型”这种做法称为“向上转型”(upcasting)。“转型”(cast)用来说明在不同的模型之间转换,说“向上”(up)是因为继承的图表的典型组织方法:把基类型放在最上边,派生类型在齐下边以扇形排布。于是我们说,转型到一个基类型是在继承图表中向上移动:“向上转型”。

 

一种面向对象的编程语言总会在某处包含向上转型,因为这使得一个对象可以在不了解它自身的具体类型时作出正确的判断。请观察doStuff() 的代码:

 

  s.erase();

  // ...

  s.draw();

 

请注意这并不是说“如果你是一个圆(Circle)请你做这个,如果你是一个正方形(Square)请你做那个,等等”,那样需要程序员对于每个形状(Shape)对象都要进行类型检查,每当你添加一个新的形状类型的时候都需要对进行修改,你能想象这样的设计有多么的糟糕。

 

doStuff() 让人激动的地方在于,不知不觉的它就正确的工作了。为Circle调用draw() 时执行的代码与Square或者Line调用draw() 时是不同的,但是当draw() 给一个匿名的Shape(就是不知道它的具体类型)发送消息时,就能产生基于Shape对象的实际类型的正确的行为。这是奇妙的,因为就像我们前边说过的,当C++ 编译器编译duStuff() 的代码的时候,它不能够知道它所需要处理的对象的具体类型时什么。所以,以一般的思路讲,它应该会调用针对Shape类型的erase() draw(),而不是针对CircleSquareLine这些具体的类型。多态确保了正确性。编译器和运行时系统解决这一问题,你所需要做的仅仅是明白这一点是会发生的,更重要的是你应该学会如何使用多态机制编程。如果一个成员函数是virtual的,你就可以对任意的对象发送消息,然后对象就会做出正确的处理,即使有向上转型出现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值