虚拟函数-1、静态联编与动态联编,引入虚函数
在实际开发工作中,为提高代码的重用性,编写通用的功能模块,往往需要设计处理几种不同对象的通用程序,如示例2.1所示。
示例清单2.1
#include "stdio.h"
#include "stdlib.h"
//定义函数指针类型DISPLAYINTEGER,指向返回值为void,参数列表为(const int)的函数
typedef void( *DISPLAYINTEGER)(const int);
//定义函数,将数字以十进制形式输出,该函数类型与DISPLAYINTEGER匹配
void DisplayDecimal(const int Number)
{
printf("The decimal value is %d\n",Number);
}
//定义函数,将数字以八进制形式输出,该函数类型与DISPLAYINTEGER匹配
void DisplayOctal(const int Number)
{
printf("The octal value is %o\n",Number);
}
//定义函数,将数字以十六进制形式输出,该函数类型与DISPLAYINTEGER匹配
void DisplayHexadecimal (const int Number)
{
printf("The hexadecimal value is %x\n",Number);
}
/********************************************************************
定义通用的显示数字函数
DisplayFormat DISPLAYINTEGER函数指针类型,实参可以是以上定义的
3个函数之一。通过传递不同的实参,将数字以各种格式输出
Number 准备输出的数字
********************************************************************/
void DisplayNumber(DISPLAYINTEGER DisplayFormat,const int Number)
{
//调用以实参传入的函数,以某种格式输出整型数字
DisplayFormat(Number);
}
int main(int argc, char* argv[])
{
int Number=0;
//如果有数字形式的命令行参数,将其输出,否则输出0
if(argc>1)
Number=atoi(argv[1]);
//分别以3种格式将数字输出
DisplayNumber(DisplayDecimal,Number);
DisplayNumber(DisplayOctal,Number);
DisplayNumber(DisplayHexadecimal,Number);
return 0;
}
命令行
c121.exe 50
的输出结果:
The decimal value is 50
The octal value is 62
The hexadecimal value is 32
示例2.1中定义了一个通用函数:void DisplayNumber(DISPLAYINTEGER DisplayFormat, const int Number)。
其功能是以各种格式显示整型数字。只要传递适当的实参(函数地址),该函数就能很好地工作。如果客户需求发生变化,例如增加二进制格式的输出,只要增加相应的功能函数,例如,void DisplayBinary (const int Number)即可。而通用函数DisplayNumber()不必改动。显然,函数指针DISPLAYINTEGER给该函数增添了灵性,使其得以通用。
其实,以上函数的通用性得益于C++的动态联编功能,而函数指针不过是该功能的一种应用形式。
在C++编译时,对于常规的函数调用,编译器在函数的调用处插入函数的相对地址,程序运行时可以由函数的相对地址计算出函数的绝对地址,这样函数可以被正确调用。这种在编译时就确定函数地址的联编过程叫做静态联编。动态联编是指在程序编译时,编译器并不知道函数的相对地址,调用函数的相对地址只有在程序运行时才能确定。例如在示例2.1中的DisplayNumber()函数体内,编译器并不知道DisplayFormat(Number)调用的函数地址,真正的地址是在运行时通过实参传入的。
2.2 引入虚拟函数
看来,基于动态联编的机制,使用函数指针就可以编写出相对通用的程序模块。然而,我们早已开始了面向对象的程序设计,类成为封装功能模块的基本单位。所以不仅需要对函数指针进行动态联编,更需要对类指针进行动态联编。幸运的是,C++的确为开发者提供了这一支持,它就是虚拟函数。
只要在类的非静态成员函数前加关键字virtual,这一函数就是虚拟函数。编译器对于虚拟函数采用动态联编方式。
2.2.1 实例:定义虚拟函数
那么,如何利用虚拟函数编写处理多种对象的通用程序呢?为通俗地阐述这一问题,下面讨论如何以虚拟函数的方式改写示例2.1。
(1)定义一个基类,名为CDispDecimal。该类封装一个整型数据成员Number、一个虚拟成员函数virtual DisplayFormat()。该虚拟函数将成员Number以十进制格式输出。
class CDispDecimal
{
public:
CDispDecimal(int i){Number=i;}
CDispDecimal(){Number=0;}
virtual DisplayFormat()
{
printf("The decimal value is %d\n",Number);
}
protected:
int Number;
};
(2)从基类CDispDecimal派生出两个子类,名为CDispOctal、CDispHexadecimal。这两个类都重载基类的虚拟函数DisplayFormat(),分别将Number以八进制、十六进制格式输出。
class CDispOctal :public CDispDecimal
{
public:
CDispOctal(int i){Number=i;}
CDispOctal(){Number=0;}
virtual DisplayFormat()
{
printf("The octal value is %o\n",Number);
}
};
class CDispHexadecimal: public CDispDecimal
{
public :
CDispHexadecimal(int i){Number=i;}
CDispHexadecimal(){Number=0;}
virtual DisplayFormat()
{
printf("The hexadecimal value is %x\n",Number);
}
};
因为编译器对虚拟函数动态联编,所以每个类的虚拟函数要能完成该类特有的功能,上面定义的3个类就是如此。注意,重载虚拟函数要求所有函数声明完全一致,即函数名称和形参列表都一致。否则等同于普通函数的重载,不能实现动态联编。
2.2.2 实例:编写通用函数
下面编写通用函数,它的一个形参是基类的指针,即CDispDecimal*。函数体内调用该基类的虚拟函数。
void DisplayNumber( CDispDecimal* DisplayFormat)
{
//调用以实参传入对象指针的虚拟函数,以某种格式输出整型数字
DisplayFormat->DisplayFormat();
}
从代码上也许看不出这个函数有什么通用的味道,但读者不要忽略这样一个事实:C++编译器可以直接将派生类的指针转换为基类指针。这样,调用该函数时,实参不仅可以是CDispDecimal对象的地址,也可以是CDispHexadecimal或DispOctal对象的地址。同时,虚拟函数采用动态联编,函数体内调用的虚拟函数DisplayFormat()并不一定是基类定义的,它将由实参决定,也可能是由CDispHexadecimal或DispOctal定义的。
2.2.3 实例:定义主函数
分别以CDispDecimal、CDispOctal、CDispHexadecimal对象的地址为实参调用通用函数DisplayNumber(),以3种格式输出整数。
int main(int argc, char* argv[])
{
CDispDecimal Deci;
CDispOctal Octa;
CDispHexadecimal Hexa;
//如果有数字形式的命令行参数,将其输出,否则输出0
if(argc>1)
{
/*因为这3个类都定义了int型的转换构造函数,即以int 为参数的构造函数,
所以下面可以直接赋值,而无需重载“=”运算符*/
Deci=atoi(argv[1]);
Octa=atoi(argv[1]);
Hexa=atoi(argv[1]);
}
//分别以3种格式将数字输出
DisplayNumber( &Deci); //将调用CDispDecimal类定义的虚拟函数DisplayFormat()
DisplayNumber( &Octa); //将调用CDispOctal类重载的虚拟函数DisplayFormat()
DisplayNumber( &Hexa); //将调用CDispHexadecimal类重载的虚拟函数DisplayFormat()
return 0;
}
命令行
c121.exe 50
的输出结果:
The decimal value is 50
The octal value is 62
The hexadecimal value is 32
上例定义的3个类分别封装了一种功能的实现,如果需要增加二进制格式的输出,只需再定义一个派生类CDispBinary,不必改写通用函数DisplayNumber()。
通过上例对虚拟函数的讨论,我们对它的应用价值已经有了一个深刻的认识。正如指针是C语言的灵魂,虚拟函数是C++的灵魂。MFC微软基础类库广泛应用了虚拟函数,增强其通用性。于是,利用MFC文档/视图框架,就可以编写出多种功能的应用程序。