c++对象模型05:虚继承内存布局

虚继承

虚继承解决了菱形继承中最派生类拥有多个间接父类实例的情况。虚继承的派生类的内存布局与普通继承很多不同,主要体现在:

  • 虚继承的派生类,如果定义了新的虚函数,则编译器为其生成一个虚函数指针(vptr)以及一张虚函数表。该vptr位于对象内存最前面。(非虚继承时,派生类新的虚函数直接扩展在基类虚函数表的下面。)
  • 虚继承的派生类有单独的虚函数表,基类也有单独的虚函数表,两部分之间用一个四个字节的0x00000000来作为分界。
  • 虚继承的派生类对象中,含有四字节的虚基表指针。

在C++对象模型中,虚继承而来的派生类会生成一个隐藏的虚基类指针(vbptr),在Microsoft Visual C++中,虚基类表指针总是在虚函数表指针之后,因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。
一个类的虚基类指针指向的虚基类表,与虚函数表一样,虚基类表也由多个条目组成,条目中存放的是偏移值。第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由上面的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。虚基类表的第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值。我们通过一张图来更好地理解。

在这里插入图片描述

代码

class B

{

public:

    int ib;

public:

    B(int i = 1) :ib(i) {}

    virtual void f() { cout << "B::f()" ; }

    virtual void Bf() { cout << "B::Bf()" ; }

};

class B1 : virtual public B

{

public:

    int ib1;

public:

    B1(int i = 100) :ib1(i) {}

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

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

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



};

对象模型

代码演示

typedef void(*Fun)(void);
int main()
{
    B1 a;
    cout << "B1对象内存大小为:" << sizeof(a) << endl;

    //取得B1的虚函数表
    cout << "[0]B1::vptr";
    cout << "\t地址:" << (int*)(&a) << endl;

    //输出虚表B1::vptr中的函数
    for (int i = 0; i < 2; ++i)
    {
        cout << "  [" << i << "]";
        Fun fun1 = (Fun) * ((int*)*(int*)(&a) + i);
        fun1();
        cout << "\t地址:\t" << *((int*)*(int*)(&a) + i) << endl;
    }

    //[1]
    cout << "[1]vbptr ";
    cout << "\t地址:" << (int*)(&a) + 1 << endl;  //虚表指针的地址
    //输出虚基类指针条目所指的内容
    for (int i = 0; i < 2; i++)
    {
        cout << "  [" << i << "]";

        cout << *(int*)((int*)*((int*)(&a) + 1) + i);

        cout << endl;
    }


    //[2]
    cout << "[2]B1::ib1=" << *(int*)((int*)(&a) + 2);
    cout << "\t地址:" << (int*)(&a) + 2;
    cout << endl;

    //[3]
    cout << "[3]值=" << *(int*)((int*)(&a) + 3);
    cout << "\t\t地址:" << (int*)(&a) + 3;
    cout << endl;

    //[4]
    cout << "[4]B::vptr";
    cout << "\t地址:" << (int*)(&a) + 4 << endl;

    //输出B::vptr中的虚函数
    for (int i = 0; i < 2; ++i)
    {
        cout << "  [" << i << "]";
        Fun fun1 = (Fun) * ((int*)*((int*)(&a) + 4) + i);
        fun1();
        cout << "\t地址:\t" << *((int*)*((int*)(&a) + 4) + i) << endl;
    }

    //[5]
    cout << "[5]B::ib=" << *(int*)((int*)(&a) + 5);
    cout << "\t地址: " << (int*)(&a) + 5;
    cout << endl;
}

结果

在这里插入图片描述

这个结果与我们的C++对象模型图完全符合。这时我们可以来分析一下虚表指针的第二个条目值12的具体来源了,回忆上文讲到的:

第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值。

在我们的例子中,也就是B类实例内存地址相对于vbptr的偏移值,也即是:[4]-[1]的偏移值,结果即为12,从地址上也可以计算出来:00F8FDE4-00F8FDD8结果的十进制数是12。现在,我们对虚基类表的构成应该有了一个更好的理解。

菱形继承

class B

{

public:

    int ib;

public:

    B(int i = 1) :ib(i) {}

    virtual void f() { cout << "B::f()" << endl; }

    virtual void Bf() { cout << "B::Bf()" << endl; }

};

class B1 : virtual public B

{

public:

    int ib1;

public:

    B1(int i = 100) :ib1(i) {}

    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;

public:

    B2(int i = 1000) :ib2(i) {}

    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;



public:

    D(int i = 10000) :id(i) {}

    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; }

};

菱形虚拟继承下,最派生类D类的对象模型又有不同的构成了。在D类对象的内存构成上,有以下几点:

  • 在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)。
  • D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔。
  • 编译器没有为D类生成一个它自己的vptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同。
  • 共同虚基类B的内容放到了派生类对象D内存布局的最后。

在这里插入图片描述

代码演示

typedef void(*Fun)(void);
int main()
{
    D d;
    cout << "D对象内存大小为:" << sizeof(d) << endl;

    //取得B1的虚函数表
    cout << "[0]B1::vptr";
    cout << "\t地址:" << (int*)(&d) << endl;

    //输出虚表B1::vptr中的函数
    for (int i = 0; i < 3; ++i)
    {
        cout << "  [" << i << "]";
        Fun fun1 = (Fun) * ((int*)*(int*)(&d) + i);
        fun1();
        cout << "\t地址:\t" << *((int*)*(int*)(&d) + i) << endl;
    }

    //[1]
    cout << "[1]B1::vbptr ";
    cout << "\t地址:" << (int*)(&d) + 1 << endl;  //虚表指针的地址
    //输出虚基类指针条目所指的内容
    for (int i = 0; i < 2; i++)
    {
        cout << "  [" << i << "]";

        cout << *(int*)((int*)*((int*)(&d) + 1) + i);

        cout << endl;
    }


    //[2]
    cout << "[2]B1::ib1=" << *(int*)((int*)(&d) + 2);
    cout << "\t地址:" << (int*)(&d) + 2;
    cout << endl;

    //[3]
    cout << "[3]B2::vptr";
    cout << "\t地址:" << (int*)(&d) + 3 << endl;

    //输出B2::vptr中的虚函数
    for (int i = 0; i < 2; ++i)
    {
        cout << "  [" << i << "]";
        Fun fun1 = (Fun) * ((int*)*((int*)(&d) + 3) + i);
        fun1();
        cout << "\t地址:\t" << *((int*)*((int*)(&d) + 3) + i) << endl;
    }

    //[4]
    cout << "[4]B2::vbptr ";
    cout << "\t地址:" << (int*)(&d) + 4 << endl;  //虚表指针的地址
    //输出虚基类指针条目所指的内容
    for (int i = 0; i < 2; i++)
    {
        cout << "  [" << i << "]";

        cout << *(int*)((int*)*((int*)(&d) + 4) + i);

        cout << endl;
    }

    //[5]
    cout << "[5]B2::ib2=" << *(int*)((int*)(&d) + 5);
    cout << "\t地址: " << (int*)(&d) + 5;
    cout << endl;

    //[6]
    cout << "[6]D::id=" << *(int*)((int*)(&d) + 6);
    cout << "\t地址: " << (int*)(&d) + 6;
    cout << endl;

    //[7]
    cout << "[7]值=" << *(int*)((int*)(&d) + 7);
    cout << "\t\t地址:" << (int*)(&d) + 7;
    cout << endl;

    //间接父类
    //[8]
    cout << "[8]B::vptr";
    cout << "\t地址:" << (int*)(&d) + 8 << endl;

    //输出B::vptr中的虚函数
    for (int i = 0; i < 2; ++i)
    {
        cout << "  [" << i << "]";
        Fun fun1 = (Fun) * ((int*)*((int*)(&d) + 8) + i);
        fun1();
        cout << "\t地址:\t" << *((int*)*((int*)(&d) + 8) + i) << endl;
    }

    //[9]
    cout << "[9]B::id=" << *(int*)((int*)(&d) + 9);
    cout << "\t地址: " << (int*)(&d) + 9;
    cout << endl;

    getchar();
}

在这里插入图片描述

在这里插入图片描述

数据成员如何访问(直接取址)

跟实际对象模型相关联,根据对象起始地址+偏移量取得。

函数成员如何访问(间接取址)

跟实际对象模型相关联,普通函数(nonstatic、static)根据编译、链接的结果直接获取函数地址;如果是虚函数根据对象模型,取出对于虚函数地址,然后在虚函数表中查找函数地址。

多态如何实现?

多态(Polymorphisn)在C++中是通过虚函数实现的。如果类中有虚函数,编译器就会自动生成一个虚函数表,对象中包含一个指向虚函数表的指针。能够实现多态的关键在于:虚函数是允许被派生类重写的,在虚函数表中,派生类函数对覆盖(override)基类函数。除此之外,还必须通过指针或引用调用方法才行,将派生类对象赋给基类对象。

为什么析构函数设为虚函数是必要的

析构函数应当都是虚函数,除非明确该类不做基类(不被其他类继承)。基类的析构函数声明为虚函数,这样做是为了确保释放派生对象时,按照正确的顺序调用析构函数。

如果析构函数不定义为虚函数,那么派生类就不会重写基类的析构函数,在有多态行为的时候,派生类的析构函数不会被调用到(有内存泄漏的风险!)。例如,通过new一个派生类对象,赋给基类指针,然后delete基类指针,缺少了派生类的析构函数调用。把析构函数声明为虚函数,调用就正常了。

  • 16
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值