转自:http://www.educity.cn/zk/gjyy/201306271108011682.htm
多态性是面向对象设计语言的基本特征。仅仅是将数据和函数捆绑在一起,进行类的封装,使用一些简单的继承,还不能算是真正应用了面向对象的设计思想。多态性是面向对象的精髓,也是难点。在C++中,多态性是通过虚函数来实现的。
1. 为什么需要虚函数
为了说明虚函数的作用,我们先看一个程序实例:
#include <iostream.h>
class vehicle{
int wheels;
float weight;
public:
void message(void) {cout << "Vehicle message\n";}
};
class car : public vehicle{
int passenger_load;
public:
void message(void) {cout << "Car message\n";}
};
class truck : public vehicle{
int passenger_load;
float payload;
public:
int passengers(void) {return passenger_load;}
};
class boat : public vehicle{
int passenger_load;
public:
int passengers(void) {return passenger_load;}
void message(void) {cout << "Boat message\n";}
};
int main(){
vehicle *unicycle;
car *sedan;
truck semi;
boat sailboat;
unicycle = new vehicle;
unicycle-> message(); //输出Vehicle message
delete unicycle;
unicycle = new car;
unicycle -> message(); //输出Vehicle message
sedan = (car *) unicycle;
sedan -> message(); //输出Car message
delete sedan;
semi.message(); //输出Vehicle message
sailboat.message(); //输出Boat message
}
该程序的运行结果,我们已经标注在程序之中。因为指针的类型决定调用那一个成员函数,所以,一个vehicle*调用vehicle成员函数,即使它指向派生类的对象。同样,一个car *也调用car 的成员函数。我们把这称为早期联编或静态联编,因为指针要调用那一个函数是在编译时就确定的。
那么,当vehicle*指向派生类对象时,我们能不能通过该指针来调用派生类的成员函数呢?在C++中,我们是可以作到的,这要用到C++的多态特性。 也就是说,基类指针是调用基类的成员函数,还是调用派生类的成员函数,不是由指针的类型决定的,而是由指针指向的对象的类型决定的。
2. 什么是多态
多态也称为动态联编或迟后联编,因为到底调用哪一个函数,在编译时不能确定,而要推迟到运行时确定。也就是说,要等到程序运行时,确定了指针所指向的对象的类型时,才能够确定。在C++中,动态联编是通过虚函数来实现的。
我们知道,函数调用是通过相应的函数名来实现的。对于源程序进行编译后,存放在内存中的可执行程序,函数实际上是一段机器代码,它是通过首地址进行标识和调用的。例如,假定定义一个函数:
void func(){
//…
};
我们可以用下面的语句调用这个函数:
func(); //调用func函数
这是在源程序中调用函数的方法,它是用函数名操作的。下面我们看看在可执行程序中函数调用是怎么操作的,我们用汇编语言来说明,因为汇编语言和机器语言(计算机可以直接执行的语言)是一一对应的。
在可执行程序中,函数调用使用下面的方法:
call [xxxxx]
xxxxx代表存放函数代码内存空间的首地址。
call是汇编语句中的一条指令,意思是调用一个函数。实际操作过程是:保存当前地址、保护现场,跳转到xxxxx地址执行。正是基于这个原因,在C/C++中的函数名是一个指针,该指针指向该函数段代码在内存中的首地址。如何将源程序中的函数调用和函数体(也就是在内存中该函数的机器代码)联系起来呢?这件工作是由编译器和连接程序来完成的。
在C/C++语言中,函数调用在程序运行之前就已经和函数体(函数的首地址)联系起来。编译器把函数体翻译成机器代码,并记录了函数的首地址。 在对函数调用的源程序段进行编译的时候,编译器知道这个函数名的首地址在那里(它可以从生成的标识符表中查到这个函数名对应的首地址),然后将这个首地址替换函数名,一并翻译成机器码。这种编译方法称为早期或静态联编。
那么,当vehicle*指向派生类对象时,我们能不能通过该指针来调用派生类的成员函数呢?从这种编译方法来看,是不可能的。因为编译器只会寻找vehicle*的成员函数。如何实现这个功能:当用基类指针调用成员函数时,是调用基类的成员函数,还是调用派生类的成员函数,不由指针的类型决定,而由指针指向的对象的类型决定呢?也就是说,如果基类指针指向基类对象,就调用基类的成员函数,如果基类指针指向派生类对象,就调用派生类的成员函数。这就要用到另外一种方法,称为动态联编或迟后联编。到底调用哪一个函数,在编译时不能确定,而要推迟到运行时确定。在C++中,动态联编是通过虚函数来实现的。下面我们先介绍虚函数,然后讨论动态联编实现的原理。
3. 为什么使用虚函数
使用虚函数,我们可以获得良好的可扩展性。在一个设计比较好的面向对象程序中,大多数函数都是与基类的接口进行通信。因为使用基类接口时,调用基类接口的程序不需要改变就可以适应新类。如果用户想添加新功能,他就可以从基类继承并添加相应的新功能。
4. 虚函数的特点
虚函数的定义很简单,只要在成员函数原型前加一个关键字virtual即可。如果一个基类的成员函数定义为虚函数,那么,它在所有派生类中也保持为虚函数,即使在派生类中省略了virtual关键字。需要注意的是:要达到动态联编的效果,基类和派生类的对应函数不仅名字相同,而且返回类型、参数个数和类型也必须相同。
基类vehicle的成员函数message被定义为虚函数,虽然其派生类中的message成员函数定义时,没有virtual关键字,但都是虚函数。如果派生类中有与基类对应的方法,并且基类指针指向派生类的对象,那么基类指针调用的方法是派生类的方法。
如果将vechicle中的message()函数定义为虚函数,那么main函数中四个相同的语句“unicycle->message();”,它们的结果并不相同,结果依次是:vechicle message, car message, car message, vechicle message, boat message。哪一个类的message成员函数被调用,不是在编译时确定的,而是根据运行时unicycle指针指向的对象的类型确定的。由于类truck没有覆盖基类的message成员函数,系统调用基类的message成员函数。
一个有意思的情况:如果在类car中,将虚函数void message(void)声明为private,那么,car sedan将无法调用message()函数(这是因为message()是car类的私有函数,实例不能调用),而vehicle *unicycle = &sedan,却可以调用message()函数,且调用的是car类中的message()函数。
5. 什么是纯虚函数及纯虚函数的作用
纯虚函数是一种特殊的虚函数。纯虚函数的定义如下:
class vehicle{
int wheels;
float weight;
public:
virtual void message(void) = 0; //纯虚函数
};
纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:
virtual 函数类型 函数名(参数表) =0;
声明为纯虚函数之后,基类中就不再给出函数的实现部分。纯虚函数的函数体由派生类给出。含有纯虚函数的类称为虚类,又叫做抽象类,不能被实例化。那这种虚类为什么会存在呢?这是因为,有些定义的基类,并不适合被实例化。比如交通工具可以是车、船等,但交通工具本身是一个抽象的概念,不对应着某一具体的事物。所以,可以以虚类为基类,然后由派生类来实现虚基类中的纯虚函数,再实例化这个派生类。