您的位置:首页 > 其它

虚函数 (virtual) --- 面向对象之概念解析

2008-07-10 10:01 281 查看
面试的时候遇到有这么一题:您在什么情况下会用到虚方法(虚函数)?它与接口有什么不同?

当不同的人面对这个问题的时候应该是有不同的反应,因为每个人对以上提到的知识点的理解程度不同。绝对有人迷惑,也有人似乎明白,有人不屑的撇撇嘴。迷惑的人因为不知道面试官想问什么,虚方法和接口在不同的讨论范围真是有点风马牛不相及;明白的人似乎知道有这么几个东西,并侃侃而谈:“由于Java不支持多继承,而有可能某个类或对象要使用分别在几个类或对象里面的方法或属性,现有的单继承机制就不能满足要求。与继承相比,接口有更高的灵活性,因为接口中没有任何实现代码。当一个类实现了接口以后,该类要实现接口里面所有的方法和属性,并且接口里面的属性在默认状态下面都是public static,所有方法默认情况下是public.一个类可以实现多个接口。”这时候面试官微笑着点点头,应聘者擦汗庆祝。

这就是所谓的答案吗?我们往下看。

1、 什么是虚函数

C++书中介绍为了指明某个成员函数具有多态性,用关键字virtual来标志其为虚函数。传统的多态实际上就是由虚函数(Virtual Function)利用虚表(Virtual Table)实现的也就是说,虚函数应为多态而生。看到虚函数,我就想到了多态。

2、 什么时候用到虚函数

既然虚函数应为多态而生,那么简单的说当我们在C++和C#中要想实现多态的方法之一就是使用到虚函数。复杂点说,那就是因为OOP的核心思想就是用程序语言描述客观世界的对象,从而抽象出一个高内聚、低偶合,易于维护和扩展的模型。

但是在抽象过程中我们会发现很多事物的特征不清楚,或者很容易发生变动,怎么办呢?比如飞禽都有飞这个动作,但是对于不同的鸟类它的飞的动作方式是不同的,有的是滑行,有的要颤抖翅膀,虽然都是飞的行为,但具体实现却是千差万别,在我们抽象的模型中不可能把一个个飞的动作都考虑到,那么怎样为以后留下好的扩展,怎样来处理各个具体飞禽类千差万别的飞行动作呢?比如我现在又要实现一个类“鹤”,它也有飞禽的特征(比如飞这个行为),如何使我可以只用简单地继承“飞禽”,而不去修改“飞禽”这个抽象模型现有的代码,从而达到方便地扩展系统呢?

一、OOP的目标:

使用面向对象的开发过程就是在不断地抽象事物的过程,我们的目标就是抽象出一个高内聚、低偶合,易于维护和扩展的模型。

二、遇到的问题:

但是在抽象过程中我们会发现很多事物的特征不清楚,或者很容易发生变动,怎么办呢?比如飞禽都有飞这个动作,但是对于不同的鸟类它的飞的动作方式是不同的,有的是滑行,有的要颤抖翅膀,虽然都是飞的行为,但具体实现却是千差万别,在我们抽象的模型中不可能把一个个飞的动作都考虑到,那么怎样为以后留下好的扩展,怎样来处理各个具体飞禽类千差万别的飞行动作呢?比如我现在又要实现一个类“鹤”,它也有飞禽的特征(比如飞这个行为),如何使我可以只用简单地继承“飞禽”,而不去修改“飞禽”这个抽象模型现有的代码,从而达到方便地扩展系统呢?

三、解决上述问题的方法:

面向对象的概念中引入了虚函数来解决这类问题。

使用虚函数就是在父类中把子类中共有的但却易于变化或者不清楚的特征抽取出来,作为子类需要去重新实现的操作(override),我们可以称之做“热点”。而虚拟函数也是OOP中实现多态的关键之一。

还是上面的例子(C#):

class 飞禽

class 麻雀 : 飞禽 // 麻雀从飞禽继承而来

class 鹤 : 飞禽 // 鹤从飞禽继承而来

这样我们只需要在抽象模型“飞禽”里定义Fly()这个行为,表示所有由此“飞禽”派生出去的子类都会有Fly()这个行为,而至于Fly()到底具体是怎么实现的,那么就由具体的子类去实现就好了,不会再影响“飞禽”这个抽象模型了。

比如现在我们要做一个飞禽射击训练的系统,我们就可以这样来使用上面定义的类:

// 如何来使用虚拟函数,这里同时也是一个多态的例子.

// 定义一个射击飞禽的方法

// 注意这里申明传入一个“飞禽”类作为参数,而不是某个具体的“鸟类”。好处就是以后不管再出现多少

// 种鸟类,只要是从飞禽继承下来的,都照打不误:)(多态的方式)

void ShootBird(飞禽 bird)

static void main()

四、C#种虚拟函数的的执行过程:

虚拟函数从C#的程序编译的角度来看,它和其它一般的函数有什么区别呢?一般函数在编译时就静态地编译到了执行文件中,其相对地址在程序运行期间是不发生变化的,也就是写死了的!而虚函数在编译期间是不被静态编译的,它的相对地址是不确定的,它会根据运行时期对象实例来动态判断要调用的函数,其中那个申明时定义的类叫申明类,那个执行时实例化的类叫实例类。

( 如:飞禽 bird = new 麻雀();

那么飞禽就是申明类,麻雀是实例类。 )

具体的检查的流程如下:

1当调用一个对象的函数时,系统会直接去检查这个对象申明定义的类,即申明类,看所调用的函数是否为虚函数;

2如果不是虚函数,那么它就直接执行该函数。而如果有virtual关键字,也就是一个虚函数,那么这个时候它就不会立刻执行该函数了,而是转去检查对象的实例类。

3在这个实例类里,他会检查这个实例类的定义中是否有重新实现该虚函数(通过override关键字),如果是有,那么OK,它就不会再找了,而马上执行该实例类中的这个重新实现的函数。而如果没有的话,系统就会不停地往上找实例类的父类,并对父类重复刚才在实例类里的检查,直到找到第一个重载了该虚函数的父类为止,然后执行该父类里重载后的函数。

知道这点,就可以理解下面代码的运行结果了:

class A

class B : A // 注意B是从A类继承,所以A是父类,B是子类

class C : B // 注意C是从A类继承,所以B是父类,C是子类

class D : A // 注意D是从A类继承,所以A是父类,D是子类

static void main()

再论虚函数与接口

如果非得要把这两者相提并论,还真能找出一丝的联系。这一丝的联系还得从多态的种类说起。多态的种类有两种,一为基类继承多态(Base Class Polymorphism),二为接口继承多态(Interface Polymorphism)。虚函数的使用实现的是基类继承多态,从设计模式的角度来说基类继承体系描述的是Is-A的问题。比如飞禽就是基类(父类),麻雀和鹤为子类继承了飞禽这个类。麻雀和鹤“Is-A”飞禽。除了基类继承多态,我们还有一种接口继承多态。顾名思义,这种多态是通过继承(更确切的说是“实现”)接口而产生继承体系的。从设计模式的角度来说接口继承体系描述的是Is-Like-A(或者叫Can-do)的问题(详见博客上另一篇文章《从设计模式看抽象类与接口的区别》)。比如一个具有报警功能的门,我们要实现“报警门”这么一个类,“报警门”“Is-A”门,而不是一个报警器,只是“Is-Like-A”报警器而已。所以“报警门”的报警功能要通过实现报警器这个接口来实现报警功能。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: