“虚函数表”推演及多态的原理

C++ 的多态性据前辈们所说,是非常难以理解的一部分内容,虽然他实现很简单,但是套用到各种设计模式后,你会非常难以理解,但无论怎样,笔者始终认为,如果了解了内部的实现原理,实际就不会那么难了。本文将介绍虚函数表的相关内容,阐述了它与多态之间难以割舍的关系。

默认情况下,一个没有任何成员变量的类,大小是 1 个字节,如下所示:

#include <iostream>
 
using namespace std;
 
class A
{
public:
    void func()
    {
        ;
    }
};
 
int main(int argc, char* argv[])
{
    cout << "class A size = " << sizeof(A) << endl;
    return 0;

}

2015-05-25_112335

给 func 函数增加一个 virtual 关键字后,class A 的大小就变成了 4 个字节。

#include <iostream>
 
using namespace std;
 
class A
{
public:
    virtual void func()
    {
        ;
    }
};
 
int main(int argc, char* argv[])
{
    cout << "class A size = " << sizeof(A) << endl;
    return 0;
}

2015-05-25_112555

这中间多了什么东西?使用VS调试一下我们可以看到,a对象中,多了一个成员,是_vfptr,如下图:

2015-05-25_112954

这是一个函数指针数组,里面包含了所有类中虚函数的指针。我们案例中只有一个虚函数,所以只看到一个,如果我们多写几个虚函数的话,就能在这个数组中看到多个函数指针。如下图:

2015-05-25_113346

我们称之为这个内建的隐藏数组为 “虚函数表” (virtual Table、v-Table)。下图为该函数表的形象图:

2015-05-25_123821

【代码推演】

#include <iostream>
 
using namespace std;
 
class A
{
public:
    virtual void func(){ cout << "class A func" << endl; }
    virtual void func1(){ cout << "class A func1" << endl; }
    virtual void func2(){ cout << "class A func2" << endl; }
};
 
int main(int argc, char* argv[])
{
    A a;
 
    typedef void(*Fun)();
    Fun pFun = NULL;
 
    cout << "object a address = " << &a << endl;
 
    // 虚函数表的地址存放在类对象内存的最起始位置的 4 个字节处
    // 而 &a 是一个对象,他的大小由类中的成员决定
    // 我们只想要前 4 个字节里面的内容
    // 所以把强制转换成 int* 类型        (int*)&a
    // 再打印解引用后的内容,就得出了前 4 个字节里面存放的数据。    *((int*)&a)
    // 这个内容被解引用后会被解释成 int 类型的数据,而非 int* 类型
    // 所以还需要再强制转换一次为 int*    (int*) (*((int*)&a))
    // 最后得出的就是 4 个字节的虚函数表 _vfptr 的起始地址
 
    cout << "object a _vfptr address = " << (int*)(*((int*)&a)) << endl;
 
    // 得到了虚函数表的起始地址后想调用表中的第一个函数
    // 就需要对地址解引用,得出第一个函数的地址  *((int*)(*((int*)&a)))
    // 然后将其强制转换为一个函数指针,进行调用
 
    cout << "_vfptr func address = " << (int*) *((int*)(*((int*)&a)) + 0) << "\t\t";
    pFun = (Fun) *((int*)(*((int*)&a)));
    pFun();
 
    // 如果想调用第二个函数,那么在这个地址的基础上+1就得到了第二个函数的地址
 
    cout << "_vfptr func1 address = " << (int*) *((int*)(*((int*)&a)) + 1) << "\t\t";
    pFun = (Fun) *((int*)(*((int*)&a)) + 1);
    pFun();
 
    // 一次类推,+2就得到了第三个函数的地址
 
    cout << "_vfptr func2 address = " << (int*) *((int*)(*((int*)&a)) + 2) << "\t\t";
    pFun = (Fun) *((int*)(*((int*)&a)) + 2);
    pFun();
 
    return 0;

}

2015-05-25_122221

以上,我们只是证实了一下虚函数表的存在,并通过间接的手段调用了一次虚函数表里面的函数。当然我们并不是单纯的只是让大家知道他的存在,而是结合虚函数表,引导大家学习多态的实现。

我们写了一个子类,继承了类 A,并且,在子类中编写了一个与类 A 中同名、同返回值、同参数(同类型、同位置)的函数 func,如下:

class B : public A
{
public:
    void func(){ cout << "class B func" << endl; }

};

此时,我们生成一个类 B 的对象,当这个对象构造完毕时,我们再次调试查看它继承的类 A 中的虚函数表中的内容。

2015-05-25_144409

很明显我们发现,继承下来的类 A 中的虚函数表第一个函数变成了 B::func,实际上,这个操作只是将虚函数表中的函数指针进行了覆盖。这种方式我们就称为覆写。当你使用子类对象初始化一个父类的指针时。这个指针在调用 func 函数时,会优先遍历虚函数表,如果发现同名函数,则调用之。如果没有发现再到非虚函数表以外的成员方法中寻找。这便是“多态”

// 会调用已经覆写的 B 类的 func 函数
A *pb = new B;
pb->func();

说说你的想法