学习笔记之深入浅出MFC 第8章 C++重要性质----虚拟函数与多态(Polymorphism)

1、虚拟函数的由来

上面我们曾经提过一个例子:

CShape shapes[5];

. . . //令5个shapes各为矩形、正方形、椭圆形、圆形、三角形

for ( int i = 0;  i<5;  i++)

{

shapes[i].display();

}

在上一节中我们说这种一般化的操作无法完成。你还记得为什么吗?是这样的,上面一节中讲到,由于每一个子类图形的绘制不同,所以display()各不相同,所以无法提升到基类中去。那么用基类定义的shapes[]数组,当然也就没有display()函数了。

可是其实我们真的很希望能在application framwork中这样操作。我们总是希望能够准备一个display函数,不管根据一大堆形状类派生出什么其他的奇形怪状的类,只要想display,向下面这样就行了:

正是为了支持这种功能,C++提供了所谓的虚拟函数(virtual  function)。

2、用一个范例展开说明

虽然不想写一长长的例子,但是不得不承认仔细分析一个例子从而引入介绍确实有利于理解。那么,我们还是从这个很长的程序例子开始吧,大家要有耐心吆!(本人第一次就没耐心看下去

假设你的Class种类如下:

#include <string.h>

首先是我们的基类,职员类CEmployee

class  CEmployee   //职员

{

private:

char   m_name[30];

public:

CEmployee();

CEmployee( const  char*   nm)    {  strcpy( m_name,  nm);  }

};

上面这个就是我们的基类了,成员变量是name,成员函数是一个结构函数和一个给m_name传入字符串的函数CEmployee( const char* nm)。我们可以知道这个类只有一个功能就是传入职员的姓名。

下面从职员类CEmployee继承下来两个子类----时薪职员和经理。、

class  CWage  :   public  CEmployee              //时薪职员是一种职员

{

private:

float  m_wage;

float  m_hours;

public:

CWage(const  char*  nm) :  CEmployee(nm) {  m_wage = 250.0 ;   m_hours  = 40.0 ; }   //赋初值

void  setWage(float  wg)  { m_wage  = wg;}

void  setHours(float  hrs)  { m_hours  = hrs;  }

float  computePay();

};

上面这段代码是时薪职员的类,除了继承了CEmployee类的m_name和CEmployee(const char* nm)之外,它本身又添加了小时m_hours和薪金m_wage两个成员变量以及设置这两个变量的函数。另外还有计算计算总薪水的函数computePay()。

同样,经理也是一种职员,也是继承自CEmployee类。

class   CManager  :   public  CEmployee    //经理也是一种职员

{

private:

float  m_salary;

public:

CManager( const char*  nm)  :  CEmployee(nm)  {  m_salary  = 15000.0;  }

void  setSalary(float  salary)        {  m_salary  =  salary;  }

float  computePay();

};

同样的经理类也在继承CEmployee类的成员变量m_name和成员函数CEmployee(const char* nm)的基础上,另外添加了薪水变量m_salary和设置薪水函数setSalary(),以及计算总工资的函数computePay();

然后,下面从时薪职员子类中又继承了一类子类,为销售员。

class   CSales  :   public  CWage     //销售员是一种时薪职员

{

private:

float  m_comm;

float  m_sale;

public:

CSales(const  char*   nm)  :  CWage(nm)    {  m_comm  = m_sale  =0.0;}

void  setCommission( float  comm)    {  m_comm  = comm ; }

void  setSales( float  sale  )    {  m_sale  =  sale ;}

float   computePay();

};

这销售员的类是从时薪职员类中继承下来的,所以自然继承了时薪职员类的所有成员,除此之外,又加了m_comm和m_sale两个变量及设置变量的函数。

下面我们写一个简单的主函数

void  main()

{

CManager    aManager("  陈美静");

CSales  aSales(" 侯俊杰");

CWage  aWager(" 曾铭源");

}

通过这一系列的继承,我们看一下到CSales时CSales拥有了哪些。

char  m_name[30];      //继承自CEmployee类的成员变量

float  m_wage;

float  m_hours;       //继承自CWage类的成员变量

float  m_comm;

float  m_sale;        //CSales本身的成员变量

void setWage(float  wg);

void setHours(float  hrs);

void setCommission(float  comm);

void setSale(float  sales);

void  computePay();        //这些是继承下来的函数

执行主函数之后,从Visual C++调试器中看到程序拥有的对象的情况:

从这个图中我们可以看到生成的三个大类以及每个类下面变量的归属情况。

例子的整个结构我们已经铺开了,那么现在借着这个背景我们就要说一说虚拟函数的故事了。成员变量和给成员变量赋值的函数我们就不提了,这个在前面结合this指针已经说过了。关于虚拟函数,我们就以每个类都有的薪水计算说起,下面是设计的computePay函数:

首先是经理的薪水(经理只有固定周薪)(注意这里都是用周薪来算的)

float  CManager::comoutePay()

{

return  m_salary;        //经理以“固定周薪”计薪

}

接着是时薪职员额薪水计算:

float  CWage::computePay()

{

return  (m_wage  *  m_hours);     //时薪职员以 “钟点费” * “每周工时”计薪

}

然后是销售员:销售员以“钟点费 * 每周工时”再加上“佣金  *  销售额”计薪

float  CSales::computePay()

{

return  (m_wage  *  m_hours  +  m_comm  *  m_sale );

}

这个地方需要注意一下,实际上上面这个句子是有语法错误的,还记得前面我们提到过类成员的几种属性吗?有private、public和protected三种,这里的m_wage和m_hours是取用的CWage的,而且属于private类型,所以不能直接调用,所以这里正确的写法应该是:

float CSales::computePay()

{

return  CWage::computePay() + m_comm * m_sale;   //当然computePay()应该标明是哪个类的

}

这样就合乎逻辑了:销售员是一般员工的一种,他的薪水应该是以时薪员工的计薪方式作为底薪,再加上额外的销售佣金。看看下面的例子:

有一个销售员叫侯俊杰:

CSales  aSales( " 侯俊杰");

那么侯俊杰的底薪应该是:

aSales.CWage::computePay() ;       //这时底薪

而侯俊杰的全薪应该是:

aSales.computePay();    //全薪

上面这个例子中我们看到,要调用父类的函数,必须使用(::)符号明白指出。

3、对象类型的转换

例子已经讲完了,要用这个例子来做些什么呢?我们随着下面的思路只要是为了通过问题引出我们的目的--虚拟函数。

接下来我们要触及对象类型的转换,这关系到指针的运用,更关系到为什么需要虚拟函数。所以了解这一部分对于application framework如MFC者的运用非常的重要。

假如现在有两个对象:

CWage  aWager;

CSales  aSales("侯俊杰");

因为销售员是时薪员工的一种,因此这样的赋值是合理的:

aWager = aSales;     //合理,销售员必定是时薪职员

但是反过来就不合理了:

aSales = aWager;     //错误,时薪职员未必是销售员

那么我如果非得要转换呢?那就必须得使用指针了,并且得做类型转换操作:

CWage* pWager;

CSales* pSales;

CSales  aSales("侯俊杰");

pWager = &aSales;      //把一个基类指针指向派生类对象,合理且自然

pSales = (aSales *)pWager;   //强迫转型。语法上可行,但是不符合现实生活

现实中为了方便我们经常会”一种动物“总称猫呀狗呀兔子呀等等,这里我们也想以”一个通用的指针“表示所有可能的职员类型。像下面这样:

CEmployee*  pEmployee;

CWage   aWager("曾铭源");

CSales   aSales("侯俊杰");

CManager  aManager("陈美静");

pEmpolyee  = &aWager;      //合理,因为时薪职员必是职员

pEmployee =  &aSales;       //合理,因为销售员必是职员

pEmployee  = &aManager;  //合理,因为经理必是职员

也就是说,可以把一个”职员指针“指向任何一个职员。这将给程序设计带来巨大的弹性。

看到这里大家或许会有豁然开朗的感觉,你不就是想用基类指针指向不同的子类对象,到时候只要调用基类指针就能调用指向的那个子类对象的函数了嘛。是的,我是想要这样,但是显示往往不尽如人意!!

事实上是这样的:

CSales  aSales("侯俊杰");

CWage* pWager;

CSales* pSales;

pWager = &aSales;    
//以基类指针指向派生类对象

pSales = &aSales;

pWager ->setSales(800.0); //错误(编译器会检测出来),因为CWage并没有定义setSales函数

pSales->setSales(800.0); //正确,调用CSales::setSales函数

虽然pSales和pWager指向同一个对象,但却因为指针的原始类型而使两者之间有了差异。

延续此例,我们看另外一种情况:

pWager ->computePay(); //调用CWage::computePay()

pSales ->computePay(); //调用CSales::computePay()

虽然pSales和pWager实际上都指向CSales对象,但是两者调用的computePay却不相同。到底调用到哪个函数,必须视指针的原始类型而定,与指针实际所指对象无关。

我们得出三个结论:

1、如果以一个”基类指针“指向一个”派生类对象“,那么经由该指针你只能调用基类所定义的函数。

2、如果以一个”派生类指针“指向一个”基类对象“,那么必须先做明显的转型操作。但这种做法很危险,不符合实际生活经验,也会给程序员带来困惑。

3、如果基类和派生类都定义了”相同名称成员函数“,那么通过对象指针调用函数时,到底调用哪一个函数,必须视该指针的原始类型而定,而不是视指针实际所指的对象类型而定。

4、虚拟函数与一般化

经过前面的铺垫,啰啰嗦嗦讲了一大通,大家还记得我们的目的是想干啥来着?回顾一下,我们是想不用每次计算各个子类的薪水,想要直接调用指针,指针指向那个对象,就调用哪个对象的computePay()函数。其实也就是一个一般化的问题。一般化之所以重要,就在于它可以把现在的、未来的情况统统纳入考虑。即便又多了个职员类型叫”顾问“,我只需要指向顾问对象,就调用顾问的computePay()函数了。

”一般化“就是我们要实现的目的,但是上面我们碰到了问题,指针只能指向指针本身类型的函数,并不能指向子对象的函数。我们现在的目的是什么呢?是”依旧以CEmployee指针代表每一种职员“,而又能在”实际指向不同种类的职员“时,调用到不同版本(子类)的computePay()函数。

其实这种性质就是”多态“,靠虚拟函数来完成。

虚拟函数就是为了解决上面这个问题而设计的。如果你以一个基类指针指向一个派生类对象,那么通过指针你就能够调用指针指向的子类的成员函数。这种功能只需要在子类的函数前面加上virtual保留字就使它们成为了虚拟函数。

看看我们之前的例子:

我们看到一种奇妙的现象:程序代码完全一样(因为一般化了),执行结果却不相同,这就是虚拟函数的妙用。

从操作型定义来看,什么是虚拟函数呢?如果你预期派生类有可能重新定义某一个成员函数,那么就在基类中把此函数设为virtual。MFC有两个十分重要的虚拟函数:与document有关的Serialize函数和与view有关的OnDraw函数。你应该在自己的CMyDoc和CMyView中改写这两个虚拟函数。

5、多态(Polymorphism)

上面这种以相同的指令却能唤起不同的函数,这种性质称为多态。编译器无法在编译时期判断pEmp->computePay到底调用哪一个函数,必须在执行期才能判断之,这称为后期绑定或动态绑定。至于C函数或C++的non-virtual函数,在编译时期就转换为一个固定地址的调用了,这称为前期绑定或静态绑定。

多态的目的,就是要让处理”基类对象“的程序代码能够无碍的继续适当处理”派生类对象“。

可以说虚拟函数是了解多态以及动态绑定的关键。同时也是了解如何使用MFC的关键。

当我们在设计一套类的时候,你并不知道使用者会派生出什么新的子类出来,比如动物世界中出现了新品种名叫雅虎,类使用者势必在CAnimal之下派生一个CYahoo。饶是如此,身为基类设计者的你,仍可以利用虚拟函数的特性,将所有动物必定会有的行为(例如咆哮roar)规划为虚拟函数。并且规划一些一般化的操作(例如让每一种动物发出一声咆哮)。那么,虽然你在基类设计以及这个一般化操作时无法掌握使用者自行派生的子类,但只要他改写roar这个虚拟函数,你的一般化操作自然就可以调用该函数。

再回到前述的Shape例子。我们说CShape是抽象的,所以它根本不该有display这个操作。但是为了在各具体的派生类中绘图,我们又不得不在基类CShape中加上display虚拟函数。你可以定义它什么也不做(空函数):

class CShape

{

public:

virtual void display();

};

或者只给个消息

class CShape

{

public:

virtual void display() { cout << "Shape \n"}

};

这两种做法都不高明,因为这个函数根本就不应该被调用(CShape是抽象的),我们根本就不应该定义它。所以C++提供了所谓的纯虚拟函数:

class CShape

{

public:

virtual void display() = 0; //注意 ” = 0“

};

纯虚拟函数不需定义其实际操作,它的存在只是为了在派生类中被重新定义,只是为了提供一个多态接口。只要是拥有纯虚拟函数的类,就是一种抽象类,他不能被实例化,也就是说你不能根据它产生一个对象。

关于抽象类,CCircle本身继承CShape之后,如果没有改写CShape中的纯虚拟函数,那么CCircle本身也就是一个拥有纯虚拟函数的类,于是它也是一个抽象类。

对虚拟函数的总结:

1、如果期望在派生类中重新定义一个成员函数,那么你应该在基类中把此函数设为virtual。

2、以单一指令唤起不同函数,这种性质称为多态。

3、虚拟函数是C++语言的多态性质以及动态绑定的关键。

4、既然抽象函数中的虚拟函数不打算被调用,我们就不应该定义它,应该设为纯虚拟函数(在函数声明之后加上” = 0“)。

5、我们可以判定,在拥有纯虚拟函数者为抽象类,以区别于所谓的具体类。

6、抽象类不能产生对象实例,但我们可以拥有指向抽象类的指针,以便操作抽象类的各派生类。

7、虚拟函数派生下去仍为虚拟函数,而且可以省略virtual关键词。

时间: 2025-01-10 11:44:31

学习笔记之深入浅出MFC 第8章 C++重要性质----虚拟函数与多态(Polymorphism)的相关文章

学习笔记之深入浅出MFC 第9章 仿真MFC 之一

在文章开始是我们的观点就强调过了,要想用好一个工具,就必须深入了解这个工具的工作原理.而仿真,正是最好的方法. 如何仿真呢?我们在console程序中仿真MFC,这样可以把程序结构的负荷降到最低.作者在仿真中的原则是:简化再简化,简化到不能再简化.请注意,以下所有程序的类层次结构.类名称.变量名称.结构名称.函数名称.函数内容,都以MFC为仿真对象,具体而微. 在档案的安排上,作者把仿真MFC的类集中在MFC.H和MFC.CPP中,把自己派生的类集中在MY.H和MY.CPP中.对于自定义的类,我

学习笔记之深入浅出MFC 第9章 仿真MFC之二

RTTI(执行期类型识别) 在前面章节中我们介绍过Visual C++4.0支持RTTI,重点不外乎是: 1.编译时需选用/GR(/GR的意思是enable C++ RTTI) 2.包含typeinfo.h 3.使用新的typeid运算符. 其实,MFC在编译器支持RTTI之前,就有了这项能力.我们现在要以相同的手法,在Console程序中仿真出来.我希望我的类库具备IsKindOf的能力,能在执行期侦测某个对象是否"属于某种类",并传回TRUE或FALSE.以前一章的Shape为例,

《python基础教程(第二版)》学习笔记 基础部分(第1章)

<python基础教程(第二版)>学习笔记基础部分(第1章)IDEWindows: IDLE(gui), Eclipse+PyDev; Python(command line);Linux/Unix: python >>> 1/2=0 注意整除得0>>> from __future__ import division 执行普通的除法python -Qnew 执行普通的除法 //整除,  1//2=0:%取余数:**乘幂长整型数: 末尾带L十六进制,以0x开头

Scala学习笔记一之基础语法,条件控制,循环控制,函数,数组,集合

前言:Scala的安装教程:http://www.cnblogs.com/biehongli/p/8065679.html 1:Scala之基础语法学习笔记: 1:声明val变量:可以使用val来声明变量,用来存放表达式的计算结果,但是常量声明后是无法改变它的值的,建议使用val来声明常量: 声明var变量:如果要声明可以改变的引用,可以使用var变量,声明的常量的值可以改变. 3:指定类型:无论声明val变量还是声明var变量.都可以手动指定其类型,如果不指定,scala会自动根据值,进行类型

课本学习笔记1:第一、二章 20135115臧文君

第一章 Linux内核简介 注:作者:臧文君,原创作品转载请注明出处. 一.Unix的历史 1.1969年,Dennis Ritchie和Ken Thompson,Unix. 2.Unix产生于贝尔试验室的一个失败的多用户操作系统Multics. 第一个被广泛使用的Unix版本是第6版,称为V6. 3.进一步开发: 加州大学伯克利分校:BSD(Berkeley Software Distributions). 4.Unix系统强大的根本原因:策略和机制分离的设计理念,确保了Unix系统具备清晰的

JavaScript高级程序设计(第三版)学习笔记20、21、23章

第20章,JSON JSON(JavaScript Object Notation,JavaScript对象表示法),是JavaScript的一个严格的子集. JSON可表示一下三种类型值: 简单值:字符串,数值,布尔值,null,不支持js特殊值:undefined 对象:一组无序的键值对 数组:一组有序的值的列表 不支持变量,函数或对象实例 注:JSON的字符串必须使用双引号,这是与JavaScript字符串最大的区别 对象 { "name":"Nicholas"

JavaScript高级程序设计(第三版)学习笔记22、24、25章

第22章,高级技巧 高级函数 安全的类型检测 typeof会出现无法预知的行为 instanceof在多个全局作用域中并不能正确工作 调用Object原生的toString方法,会返回[Object NativeConstructorName]格式字符串.每个类内部都有一个[[Class]]属性,这个属性中就指定了上述字符串中的构造函数名. 原生数组的构造函数名与全局作用域无关,因此使用toString方法能保证返回一致的值,为此可以创建如下函数: function isArray(value)

JavaScript高级程序设计(第三版)学习笔记11、12、17章

第11章, DOM扩展 选择符 API Selector API Level1核心方法querySelector .querySelectorAll,兼容的浏览器可以使用 Document,Element 实例调用它们,支持浏览器:IE8+,Firefox3.5+,Safari3.1+,chrome,Opera10+ querySelector方法 接收一个 CSS选择符,返回与该模式匹配的第一个元素 通过 Document类型调用该函数,会在文档范围查找匹配元素,通过 Element类型调用该

JavaScript高级程序设计(第三版)学习笔记8、9、10章

第8章,BOM BOM的核心对象是window,具有双重角色,既是js访问浏览器的一个接口,又是ECMAScript规定的Global对象.因此,在全局作用域中声明的函数.变量都会变成window对象的属性和方法. 例: var age = 20; function sayAge(){ alert(this.age); } alert(window.age); //20 window.sayAge(); //20 定义全局变量与在window对象上直接定义属性区别:全局变量不能通过delete操