派生类可以使用基类的方法而不做任何修改,但我们并不是总是希望同一个方法在基类和派生类中的行为是相同的。我们可以借助虚函数实现多态公有继承。
虚函数
声明与定义
下面举例说明虚函数
cpp
#ifndef BRASS_H
#define BRASS_H
#include <string>
class Brass
{
private:
std::string fullName;
long acctNUm;
double balance;
public:
...
virtual void ViewAcct() const;//基类添加virtual关键字声明虚函数
virtual ~Brass() {}//虚函数只能是成员函数,友元不行
};
class BrassPlus:public Brass
{
private:
double maxLoan;
dohble rate;
double owesBank;
public:
...
virtual void ViewAcct() const;//基类中已声明虚函数,这里的virtual是可选的
}
声明了虚函数之后,程序将根据调用方法的对象类型来确定使用哪个版本。
cpp
Brass dom;
BrassPlus dot;
dom.ViewAcct();//使用Brass版本
dot.ViewAcct();//使用BrassPlus版本
特别地,如果方法是通过引用或指针而不是对象调用的,程序将根据引用或指针指向的对象的类型来选择方法(前提是虚函数,否则根据引用或指针的类型)。
注意:
- 在函数定义部分无需使用virtual关键字标明虚函数。
- 含继承关系时,应尽量将析构函数设置为虚函数。这样能保证无论指针指向的是基类还是派生类,都会调用对象对于类型的析构函数,避免错误析构。
- 在派生类中重新定义(参数列表不同)虚函数将隐藏基类的所有同名方法。 (但特殊地,允许返回类型是基类引用或指针时,可修改为指向派生类的引用或指针)
- 如果基类声明被重载,则应在派生类中重新定义所有的接力版本,未定义的版本在派生类中将被隐藏。
静态联编和动态联编
将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编 。在C语言中这项任务很简单,因为每个函数名都对应一个不同的函数。但C++由于函数重载等特性,这项任务变得更复杂。C/C++都能在编译过程中进行联编,这被称为静态联编。
但虚函数使这项工作变得复杂,因为使用哪个函数无法在编译过程中确定,这取决于对象的类型。所以编译器必须生成在程序运行时选择正确的虚方法代码,这被称为动态联编。
cpp
BrassPlus ophelia;
Brass*bp;
bp=&ophelia;
bp->ViewAcct();//执行哪个版本?
正如前介绍,若ViewAcct()不是虚函数,则将根据指针类型调用Brass::ViewAcct()。指针在编译时已知,因此在编译时会将ViewAcct()关联到Brass::ViewAcct。总之编译器对非虚函数使用静态联编。
若ViewAcct()是虚函数,则将根据对象类型调用BrassPlus::ViewAcct()。但在运行时才能确定对象类型,所以是动态联编。
虚函数工作原理
通常,编译器处理虚函数的方法是:给每一个对象添加一个隐藏成员。隐藏成员包含了一个指向函数地址数组的指针,被称为虚函数表。每声明一个虚函数都将向虚函数表中添加一个函数地址(若派生类中重新定义基类虚函数则对应函数地址处于新定义位置)。调用虚函数时将按照以下四个步骤:
- 获悉对象成员中指向虚函数表的指针的地址。
- 前往该指针指向地址的虚函数表。
- 获悉表中对应函数的地址。
- 前往函数定义地址并执行这里的函数。
纯虚函数
并非所有继承都使用is-a规则。例如,我们知道圆是特殊的椭圆,如果我们从椭圆派生到圆就会使得派生类多出许多无用的成员,导致信息冗余。于是我们可以从椭圆和圆中抽象出它们的共性,将这些特性放到一个抽象类(ABC)中,从抽象类分别派生出圆和椭圆。
抽象类中,通过将成员函数声明为纯虚类来指出其在各个派生类中的实现不一定相同,相同的则使用非纯虚函数。
cpp
class BaseEllipse
{
private:
double x;
double y;//(椭)圆心坐标
...
public:
BaseEllipse(double x0=0,double y0=0):x(x0),y(y0){}
virtual ~BaseEllipse(){}//虚析构函数
void Move(int nx,ny){x=nx,;y=ny}//派生类实现相同的函数不设为纯虚函数
virtual double Area() const=0;//派生类实现不同的函数设置为纯虚函数
...
}
当类声明中包含纯虚函数时,表明这是一个抽象类,只能作为基类且不能创建该类的对象。纯虚函数是否被定义是可选的,若被定义了也可在派生类中使用作用域解析运算符::进行访问。