分享

深刻剖析经典面试题之三:关于虚函数

 沧海九粟 2007-09-28

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

 

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

 

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

 

1、   什么是虚函数

 

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

 

2、   什么时候用到虚函数

 

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

 

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

 

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

 

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

 

下面引举一个例子:(C#描述)

class 飞禽

 

{

 

     public string wing;         // 翅膀

 

     public string feather;      // 羽毛

 

     ……               // 其它属性和行为

 

     public virtual bool Fly()   // 利用关键字virtual来定义为虚函数,这是一个热点

 

     {

 

         // 空下来让子类去实现

 

     }

 

}

 

 

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

 

{

 

     …… // 定义麻雀自己特有的属性和行为

 

     public override bool Fly()  // 利用关键字override重载飞翔动作,实现自己的飞翔

 

     {

 

         …… // 实现麻雀飞的动作

 

     }

 

}

 

 

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

 

{

 

…… // 定义鹤自己的特有的属性和行为

 

     public override bool Fly()  // 利用关键字override重载实现鹤的飞翔

 

     {

 

         …… // 实现鹤飞的动作

 

     }

 

}

 

 

 

 

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

 

 

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

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

 

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

 

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

 

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

 

void ShootBird(飞禽 bird)

 

{

 

     // 当鸟在飞就开始射击

 

     if(bird.Fly())

 

     {

 

         …… // 射击动作

 

     }

 

}

 

 

static void main()

 

{

 

     / /打麻雀

 

     ShootBird(new 麻雀());

 

     // 打鹤

 

     ShootBird(new ());

 

     // 都是打鸟的过程,我只要实现了具体某个鸟类(从“飞禽”派生而来)的定义,就可以对它

 

     // 进行射击,而不用去修改ShootBird函数和飞禽基类

 

     ShootBird(new 其它的飞禽());

 

}

 

 

 

虚函数从C#的程序编译的角度来看,它和其它一般的函数有什么区别呢?一般函数在编译时采用先期绑定(详见我的文章:深刻剖析经典面试题之四:OOP的三个核心本质之多态)编译到了执行文件中,其相对地址在程序运行期间是不发生变化的。而虚函数在编译期间采用的是后期绑定,它的相对地址是不确定的,它会根据运行时期对象实例来动态判断要调用的函数,其中那个声明时定义的类叫声明类,那个执行时实例化的类叫实例类。

 

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

 

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

 

具体的检查的流程如下:

 

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

 

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

 

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

 

 

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

class A

 

{

 

     protected virtual Func()    // 注意virtual,表明这是一个虚函数

 

     {

 

         Console.WriteLine("Func In A");

 

     }

 

}

 

 

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

 

{

 

     protected override Func()   // 注意override ,表明重新实现了虚函数

 

     {

 

         Console.WriteLine("Func In B");

 

     }

 

}

 

 

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

 

{

 

    

 

}

 

 

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

 

{

 

     protected new Func()        // 注意new ,表明覆盖父类里的同名类,而不是重新实现

 

     {

 

         Console.WriteLine("Func In D");

 

     }

 

}

 

 

static void main()

 

{

 

     A a;               // 定义一个a这个A类的对象.这个A就是a的声明类

 

     A b;               // 定义一个b这个A类的对象.这个A就是b的声明类

 

     A c;               // 定义一个c这个A类的对象.这个A就是b的声明类

 

     A d;               // 定义一个d这个A类的对象.这个A就是b的声明类

 

 

     a = new A();           // 实例化a对象,Aa的实例类

 

     b = new B();           // 实例化b对象,Bb的实例类

 

     c = new C();           // 实例化b对象,Cb的实例类

 

     d = new D();           // 实例化b对象,Db的实例类

 

 

     a.Func() ;

 

     // 执行a.Func1.先检查声明类A 2.检查到是虚方法 3.转去检查实例类A,就为本身 4.执行实例类A中的方法 5.输出结果 Func In A 

 

     b.Func() ;

 

     // 执行b.Func1.先检查声明类A 2.检查到是虚方法 3.转去检查实例类B,有重载的 4.执行实例类B中的方法 5.输出结果 Func In B

 

     c.Func() ;

 

     // 执行c.Func1.先检查声明类A 2.检查到是虚方法 3.转去检查实例类C,无重载的 4.转去检查类C的父类B,有重载的 5.执行父类B中的Func方法 5.输出结果 Func In B

 

     d.Func();

 

     // 执行d.Func1.先检查声明类A 2.检查到是虚方法 3.转去检查实例类D,无重载的(这个地方要注意了,虽然D里有实现Func(),但没有使用override关键字,所以不会被认为是重载) 4.转去检查类D的父类A,就为本身 5.执行父类A中的Func方法 5.输出结果 Func In A

 

 

     D d1 = new D()

 

     d1.Func();    // 执行D类里的Func(),输出结果 Func In D}

 

 

 

 

 

 

 

 

 

 

3、   再论虚函数与接口

 

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

 

4、   虚函数和接口哭了

 

它们哭着说:“面试官,强扭的瓜不甜····”。

 

 

后记:老实说我是怀着一种忐忑不安的心情在总结这个问题,或许我的总结不是最后的答案。但是我希望这些都化做上升的一种过程,欢迎大家的批驳与交流。有句成语讲的好,叫“从善如流”。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多