2.4 多态和虚函数

多态性是面向对象程序设计的重要特性之一,它与封装性和继承性构成了面向对象程序设计的三大特性。所谓多态性,是指不同类型的对象接收相同的消息时产生不同的行为。这里的消息主要是指对类的成员函数的调用,而不同的行为是指成员函数的不同实现。例如,函数重载就是多态性的典型例子之一。

2.4.1 多态概述

在C++中,多态性可分为两种:编译时的多态性和运行时的多态性。编译时的多态性是通过函数或运算符的重载来实现的。而运行时的多态性是通过虚函数来实现的,它指在程序执行之前,根据函数和参数还无法确定应该调用哪一个函数,必须在程序的执行过程中,根据具体的执行情况动态地确定。

与这两种多态性方式相对应的是两种编译方式:静态联编和动态联编。所谓联编(binding,又称绑定),就是将一个标识符和一个存储地址联系在一起的过程,或是一个源程序经过编译、连接,最后生成可执行代码的过程。

静态联编指这种联编是在编译阶段完成的,由于联编过程是在程序运行前完成的,所以称为早期联编。动态联编是指这种联编要在程序运行时动态进行,所以又称晚期联编。

一般来说,在静态联编的方式下,同一个成员函数在基类和派生类中的不同版本是不会在运行时根据程序代码的指定进行自动绑定的。因此,必须通过类的虚函数机制,才能实现基类和派生类中的成员函数不同版本的动态联编。

2.4.2 虚函数

先来看一个虚函数应用实例。

【例Ex_VirtualFunc】 虚函数的使用

#include <iostream.h>
class CShape
{
public:
    virtual float area()               // 将area定义成虚函数
    {   return 0.0; }
};
class CTriangle : public CShape
{
public:
    CTriangle(float h, float w)
    {   H=h;    W=w;   }
    float area()
    {   return(float)(H*W*0.5); }
private:
    float H, W;
};
class CCircle : public CShape
{
public:
    CCircle(float r)
    {   R=r;
    }
    float area()
    {   return(float)(3.14159265*R*R);
    }
private:
    float R;
};
int  main()
{
    CShape *s[2];
    s[0] = new CTriangle(3,4);
    cout<<s[0]->area()<<endl;
    s[1] = new CCircle(5);
    cout<<s[1]->area()<<endl;
    return 0;
}

程序运行结果如下:

6

78.5398

代码中,虚函数area是通过在基类的area函数的前面加上virtual关键字来实现的。程序中*s[2]是定义的基类CShape指针,语句“s[0]=new CTriangle(3,4);”是将s[0]指向派生类CTriangle,因而“s[0]->area();”实际上是调用CTriangle类的area成员函数,结果是6;同样可以分析s[1]->area()的结果。

从这个例子可以看出,正是通过虚函数,达到了用基类指针访问派生类对象成员函数的目的,从而使一个函数具有多种不同的版本,这一点与重载函数相似,只不过虚函数的不同版本是在该基类的派生类中重新进行定义的。这样,只要声明了基类指针就可以使不同的派生类对象产生不同的函数调用,实现了程序的运行时多态。

需要说明的是:

(1)虚函数在重新定义时,参数的个数和类型必须和基类中的虚函数完全匹配,这一点和函数重载完全不同。

(2)只有通过基类指针才能实现虚函数的多态性,若虚函数的调用是通过普通方式来进行的,则不能实现其多态性。例如:

CShape  ss;
cout<<ss.area()<<endl;

输出的结果为0.0。

(3)如果不使用new来创建相应的派生类对象指针,也可通过使用&运算符来获取对象的地址。例如:

void main()
{
    CShape *p1, *p2;
    CTriangle tri(3, 4);
    CCircle cir(5);
    p1=&tri; p2=&cir;
    cout<<p1->area()<<endl;
    cout<<p2->area()<<endl;
}

(4)虚函数必须是类的一个成员函数,不能是友元函数,也不能是静态的成员函数。

(5)可把析构函数定义为虚函数,但不能将构造函数定义为虚函数。通常在释放基类中及其派生类中动态申请存储空间时,也要把析构函数定义为虚函数,以便实现撤销对象时的多态性。

2.4.3 纯虚函数和抽象类

在定义一个基类时,有时会遇到这样的情况:无法定义基类中虚函数的具体实现,其实现完全依赖于其不同的派生类。例如,一个“形状类”(基类)由于没有确定的具体形状,因此其计算面积的函数也就无法实现。这时可将基类中的虚函数声明为纯虚函数

声明纯虚函数的一般格式为:

virtual <函数类型><函数名>(<形参表>) = 0;

显然,它与一般虚函数不同的是:在纯虚函数的形参表后面多了个“= 0”。把函数名赋为0,本质上是将指向函数的指针的初值赋为0。需要说明的是,纯虚函数不能有具体的实现代码。

抽象类是指至少包含一个纯虚函数的特殊的类。它本身不能被实例化,也就是说不能声明一个抽象类的对象。必须通过继承得到派生类后,在派生类中定义了纯虚函数的具体实现代码,才能获得一个派生类的对象。

下面举例说明纯虚函数和抽象类的应用。

【例Ex_PureVirtualFunc】 纯虚函数和抽象类的使用

#include <iostream.h>
class CShape
{
public:
    virtual float area()=0;               // 将area定义成纯虚函数
};
class CTriangle:public CShape
{
public:
    CTriangle(float h, float w)
    {   H=h;    W=w;
    }
    float area()                         // 在派生类中定义纯虚函数的具体实现代码
    {   return(float)(H*W*0.5);
    }
private:
    float H, W;
};
class CCircle:public CShape
{
public:
    CCircle(float r)
    {   R=r;
    }
    float area()                         // 在派生类中定义纯虚函数的具体实现代码
    {   return(float)(3.14159265*R*R);
    }
private:
    float R;
};
int  main()
{
    CShape *pShape;
    CTriangle tri(3, 4);
    cout<<tri.area()<<endl;
    pShape = &tri;
    cout<<pShape->area()<<endl;
    CCircle cir(5);
    cout<<cir.area()<<endl;
    pShape = &cir;
    cout<<pShape->area()<<endl;
    return 0;
}

程序运行结果如下:

6

6

78.5398

78.5398

从这个示例可以看出,与虚函数使用方法相同,也可以声明指向抽象类的指针,虽然该指针不能指向任何抽象类的对象(因为不存在),但可以通过该指针获得对派生类成员函数的调用。事实上,纯虚函数是一个特殊的虚函数。