多态性是面向对象程序设计的关键技术之一。利用多态性技术,可以调用同一个函数名的函数,实现完全不同的功能。若程序设计语言不支持多态性,不能称为面向对象的语言。
在C++中有两种多态性:
- 编译时的多态性:通过函数的重载和运算符的重载来实现的。
- 运行时的多态性:在程序执行前,无法根据函数名和参数来确定该调用哪一个函数,必须在程序执行过程中,根据具体情况来动态地确定。它是通过类继承关系和虚函数来实现的,目的也是建立一种通用的程序。
虚函数的定义
◆ 1、定义格式
虚函数是一个类的成员函数,定义格式如下:
virtual 返回类型 函数名(参数表);
说明:
- 关键字virtual指明该成员函数为虚函数。virtual仅用于类定义中,如虚函数在类外定义,不可加virtual。
- 当某一个类的一个类成员函数被定义为虚函数,则由该类派生出来的所有派生类中,该函数始终保持虚函数的特征。
- 当在派生类中重新定义虚函数(overriding a virtual function,亦译作超载或覆盖)时,不必加关键字virtual。但重新定义时不仅要同名,而且它的参数表和返回类型全部与基类中的虚函数一样,否则联编时出错。
- 虚函数与“产生派生类”的第二步——改造类成员,同名覆盖(override)相区别:如未加关键字virtual,则是普通的派生类中的新成员函数覆盖基类同名成员函数(当然参数表必须一样,否则是重载),可称为同名覆盖函数,它不能实现运行时的多态性。
◆ 2、通过虚函数实现多态性
虚函数怎样实现多态性?请参见下面2个例子。
①【例8.6】计算学分。可由本科生类派生出研究生类,但它们各自的从课程学时数折算为学分数的算法是不同的,本科生是16个学时一学分,而研究生是20个学时一学分。赋值兼容规则与自定义的复制构造函数。
#include<iostream> #include<string> using namespace std; class Student{ string coursename; //课程名 int classhour; //学时 int credit; //学分,未考虑0.5学分 public: Student(){coursename="#";classhour=0;credit=0;} virtual void Calculate(){credit=classhour/16;} void SetCourse(string str,int hour){ coursename=str; classhour=hour; } int GetHour(){return classhour;} void SetCredit(int cred){credit=cred;} void Print(){cout<<coursename<<' '<<classhour<<"学时"<<' '<<credit<<"学分"<<endl;} }; class GradeStudent:public Student{ public: GradeStudent(){}; void Calculate(){SetCredit(GetHour()/20);} }; int main(){ Student s,*ps; GradeStudent g; s.SetCourse("物理",80); s.Calculate(); g.SetCourse("物理",80); g.Calculate(); cout<<"本科生:"<<' '; s.Print(); cout<<"研究生:"<<' '; g.Print(); s.SetCourse("数学",160); g.SetCourse("数学",160); ps=&s; ps->Calculate(); cout<<"本科生:"<<' '; ps->Print(); ps=&g; ps->Calculate(); cout<<"研究生:"<<' '; ps->Print(); getchar(); return 0; }
运行结果为:
本科生:物理 80学时 5学分
研究生:物理 80学时 4学分
本科生:数学 160学时 10学分
研究生:数学 160学时 8学分
分析:
第一行,学分是由Student类的成员函数Calculate()计算。
第二行,学分是由GradeStudent重新定义的Calculate()计算,它屏蔽了基类的同名函数。与不定义为虚函数一样,属编译时的多态性。
第三行,用的是指向Student类的对象s的指针,用的是Student类的Calculate()。
第四行,指针类型是指向基类的指针,但这里指针指向了派生类GradeStudent的对象g,按赋值兼容规则是准许的,但只能用基类的成员,可实际上用了派生类中新定义的Calculate()。这就是虚函数体现的多态性。如果不是虚函数,第四行输出是10学分。如果不使用基类指针指向派生类GradeStudent的对象g,也不能实现运行时的多态性。
②为体现虚函数的多态性的优点,可改造【例8.6】为
#include<iostream> #include<string> using namespace std; class Student{ string coursename; //课程名 int classhour; //学时 int credit; //学分,未考虑0.5学分 public: Student(){coursename="#";classhour=0;credit=0;} virtual void Calculate(){credit=classhour/16;} void SetCourse(string str,int hour){ coursename=str; classhour=hour; } int GetHour(){return classhour;} void SetCredit(int cred){credit=cred;} void Print(){cout<<coursename<<' '<<classhour<<"学时"<<' '<<credit<<"学分"<<endl;} }; class GradeStudent:public Student{ public: GradeStudent(){}; void Calculate(){SetCredit(GetHour()/20);} }; void Calfun(Student &ps,string str,int hour){ ps.SetCourse(str,hour); ps.Calculate(); ps.Print(); } int main(){ Student s; GradeStudent g; cout<<"本科生:"; Calfun(s,"物理",80); cout<<"研究生:"; Calfun(g,"物理",80);//派生类对象初始化基类的引用,只有calculate()为虚函数才能实现动态的多态性 return 0; }
运行结果为:
本科生:物理 80学时 5学分
研究生:物理 80学时 4学分
分析:
这里没有用指针,而用了Student的引用,正如在第四章中所叙述的对编译器而言引用的处理同样是通过地址间接完成的,所以引用也可以实现运行时的多态性。加了一个Calfun()函数,使用更为方便。
◆ 3、注意事项
一个类中将所有的成员函数尽可能地设置为虚函数总是有好处的,但必须注意以下几条:
- 派生类中定义的虚函数除必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是重载,而不是虚函数。如果基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外。
- 只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。
- 静态成员函数,是所有同类对象共有,不受限于某个对象,不能作为虚函数。
- 类的一个对象,可以有静态类型和动态类型,实质是相同的,但使用方式不同。实现动态多态性时,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针使用虚函数,才能实现动态的多态性。
- 内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。
- 析构函数可定义为虚函数,构造函数不能定义虚函数,因为在构造函数时对象还没有完成实例化。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。
- 虚函数执行速度要稍慢一些。因为,为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价,但通用性是一个更高的目标。
- 如果定义放在类外,virtual只能加在函数声明前面,不能(再)加在函数定义前面。正确的定义必须不包括virtual。