首页 > 技术文章 > C++虚函数的简单实现原理

JCpeng 2021-08-03 21:39 原文

C++中的虚函数的作用主要是实现多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。

虚函数表

C++ 虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为vtable。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

这里我们着重看一下这张虚函数表。在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。具体的用法如下:

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
private:
    int a
};

class B : public A {
public:
    void vfunc2();
private:
    int b;
};

因为类A中定义了虚函数vfunc1( ),和vfunc2( ),所以编译器为A类准备了一个虚表vtableA,内容如下:

A::vfunc1的地址
A::vfunc2的地址

类B因为继承了A,所以编译器也为B准备了一个虚表vtableB,内容如下:

A::vfunc1的地址
B::vfunc2的地址

因为类B中重写了A的vfunc2,所以B的虚表的vfunc2放的是B::vfunc2的入口地址,但是vfunc1是从上面的A继承下来的,所以vfunc2的地址是A::vfunc2的入口地址。

虚函数指针

当主程序需要声明B类对象b的时候,编译器分配空间时,除了A的成员的int a,B的成员int b;以外,还需要分配一个虚指针vptr,指向B的虚表vtableB,类B的成员b的布局如下:

vptr : 指向B的虚表vtableB
int a: 继承A的成员
int b: B成员

当如下语句的时候:

B b;
A *pa = &b;

pa的结构就是A的布局(就是说用pa只能访问的到b对象的前两项,访问不到第三项int b)

那么pa->vfunc2( )中,编译器知道的是,vfunc2是一个声明为virtual的成员函数,而且其入口地址放在表格(无论是vtalbeA表还是vtalbeB表)的第2项,那么编译器编译这条语句的时候就如是转换:call *(pa->vptr)[1](C语言的数组索引从0开始)。

这一项放的是B :: vfunc2( )的入口地址,这就实现了多态(注意b的vptr指向的是B的虚表vtableB)。

推荐阅读