1 多态性
1.1 多态的概念
多态性:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(方法)。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。函数的重载、运算符重载都是多态现象。
C++中的多态:具有不同功能的函数可以用同一个函数名,这样就可以实现用一个函数名调用不同内容的函数。
1.2 多态的分类
多态性可以分为:静态多样性、动态多样性。
静态多态性又称编译时的多态性,通过函数重载实现。在程序运行前就已经决定了执行的函数和方法。
动态多样性又称运行时的多态性,通过虚函数实现,在程序运行过程中才动态地确定操作所针对的对象。
2 利用虚函数实现动态多态性
2.1 虚函数的作用
所谓虚函数,就是在基类声明函数是虚拟的,并不是实际存在的函数,然后在派生类中才正式定义此函数。
虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
通过虚函数与指向基类对象的指针变量的配合使用,就能实现动态的多态性。如果想调用同一类族中不同类的同名函数,只有先用基类指针指向该类对象即可。如果指针先后指向同一类族中的不同类的对象,就能不断地调用这些对象中的同名函数。
函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是不同派生层次上的同名函数问题。
下面给出两个例子,请注意观察未使用虚函数和使用虚函数得到的结果。example1.cpp是未使用虚函数的例子,example2.cpp是使用虚函数的例子。
example1.cpp
#include<iostream>
#include<string>
using namespace std;
//基类与派生类中有同名函数
class Student { //声明基类Student
public: //基类公用成员
Student(int, string, float); //声明构造函数
void display(); //声明输出函数
protected: //基类保护成员,派生类可以访问
int num;
string name;
float score;
};
//Student类成员函数的实现
Student::Student(int n, string nam, float s) { //定义构造函数
num = n;
name = nam;
score = s;
}
void Student::display() { //定义输出函数
cout << endl << " num:" << num << endl;
cout << " name:" << name << endl;
cout << " score:" << score << endl;
}
class Graduate :public Student { //以public方式声明派生类Graduate
public:
Graduate(int, string, float, float); //声明构造函数
void display(); //声明输出函数,与基类的输出函数同名
private:
float wage; //津贴
};
//Graduate类成员函数的实现
Graduate::Graduate(int n, string nam, float s, float w) :Student(n, nam, s), wage(w) {} //定义构造函数
void Graduate::display() { //定义输出函数
cout << " \n\nnum:" << num << "\n name:" << name << " \nscore:"
<< score << "\n wage:" << wage << endl;
}
//主函数
int main() {
Student stud1(1001, "Li", 87.5); //定义Student类对象stud1
Graduate grad1(2001, "Wang", 98.5, 1000); //定义Graduate类对象grad1
Student *pt = &stud1; //定义指向Student类对象的指针并指向stud1
pt->display(); //调用stud1.display函数
pt = &grad1; //指针指向grad1
pt->display(); //希望输出Graduate类对象grad1中的数据
system("pause");
return 0;
}
输出结果如图所示,学生stud1的全部数据都被输出,grad1的基类数据成员被输出,grad1的wage成员没有输出。说明调用了stud1的display函数。
现在思考一下为什么没有输出grad1的wage成员。
通过指向基类对象的指针,只能访问派生类中的基类成员,而不能访问派生类增加的成员。 pt是指向Student类对象的指针,因此即使指向grad1,实际上指向的也是grad1从基类继承的部分。即pt->display()调用的不是派生类Graduate对象所增加的display函数,而是基类的display函数。
如果想通过指针输出研究生grad1的wage成员,可以再定义一个指向派生类对象的指针变量ptr,使它指向grad1,然后用ptr->display()调用派生类对象的display函数。但这样比较复杂,而虚函数可以方便地解决这个问题。具体办法如下:
将基类Student中的display函数声明为虚函数,其他部分不做改动。
virtual void display();
在基类中的display被声明为虚函数,在声明派生类时被重载,这时派生类的同名函数display就取代了其基类中的虚函数。因此在使基类指针指向派生类对象后,调用display函数就调用了派生类的display函数。
example2.cpp
#include<iostream>
#include<string>
using namespace std;
//基类与派生类中有同名函数
class Student { //声明基类Student
public: //基类公用成员
Student(int, string, float); //声明构造函数
virtual void display(); //声明输出函数为虚函数
protected: //基类保护成员,派生类可以访问
int num;
string name;
float score;
};
//Student类成员函数的实现
Student::Student(int n, string nam, float s) { //定义构造函数
num = n;
name = nam;
score = s;
}
void Student::display() { //定义输出函数
cout << endl << " num:" << num << endl;
cout << " name:" << name << endl;
cout << " score:" << score << endl;
}
class Graduate :public Student { //以public方式声明派生类Graduate
public:
Graduate(int, string, float, float); //声明构造函数
void display(); //声明输出函数,与基类的输出函数同名
private:
float wage; //津贴
};
//Graduate类成员函数的实现
Graduate::Graduate(int n, string nam, float s, float w) :Student(n, nam, s), wage(w) {} //定义构造函数
void Graduate::display() { //定义输出函数
cout << " \n\nnum:" << num << "\n name:" << name << " \nscore:"
<< score << "\n wage:" << wage << endl;
}
//主函数
int main() {
Student stud1(1001, "Li", 87.5); //定义Student类对象stud1
Graduate grad1(2001, "Wang", 98.5, 1000); //定义Graduate类对象grad1
Student *pt = &stud1; //定义指向Student类对象的指针并指向stud1
pt->display(); //调用stud1.display函数
pt = &grad1; //指针指向grad1
pt->display(); //调用grad1.display函数
system("pause");
return 0;
}
输出结果如下图所示。学生stud1的全部数据都被输出,grad1的全部数据也被输出。说明调用了grad1的display函数。
2.2 静态关联和动态关联
确定调用的具体对象的过程称为关联。在这里是指把一个函数名与一个类对象捆绑在一起,建立关联。函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于哪一个类,其过程称为静态关联,也称早期关联。
在运行阶段确定关联关系称为动态关联,通过基类指针和虚函数结合实现多态就是动态关联的一个例子。先定义一个指向基类的指针变量,并使它指向相应的类对象,然后通过这个基类指针去调用虚函数。运行阶段基类指针指向了确定的对象,调用该对象中的函数,调用哪一个对象的函数确定无疑,这就是动态关联。动态关联也称为滞后关联。
2.3 虚函数的注意事项
- 只能用virtual声明类的成员函数,使其作为虚函数。不能声明类外的普通函数为虚函数。
- 一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数。
- 当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。
- 在派生类中重新定义此函数,函数名、函数类型、函数参数个数和类型必须与基类的虚函数相同,根据派生类的需要重新定义函数体。
- 使用虚函数,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表,是一个指针数组,存放每个虚函数的入口地址。
2.4 虚析构函数
当派生类的对象从内存在撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。但是,如果用new运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针变量。在程序用带指针参数的delete运算符撤销对象时,系统只会执行基类的析构函数,而不执行派生类的析构函数。
可以把基类的析构函数声明为虚函数,这样所有派生类的析构函数自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。如果程序中显式地用了delete运算符准备删除一个对象,而delete运算符的操作对象用了指向派生类对象的基类指针,则系统会采用动态关联,调用相应类的析构函数,以保证在撤销动态分配空间时得到正确的处理。
构造函数不能声明为虚函数。这是因为在执行构造函数时类对象还未完成建立过程,当然谈不上把函数与类对象的绑定。
参考文献
《C++程序设计(第三版)》谭浩强编著