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关键词。