三谈多态——善用virtual

原创 2002年05月08日 10:18:00

三谈多态——善用virtual

作者:Nicrosoft(nicrosoft@sunistudio.com) —— 2002.4.30

出处:东日软件开发网络(SSDN) http://www.ssdn.net

 
  多态性,是一种能给程序带来灵活性的东西。看过《设计模式》的程序员应该都知道,相当多的模式(几乎所有)都是依靠多态来实现的,以此给程序提供可扩展、可重用性。在《再谈多态——向上映射及VMT/DMT》一文中,提到了多态性是依赖于虚函数/虚方法(即动态绑定)来实现的,也介绍了虚函数/虚方法(virtual)的实现方法。那么本文就来谈一下,如何使用virtual、善用virtual来获取多态性给我们带来的灵活性。
  
  实例是最好的教材,因此本文还是假设一个需求,写一个实例来讲解。不过,我想没有必要给出所有源码,因此在本文中有些实现的代码会粗略带过。另外,本文所有代码均为Object Pascal语言编写,实现环境为Delphi。
  
  另外,由于“方法(Method)”一词已经成为Object Pascal的术语,因此,以下称成员函数都为“方法”。也许C++程序员会不太适应这样的称呼(呵呵,我自己也不太适应),见谅吧。
  
  假设我们要编写一个纯文本内的编辑器,也就是记事本(呵呵,别嫌例子老套,记事本程序在相当多方面都是很好的教材),编辑控件我们一般会用TMemo或TRichEdit,但是它们的功能都不甚强大,也许我们目前没有,但日后会找到一个更好的第三方文本编辑控件(比如,支持语法着色的)。因此,我们必须为未来的改进留下方便之门,否则到时候再重写全部程序真是太傻了。
  
  界面层(菜单响应、状态显示等)对文本编辑器控件的控制的代码对于所有编辑器来说都是类似的,应该可以被重用。那么就必须将界面层的代码与编辑器控件的控制代码隔离开来。
  
  如何隔离?我们可以为所有的编辑器控件指定一个公共的接口(抽象类),界面层只看得到这个接口,只使用接口提供的功能,那么,我们更换任何编辑器控件时,都不必更改界面层的代码了。
  
  首先,抽象出编辑器的基本操作,如:Load(打开文本)、Save(保存到文件)、Copy(复制到剪贴板)等等,将这些操作作为public方法。其次,考虑这些操作中有哪些会涉及到具体相关控件的,对于这些操作,你有两种选择:1、如果完全依赖控件本身的,可以选择将其定义为虚方法或抽象虚方法(即C++中的纯虚函数);2、如果只是有部分代码依赖控件本身的,将这部分操作抽象到一个protected的抽象虚方法中,而将相同的部分代码写在基类中,并由基类的方法中调用protected的抽象虚方法。
  
  如果还没有完全明白,请看代码:
   
    TEditor = class // 抽象基类
  private
      m_FileName : String;
  protected
      function DoLoad(FileName : String) : Boolean; virtual; abstract;
    public
        function Load(FileName : String) : Boolean;
        function Save() : Boolean;
        function SaveAs(FileName : String) : Boolean; virtual; abstract;
      // ... 其他需要的操作,由需求而定
    end;
  
  好,我们来详细说明一下TEditor为什么是这个样子的。其有一个私有成员,保存编辑器所对应的文件名。然后看public部分,它至少提供了三个操作:Load——从某文件中读取文本到编辑器;Save——将文本保存到文件,文件名为m_FileName所保存的;SaveAs——以指定的一个文件名保存文本。

  三个操作中,SaveAs为抽象虚方法,因为它的实际动作与每个编辑控件相关,而基类本身并不知道该如何保存文件。

  Save和Load都是非虚方法,其中Save的任务很简单,就是将文本以m_FileName所保存的文件名保存,其实现可以是调用抽象的SaveAs,只需要将文件名传给SaveAs即可。基类完全可以确定如何完成Save(因为它可以保证派生类实现了SaveAs),因此其为非虚方法。

  而Load呢?Load操作的步骤是:1、将文本从文件中读取到编辑器控件中;2、将m_FileName的值设置为所读取的文件名。其中第一个步骤与具体编辑器控件相关,应该是由派生类去实现它,第二个步骤派生类无法实现,因为派生类看不到私有的m_FileName成员。但即使把m_FileName移到protected节中,以使派生类可以访问它,也并非好办法,因为这样的话,在实现每个派生类时,都要记住设置m_FileName的值,这不仅造成代码重复(每个派生类都要这样做),而且这不应该是派生类的义务。因为m_FileName应该是基类的内部实现,对外不可见。因此,第二个步骤应该由基类来完成。如何达成这个目的呢?

  我们可以注意到,protected节中有一个DoLoad方法,它就被用来完成第一个步骤——每个编辑器控件去将文本读入编辑器。然后,DoLoad由Load方法中被选择在适当的时机调用。
  
    基类的实现如下:

    function TEditor.Load(FileName : String) : Boolean;
  begin
      Result := DoLoad(FileName);
      if Result then
          m_FileName := FileName;
    end;
  
    function TEditor.Save() : Boolean;
  begin
      SaveAs(m_FileName); // 调用抽象的 SaveAs
    end;
  
  接着,我们使用TMemo来实现一个编辑器类:
  
  TMemoEditor = class(TEditor)
  private
      m_Editor : TMemo;
  protected
      function DoLoad(FileName : String) : Boolean; override;
    public
      constructor Create();
      destrcutor Destroy(); override;
  
      function SaveAs(FileName : String) : Boolean; override;
      // ...其它需要的操作
  end;
  
  在该派生类中,有一个私有成员,即TMemo控件的实例。然后覆盖(override)了基类的两个抽象虚方法:DoLoad和Save。
  
  其实现如下:
  function TMemoEditor.Create();
  begin
      // 创建TMemo实例
        m_Editor := TMemo.Create(nil);
     
        // 接着完成将TMemo实例置于界面上显示出来等操作,省略
    end;
  
  function TMemoEditor.Destroy();
  begin
      // 其他清理工作
  
      m_Editor.Free();
      m_Editor := nil;
  end;
  
  function TMemoEditor.DoLoad(FileName : String) : Boolean;
  begin
        Result := false;
        try
          m_Editor.LoadFromFile(FileName);
       except end;
      Result := true;
  end;
  
  function TMemoEditor.SaveAs(FileName : String) : Boolean;
  begin
      Result := false;
      try
          m_Editor.SaveToFile(FileName);
      except end;
      Result := true;
    end;
  
  很好,这样的实现已经可以使个部分运作正常了。如果,今后找到更好的编辑器控件,只需要从TEditor派生,再实现一个TXXXEditor类即可,其他部分的代码不用作任何改动。而且,具体实现的TXXXEditor类中的代码,只和具体控件本身特性相关(如:读取、保存文件的方法),而公共逻辑也已经在TEditor类中实现了。
  
  virtual的使用方法,基于笔者个人认识与经验:
  
  1、如果基类不知道如何实现某方法(只有派生类知道),而基类的其他方法又必须使用该方法,则把该方法声明为抽象虚方法—— virtual; abstract;(即C++的纯虚函数)。
  
  2、如果基类能够为某方法提供一种默认实现,但派生类可能完全重写这个实现,则将该方法声明为虚方法—— virtual;并实现默认算法。

  3、如果基类能够且必须提供某方法的部分的实现,而派生类必须提供另一部份的实现,则将该方法声明为非虚方法,并在基类中为其配套提供一个虚方法或抽象虚方法,以允许由基类本身调用和被派生类覆盖。犹如上例中的Load与DoLoad。
  
  善用virtual,善用多态,你的代码将更具灵活性!

三谈多态——善用virtual

  • zgqtxwd
  • zgqtxwd
  • 2008年04月30日 16:36
  • 116

学习整理——C++ virtual虚函数与多态

多态与动态绑定 多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。引用Charlie Calverts对多态的描述——多态性是允许你将...
  • Jeffery_Gong
  • Jeffery_Gong
  • 2016年10月07日 17:54
  • 996

多态(virtual)– 真正的面向对象编程

本文介绍多态的实现机制、virtual函数的用法及其注意事项。
  • yeming81
  • yeming81
  • 2010年06月16日 01:13
  • 3335

virtual C++多态实现原理

1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。 2. 存在虚函数的类都有一个一维的虚函数表叫做虚表。类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对...
  • menyangyang
  • menyangyang
  • 2014年04月30日 11:39
  • 812

C++虚函数和多态性的关系【virtual】之一

很多学C++的人,对于如何理解其多态性,相信都挺模糊的,我也是其中一个。现在,我感觉我对这个理解有了更深一层的理解,现在写出来,和大家一起分享,如果有错,请大家不吝赐教。 如果没有虚函数【也就是vi...
  • ComeOnTom
  • ComeOnTom
  • 2012年05月10日 11:03
  • 3746

Java基础中的多态理解

个人对Java中多态的部分理解,多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用...
  • u013682392
  • u013682392
  • 2015年07月29日 10:45
  • 593

java提高篇(三)-----理解java的三大特性之多态

面向对象编程有三大特性:封装、继承、多态。        封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访...
  • chenssy
  • chenssy
  • 2013年10月16日 19:44
  • 57543

【面试题】Java三大特性封装继承多态总结

本文内容总结于多篇博客,参考到的地方在文中都会一一列出 http://www.cnblogs.com/ibalintha/p/3897821.html 1.封装 封装...
  • zjkC050818
  • zjkC050818
  • 2017年10月18日 22:15
  • 624

OOP的三个核心本质之多态

OOP的三个核心本质是什么?     这是道基础中见思想的面试题,面试官爱问这个问题。不过关于OOP三个核心众多书籍似乎莫衷一是。《Java编程思想》第7章谈到多态的开篇语便是:除了数据的抽象化(da...
  • u011617875
  • u011617875
  • 2014年02月06日 17:57
  • 1326

如何高效使用搜索引擎 - 善用高级搜索

如何高效使用搜索引擎 - 善用高级搜索 1、双引号   把搜索词放在双引号中,代表完全匹配搜索,也就是说搜索结果返回的页面包含双引号中出现的所有的词,连顺序也必须完全匹配。bd和Goo...
  • Techzero
  • Techzero
  • 2013年05月31日 21:05
  • 2820
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:三谈多态——善用virtual
举报原因:
原因补充:

(最多只允许输入30个字)