虚函数使用方法很简单,直接在函数名前面添加关键字virtual声明即可,如果虚函数末尾增加=0则表示为纯虚函数,纯虚函数要求所有派生类都必须重写该该函数,带有纯虚函数的类我们也称为虚基类。
虚函数的实现,作为一个老生常谈的问题,要想透彻的讲明白,还是需要对底层机制有进一步的理解的。
问题抛出
基类指针为什么能调用子类的虚函数?
虚函数实现的关键原理和虚函数表指针vptr有莫大关系,vptr实际上是指向一个虚函数表(一维数组),该表存储了每个虚函数的函数地址,那么虚函数是如何借助这个vptr实现运行时对象的多态性,也就是我们常说的动态绑定。
从C++对象内存结构说起
阅读过《深度探索C++对象模型》的同学应该比较熟悉下面这个结构,每一个派生类对象实际上是由两个部分组成,如下面的图所示
- 父类的部分,包括成员变量、vptr、成员函数等,都是共享给子类(当然有一定的权限设置)
- 子类自身的部分,子类自己成员变量,成员函数
所以子类就是个特殊的父类,享有父类所有属性,是is-a的关系。所以子类也就能直接强制转换为父类,我们通常使用dynamic_cast将子类指针转为父类指针,那么这个父类指针的访问域也就变为内存模型中的上半部分,无法再访问子类的任何资源,但是有个例外,那就是虚表指针vptr,为什么呢?
我们来进一步看看vptr在类继承过程中到底是怎么变化?
通常函数地址都是在编译的时候就确定了,但是虚函数的调用地址需要到运行的时候才能确定,因为你无法确定一个基类的指针到底是执行基类对象还是子类对象。
实际上,虚函数表指针是在对象执行构造函数的确定的。对于基类来说,执行基类构造函数时,直接把虚函数表填充为基类的虚函数地址即可;对于派生类来说,派生类对象构造的时候,会先执行父类的构造函数(把虚函数表全部填充为基类的虚函数地址),然后再执行子类构造函数(对于子类重写的虚函数,修改虚函数表中对于的函数地址,将其改为子类的虚函数地址),具体过程如下图2个步骤所示:
动态绑定
有了以上基础后,回到之前的问题,动态绑定是怎么发生的?
现在回答这个问题很简单了,对于一个指向子类对象的基类指针,它的vptr其实在子类构造过程被改写过,所以使用基指针调用虚函数的时候,如果子类有重写,会调用子类的虚函数,如果没有重写,则直接调用基类的虚函数,这样就实现了运行时对象的多态性。
代码实例
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
virtual void sleep()
{
cout<<"animal sleep"<<endl;
}
virtual void breathe()
{
cout<<"animal breathe"<<endl;
}
string name;
};
class Fish:public Animal
{
public:
virtual void breathe()
{
cout<<"fish bubble"<<endl;
}
int skin;
};
int main()
{
Fish fh;
Fish *pFish = &fh;
Animal* pAnimal = dynamic_cast<Animal*>(pFish);
pAnimal->breathe();//fish bubble 执行子类重写的虚函数
pAnimal->sleep(); //animal sleep 执行基类的虚函数
pAnimal->name; //name位于基类域,能访问
// pAnimal->skin; skin位于子类域,无法访问
}