瞅瞅C++中的对象模型(下)

本文深入探讨了C++中虚继承的概念,通过实例讲解了重复继承、单一虚继承及虚继承如何解决菱形继承问题。文章详细分析了不同情况下对象的内存布局,揭示了虚继承在解决数据成员二义性问题上的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

上一篇讲了C++对象模型中的多重继承部分,这一篇该总结总结虚继承的部分了。

1.9 重复继承

有了上面的基础,很自然的就会引入重复继承的问题。其基本继承关系如下:
在这里插入图片描述
Derive会从Base1中和Base2继承两份b_data。 如果直接引入这个b_data数据成员的话会造成数据的二义性(编译器不知道你是想访问Base1::b_data还是Base2::b_data,),除非进行显式定义。如d.Base1::b_data 、或者d.Base2::bata 。
 

同样的,实验代码验证一下:

//Base类
class Base{
public:
    Base(int val = 5):b_data(val){cout<<"constructor in base\n";}
    virtual void f() {cout<<"Base::f\n";}
    int b_data;                //普通成员函数
};
//Base1类
class Base1 :public Base{
public:
    Base1(int val = 10):b1_data(val){ cout<<"constructor in base1\n";}      //构造函数

    //以下三个是测试的虚函数
    virtual void f() { cout << "virtual Base1::f" << endl; }
    virtual void g() { cout << "virtual Base1::g" << endl; }
    virtual void h1() { cout << "virtual Base1::h1" << endl; }
    void show(){cout<<"Base1:: show"<<endl;}     //普通的成员函数

    int b1_data;     //数据成员
};
//Base2类
class Base2 :public Base{
public:
    Base2(int val = 20):b2_data(val){ cout<<"constructor in base2\n";}      //构造函数

    //以下三个是测试的虚函数
    virtual void f() { cout << "virtual Base2::f" << endl; }
    virtual void g() { cout << "virtual Base2::g" << endl; }
    virtual void h2() { cout << "virtual Base2::h2" << endl; }
    
    void show(){cout<<"Base2:: show"<<endl;}     //普通的成员函数
    int b2_data;     //数据成员

};
//main 函数
typedef void(*Fun)();

int main()
{
    Base1 b(100),b1(1000);
    Derive d(200);

    cout<<"the size of d(Derive):"<<sizeof(d)<<endl;

    cout<<"\nduplicate member test\n";

   // cout<<"show d.b_data:"<<d.b_data<<endl;             //会引起数据二义性

    cout<<"show d.Base1::b_data:"<<d.Base1::b_data<<endl;

    int** pVtab = (int**)&d;
    Fun pFun = NULL;

    cout<<"\nvirtual function test.....\n";

    cout << "[0] Base1:: vptr->" << endl;

    pFun = (Fun)pVtab[0][0];
    cout << "     [0] ";
    pFun();

    pFun = (Fun)pVtab[0][1];
    cout << "     [1] ";
    pFun();

     pFun = (Fun)pVtab[0][2];
    cout << "     [2] ";
    pFun();

    pFun = (Fun)pVtab[0][3];
    cout << "     [3] ";
    pFun();

    pFun = (Fun)pVtab[0][4];
    cout << "     [4] ";
    cout<<pFun<<endl;

    cout << "[1] Base1.b_data = " << (int)pVtab[1] << endl;     //继承自Base
    cout << "[2] Base1.b1_data = " << (int)pVtab[2] << endl;

    int s = sizeof(Base1)/4;

    cout << "\n[" << s << "] Base2::_vptr->"<<endl;
    pFun = (Fun)pVtab[s][0];
    cout << "     [0] ";
    pFun();

    pFun = (Fun)pVtab[s][1];
    cout << "     [1] ";
    pFun();

    pFun = (Fun)pVtab[s][2];
    cout << "     [2] ";
    pFun();

    pFun = (Fun)pVtab[s][3];
    cout << "     [3] ";
    cout<<pFun<<endl;

    cout << "["<< s+1 <<"] Base2.b_data = " << (int)pVtab[s+1] << endl;
    cout << "["<< s+2 <<"] Base2.b2_data = " << (int)pVtab[s+2] << endl;
     cout << "["<< s+3 <<"] Derive::d_data = " << (int)pVtab[s+3] << endl;
    return 0;
}

实验结果如下所示:
在这里插入图片描述
如果有了前面的基础,这个应该很好理解。 在此就不多说了。

同样的,最后上个图帮助理解。
 
在这里插入图片描述
 
以上总结的重复继承也叫做菱形继承问题,为了解决重复继承的子类数据成员问题,C++中又有了一个叫做虚继承的概念。 这里先简单介绍单一的虚继承,再总结虚继承解决上面的菱形继承问题。

 

1.10 单一虚继承

所谓虚继承在形式上,就是在普通的继承语句中加一个virtual 关键字。但是就是这个关键字,改变了子类的内存布局。

做个实验瞧瞧!

实验代码中新的继承关系如下图所示:

在这里插入图片描述
实验代码如下:

//Base类
class Base{
public:
    Base(int val = 5):b_data(val){cout<<"constructor in base\n";}
    virtual void f() {cout<<"Base::f\n";}
    virtual void m(){cout<<"Base::m\n";}
    int b_data;                //普通成员函数

};
//Base1类
class Base1 : virtual public Base{
public:
    Base1(int val = 10):b1_data(val){ cout<<"constructor in base1\n";}      //构造函数

    //以下三个是测试的虚函数
    virtual void f() override{ cout << "virtual Base1::f" << endl; }
    virtual void g() { cout << "virtual Base1::g" << endl; }

    virtual void h1() { cout << "virtual Base1::h1" << endl; }

    void show(){cout<<"Base1:: show"<<endl;}     //普通的成员函数

    int b1_data;     //数据成员
};
//main函数
#include<iostream>
#include<cstdio>
using namespace std;

//#include "Derive.h"
#include "base1.h"

//main 函数
typedef void(*Fun)();
int main()
{
    //cout<<"sizeof(int*) :"<<sizeof(int**)<<endl;
    Base1 b(100),b1(1000);

    cout<<"the size of b(Base1):"<<sizeof(b)<<endl;

    int** pVtab = (int**)&b;
    Fun pFun = NULL;

    cout<<"\nvirtual function test.....\n";

    cout << "\n[0] Base1:: vptr->" << endl;

    pFun = (Fun)pVtab[0][0];
    cout << "     [0] ";
    pFun();

    pFun = (Fun)pVtab[0][1];
    cout << "     [1] ";
    pFun();

    pFun = (Fun)pVtab[0][2];
    cout << "     [2] ";
    pFun();

    pFun = (Fun)pVtab[0][3];
    cout << "     [3] ";
    cout<<pFun<<endl;
    //pFun();

    cout << "[1] Base1.b1_data = " << *((int*)(&b) + 1) << endl;
//    cout << "[2]  = " << *((int*)(&b) + 2) << endl;
//    cout << "[3]  = " << *((int*)(&b) + 3) << endl;

    pVtab = (int**)((int*)(&b) + 2);

    cout << "\n[" << 2<< "] Base::_vptr->"<<endl;
    pFun = (Fun)pVtab[0][0];
    cout << "     [0] "<<"Base1::virtual thunk to Base1::f()";
  //  pFun();           //此函数不能直接这样执行,否则会报错。 pFun此处指向的是一段利用thunk机制的代码

    pFun = (Fun)pVtab[0][1];
    cout << "     [1] ";
    pFun();

    pFun = (Fun)pVtab[0][2];
    cout << "     [2] ";
    cout<<pFun<<endl;

    return 0;
}

 
实验结果如下图所示。
在这里插入图片描述
 
同样的,我们画个逻辑图,呈现一下子。
在这里插入图片描述

 
关于上图,要做个说明:
(1)、图中蓝色圈圈表明了Base1原生的部分放在布局的前面,继承的Base虚表指针和数据成员放在后面.( 这个和非虚继承都不一样),按照【1】中的说法,对于虚继承来说,其内存被一般被分为两个部分,一个不变区域(如蓝色圈中的Base1部分)和一个共享区域(如蓝色圈中的Base)。 不变区域中的数据不管后继如何演化,总是拥有固定的offset,所以这一部分的数据可以直接存取。 而共享区域的数据,其位置会随着每次的派生类操作而有变化,所以他们只能被间接存取。
 
(2)、红色圈中的逻辑上应该是Base类中被重写的f(), 但是在虚继承具体实现的过程中为了保证多态需要进行this指针的动态调整,所以引出了chunk机制。(这里面的内容又够写一篇博客了,同时对于这一部分还是稍微有点乱,下次有机会在好好总结,在发出来吧)

(3)、绿色圈圈指示虚表中虚函数的末尾都是0,这一点和前面的非虚继承也不一样,至于为啥要这样,我暂且也说不出来 。 ^ _ ^ |||

 

1.11 虚继承解决菱形继承问题

好了,还剩最后一种情况,这是上面情况的升级版,也是重复继承的改进版。

其基本的继承图如1.9 章节所示 。
再贴一下吧。
在这里插入图片描述

验证的代码也和1.9 节的差不多。只不过把Base1 和Base2 继承自Base的类改成虚继承。
同时main函数要稍微修改一下 。 下面只贴上main函数调用代码。

#include<iostream>
#include<cstdio>
using namespace std;

#include "Derive.h"
typedef void(*Fun)();

int main()
{
    Base1 b(100),b1(1000);
    Derive d(200);

    cout<<"the size of d(Derive):"<<sizeof(d)<<endl;

    cout<<"\nduplicate member test\n";

    cout<<"show d.b_data:"<<d.b_data<<endl;             //采用虚继承后不会引起数据二义性

    //cout<<"show d.Base1::b_data:"<<d.Base1::b_data<<endl;

    int** pVtab = (int**)&d;
    Fun pFun = NULL;

    cout<<"\nvirtual function test.....\n";
    cout << "[0] Base1:: vptr->" << endl;

    pFun = (Fun)pVtab[0][0];
    cout << "     [0] ";
    pFun();

    pFun = (Fun)pVtab[0][1];
    cout << "     [1] ";
    pFun();

     pFun = (Fun)pVtab[0][2];
    cout << "     [2] ";
    pFun();

    pFun = (Fun)pVtab[0][3];
    cout << "     [3] ";
    pFun();

    pFun = (Fun)pVtab[0][4];
    cout << "     [4] ";
    cout<<pFun<<endl;

    cout << "[1] Base1.b1_data = " << (int)pVtab[1] << endl;     //继承自Base

    cout << "\n[" << 2 << "] Base2::_vptr->"<<endl;
    pFun = (Fun)pVtab[2][0];
    cout << "     [0] ";
    pFun();

    pFun = (Fun)pVtab[2][1];
    cout << "     [1] ";
    pFun();

    pFun = (Fun)pVtab[2][2];
    cout << "     [2] ";
    pFun();

    pFun = (Fun)pVtab[2][3];
    cout << "     [3] ";
    cout<<pFun<<endl;

    cout << "["<< 3 <<"] Base2.b2_data = " << (int)pVtab[3] << endl;
    cout << "["<< 4 <<"] Derive.d_data = " << (int)pVtab[4] << endl;

    cout << "\n[" << 5 << "] Base2::_vptr->"<<endl;
    pFun = (Fun)pVtab[5][0];
    cout << "     [0] "<<"Derive::virtual thunk to Base::f()\n";
    //pFun();

    pFun = (Fun)pVtab[5][1];
    cout << "     [1] ";
   cout<<pFun<<endl;

   cout << "["<< 6 <<"] Base.b_data = " << (int)pVtab[6] << endl;

    return 0;
}

贴一下最终的结果图。

在这里插入图片描述
 
清晰一点的图如下:
 
在这里插入图片描述
解释一波:
 
(1)、使用了虚继承的菱形继承,在内存布局中先排布直接子类的成员(注意这里不包括从间接子类中继承下来的成员),然后原生的成员。以上都是不变的部分,最后才是上面所说的共享的可变的部分,也就是从Base类中继承而来的。 如上图中蓝色大括号中所示.

 
(2)、Derive会从直接子类(Base1和Base2)中继承虚函数表,并按照普通继承那样修改重写的虚函数。如上图中红色圈出来的部分。同时新增的虚函数会增加在第一个继承的虚函数表中。如上图中绿色圈出来的部分。

 
(3)、菱形继承的“顶端尖尖”(Base类), Derive中只会留有一份,放在内存模型的最后,同时也会继承其虚函数表,如果重写了了虚函数表中的虚函数(如上图中的Base::f()),那么 这里就会使用chunk 机制来动态定位最终的虚函数。 (和1.10节中说的类似)

 

二、总结

  原来以为一篇博客就可以把这此要总结的内容都总结完的,每想到一总结起来就有点“没完没了”的感觉,才发现还有很多内容把握的不透彻,了解的不深入,边查边学、边学编想、边想边总结,零零散散花了一周多时间,才将将把内存模型的一部分总结完。 这还是要总结的一部分,还有总结过程中延伸的部分,如chunk机制的原理、虚继承多态的具体实现原理(上面顶多只是说了是什么,但是还没有研究为什么是这样?)等,发现自己搞的还不是很透彻,这个就留在后面有时间在研究总结吧。


 最后对整个实验的做下说明:我在整个实验的过程中采用的是gcc -4.9.2 版本的编译器,可能有些结果和其他的编译器有些不同,这里要稍微注意一下。

参考

【1】、《深度探索C++对象模型》
【2】、C++ Data Member内存布局
【3】、C++ 对象的内存布局(上)
【4】、C++ 虚函数表解析
【5】、C++ 对象的内存布局(下)
【6】、C++幕后故事(四)-- 虚函数的魅力
【7】、图解C++虚函数
【8】、(好)C++ 多继承和虚继承的内存布局
【9】、C++中虚函数、虚继承内存模型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值