c++中虚基类表和虚函数表的布局

时间:2021-11-23 20:57:29

本文涉及到C++中对象的内存布局知识,若无该方面基础建议先阅读haoel(陈皓)专栏的C++对象内存布局的博客:
http://blog.csdn.net/haoel/article/details/3081328

在拜读上述博客之后,我深受启发,且对C++关于虚函数表的问题有了新的认识和疑惑。比如,在上述博客的最后作者抛出了一个问题:在VS环境下,对象虚函数表指针的下一个字中,存储的不是对象的成员变量,而是一个取值后为-4的地址。这个地址代表什么呢?虚函数表上是如何放置基类和派生类的虚函数呢?

注意:对于在对象中存取虚基类的问题,虚基类表仅是Microsoft编译器的解决办法。在其他编译器中,一般采用在虚函数表中放置虚基类的偏移量的方式。

通过实验,我得到了这样的结论:
1. 一个对象实例只有一个虚函数表,只有一个虚基类表。
2. 对象的每个基类都有一个属于自己的虚函数表指针(vfptr)指向虚函数表(vftbl)的某一项,都有一个属于自己的虚基类表指针(vbptr)指向虚基类表(vbtbl)的某一项。
3. 虚函数表中按照对象继承的顺序排列对象的虚函数地址,虚基类表中按照对象继承的顺序排列对象的直接虚继承类到虚基类的偏移。
4. 当基类无虚函数,且派生类有独立虚函数时,派生类对象起始位置为自己的虚函数表指针。否则派生类的虚函数会归到第一个带虚函数表指针的基类的虚函数表指向范围,这样就节省了一个vfptr的空间。

沿用上述博客的代码:

class B
{
public:
    int ib;
    char cb;
public:
    B() :ib(0), cb('B') {}

    virtual void f() { cout << "B::f()" << endl; }
    virtual void Bf() { cout << "B::Bf()" << endl; }
};
class B1 : virtual public B
{
public:
    int ib1;
    char cb1;
public:
    B1() :ib1(11), cb1('1') {}

    virtual void f() { cout << "B1::f()" << endl; }
    virtual void f1() { cout << "B1::f1()" << endl; }
    virtual void Bf1() { cout << "B1::Bf1()" << endl; }


};
class B2 : virtual public B
{
public:
    int ib2;
    char cb2;
public:
    B2() :ib2(12), cb2('2') {}

    virtual void f() { cout << "B2::f()" << endl; }
    virtual void f2() { cout << "B2::f2()" << endl; }
    virtual void Bf2() { cout << "B2::Bf2()" << endl; }

};

class D : public B1, public B2
{
public:
    int id;
    char cd;
public:
    D() :id(100), cd('D') {}

    virtual void f() { cout << "D::f()" << endl; }
    virtual void f1() { cout << "D::f1()" << endl; }
    virtual void f2() { cout << "D::f2()" << endl; }
    virtual void Df() { cout << "D::Df()" << endl; }

};
实验方法:

int main(void)
{
    typedef void(*F)();
    D d;

    int *op = reinterpret_cast<int*>(&d);    //1 
    int *vfptr = reinterpret_cast<int*>(*op);//2
    F fun = reinterpret_cast<F>(*(vfptr));   //3

    fun();


    return 0;
}

运行结果为:
D::f1()

将上述语句中3号语句加1呢?
F fun = reinterpret_cast(*(vfptr+1));
运行结果为:
B1::Bf1()

因此说明基类B1的虚函数表指针指向虚函数表挂载着基类B1可调用的虚函数,此时共有3个,指针偏移到3的时候fun的值为0,即VS编译器用来标识的终结。但如果继续增加偏移量呢?

将上述语句中3号语句加4呢?
运行失败,编译器显示此时fun访问一个名为RTTICompleteObjectLocator的类型对象指针。此类型用来进行运行时类型识别,该类型对象存储由编译器生成的特殊类型信息,包括对象继承关系,对象本身的描述。一般放在基类虚函数表之前的一个字中。因此此时fun内存储的是与B2对应的RTTICompleteObjectLocator类型指针。

将上述语句中3号语句加5呢?
运行结果为:
D2::f2()

综上所述,我们可以得到虚函数表的构造:

c++中虚基类表和虚函数表的布局

下一个实验,我们探索虚基类表:

int main(void)
{
    D d;

    int *op = reinterpret_cast<int*>(&d);        //1
    int *vbptr = reinterpret_cast<int*>(*(op+1));//2
    int offset = *vbptr;                         //3

    std::cout << offset << endl;

    return 0;
}

运行结果为:
-4

这个-4是什么呢?其实此时vbptr为基类B1的虚基类表指针,位于vfptr的下方(从内存布局角度看是下方,但是地址更高)。对虚基类表的第一项取值得到的就是-4,表示虚基类表指针到基类B1的偏移量。

如果将3号语句的vbptr加1呢?
运行结果为:
40

此时40表示从基类B1的vbptr到B1虚继承的基类B的偏移量。我们按照我引用的文章给的内存布局推算,正好是40个字节。

如果将3号语句的vbptr加2呢?
运行结果为:
0

此处的0也是用来标识B1虚基类表的终结。

如果将3号语句的vbptr加3呢?
运行结果为:
-4

即B2的虚基类表指针到B2对象的偏移量。

如果将3号语句的vbptr加4呢?
运行结果为:
24

即B2的虚基类表指针到其虚继承的基类B对象的偏移量。

综上所述,我们可以得到虚基类表的构造:

c++中虚基类表和虚函数表的布局

注意:测试不同类型数据的时候发现虚函数表上不同类型到基类的offset分布未必相邻!。