C++多态的概念及前提条件

在 C++ 程序中,每一个函数在内存中会分配一段存储空间,存储空间的起始地址则为函数的入口地址。例如我们在设计一个程序时都必须为程序设计一个主函数,主函数同样会在内存中被分配一段存储空间,这段存储空间的起始地址就是函数的入口地址。

前面列举的所有程序中,函数的入口地址与函数名是在编译时进行绑定的,我们称之为编译期绑定,而多态的功能则是将函数名动态绑定到函数入口地址,这样的动态绑定过程称为运行期绑定。换句话说,运行期绑定指的是函数名与函数入口地址在程序编译时无法绑定到一起,只有等运行的时候才确定函数名与哪一个函数入口绑定到一起。

那么多态到底有什么用处呢?我们不妨来看个例子。在 windows 操作系统中,我们经常会进行一些关闭操作,比如关闭文件夹、关闭文本文件、关闭播放器窗口等,这些关闭动作对应的 close() 函数假设都继承自同一个基类,但是每一个类都需要有自己的一些特殊功能,比如清理背景、清除缓存等工作。当执行 close() 函数时,我们当然希望根据当前所操作的窗口类型来决定该执行哪一个 close() 函数,因此运行期绑定就可以派上用场了。

编译期绑定是指在程序编译时就将函数名与函数入口地址绑定到一起,运行期绑定是指在程序运行时才将函数名与函数入口地址绑定到一起,而在运行期绑定的函数我们称其是多态的。

为了说明虚函数的必要性,我们先来看一个示例程序。

【例 1】
#include<iostream>
using namespace std;

class base
{
public:
    void display(){cout<<"I'm base class!"<<endl;}
};

class derived: public base
{
public:
    void display(){cout<<"I'm derived class!"<<endl;}
};

int main()
{
    base * p;
    derived test;
    p = &test;
    p->display();
    return 0;
}
这个例子非常简单,两个类,一个是 base 类,一个是 derived 类,二者构成继承关系,同时在这两个类中均含有一个 display() 函数,因为函数同名,故在派生类对象中会出现遮蔽现象,即派生类中的 display() 函数会遮蔽基类中的 display() 函数。

在主函数中,定义了一个基类类型的指针 p 和派生类对象 test,之后 p 指针指向派生类对象 test,然后通过指针调用 display() 函数。此程序最终运行结果如下:

I'm base class!

 从结果来看,这个程序最终调用的 display() 函数是基类的 display () 函数,而非派生类中的 display() 函数。但此程序的本意是先通过基类类型的指针根据所指向对象的类型来自动决定调用基类还是派生类的 display() 函数。为了实现这样的一种功能,C++ 提供了多态这一机制。

要想形成多态必须具备以下三个条件:
  • 必须存在继承关系;
  • 继承关系中必须有同名的虚函数;
  • 存在基类类型的指针或引用,通过该指针或引用调用虚函数。

根据这三个条件,我们将例 1 进行修改,使 display() 函数具有多态特性。修改后程序如例 2 所示。

【例 2】
#include<iostream>
using namespace std;

class base
{
public:
    virtual void display(){cout<<"I'm base class!"<<endl;}
};

class derived: public base
{
public:
    virtual void display(){cout<<"I'm derived class!"<<endl;}
};

int main()
{
    base * p;
    derived test;
    p = &test;
    p->display();
    return 0;
}
相对于例 1,例 2 只是在 display() 函数前各添加了一个 virtual 关键字。我们对照三个多态的构成条件来分析一下:
  • 多态需要继承关系,derived 类继承自 base 类,因此 base 类和 derived 类构成继承关系;
  • 多态需要同名的虚函数,base 类和 derived 类中都有 display() 函数,同名满足,同时通过添加关键字 virtual 后,display() 函数成为虚函数;
  • 多态需要通过基类类型的指针或引用来调用虚函数,在例 2 中的主函数中,p 即为基类类型指针,并且将该指针指向派生类对象,然后调用 display() 函数。

这段程序最终运行结果如下:

I'm derived class!

例 2 这个程序展示出来的就是多态,display() 函数通过 virtual 关键字声明为虚函数,具有多态特性。我们将例 2 中的主函数修改成以下形式再来分析一下函数运行结果。
int main()
{
    base * p = new base;
    p->display();
    delete p;
    p = new derived;
    p->display();
    delete p;
    return 0;
}
在这个主函数中,同样是声明一个基类类型的指针,之后通过 new 给指针分配一个基类对象,通过 p 指针调用 display() 函数,不用说肯定是输出“I'm base class!”,因为这中间一直没有涉及到派生类的事情。之后销毁之前 new 分配的 base 对象,然后通过 new 分配一个 derived 类对象,p 指向该派生类对象,通过 p 指针调用 display() 函数,此时的情况和例 2 是完全相同的,因此输出“I'm derived class!”,之后再 delete 销毁派生类对象。

修改主函数之后程序输出结果如下:

I'm base class!
I'm derived class!

这样的输出结果与我们的分析结果是一致的。