• VS下对象内存模型


    以下基于VS2008测试
    看内存布局为,VS工具的命令提示符,cd到当前工程当前文件目录
    cl [filename] /d1reportSingleClassLayoutX便可查看filename中的类X的内存布局。

    一.单继承##

    1.空类单继承###

    #include <cstdio> 
    #include <string> 
    
    using namespace std; 
    
    class A 
    { 
    
    }; 
    
    class B: public A
    {
    
    };
    
    class C: public A
    {
    	virtual ~C();
    };
    
    class D : virtual public A
    {
    	virtual ~D(){}
    };
    
    int main(void) 
    { 
    	getchar(); 
    	return 0; 
    }
    




    可见空类被继承后,只要子类非空(存在虚函数或者虚基类),那么占位的一个字节就被优化掉。

    2.非空类单继承,基类无虚函数###

    #include <cstdio> 
    #include <string> 
    
    using namespace std; 
    
    class A 
    { 
    public:
    	int a;
    }; 
    
    class B: public A
    {
    public:
    	int b;
    };
    
    class C: public A
    {
    public:
    	virtual ~C();
    	int c;
    };
    
    class D : virtual public A
    {
    public:
    	virtual ~D(){}
    	int d;
    };
    
    int main(void) 
    { 
    	getchar(); 
    	return 0; 
    }
    




    3.非空类单继承,基类有虚函数###

    #include <cstdio> 
    #include <string> 
    
    using namespace std; 
    
    class A 
    { 
    public:
    	int a;
    	virtual ~A(){}
    }; 
    
    class B: public A
    {
    public:
    	int b;
    };
    
    class C: public A
    {
    public:
    	virtual ~C();
    	int c;
    };
    
    class D : virtual public A
    {
    public:
    	virtual ~D(){}
    	int d;
    };
    
    int main(void) 
    { 
    	getchar(); 
    	return 0; 
    }
    




    B和C的结构可以看出,不管C是否显示声明虚析构,都会有一个虚析构并覆盖掉基类的虚析构。


    D这里有虚基类,且没有普通继承(有普通继承后会导致优先存放普通继承的,多个普通继承优先存放有虚函数的,后续实例说明),故而先放虚基类表指针,和子类成员,然后放虚基类对象成员,该基类有虚表和成员a。
    虚基类表第1项D::$vftable指示this指针和虚基类A子对象偏移8字节,即在后续调整中,指示A* pA = new D()时,使用pA的时候地址需要调整的字节数。
    虚函数表存在一个覆盖基类的虚析&D::(dtor)
    D::{dtor} this adjustor表示this调用虚函数需要调整8个字节去先找到虚函数表
    另外,因为VS编译器的typeinfo是放在虚函数表前的,不占用虚表位置,故而虚函数表索引0就是第一个虚函数。

    这里因为不存在子类覆盖基类非构造的虚函数,导致的可能在基类构造析构函数中调用基类的虚函数情况,故而fVtorDisp被编译器设置为0。
    关于vtordisp参考vtordisp pragma

    以上是简单的继承过程中的VS编译器下的对象结构

    二..多继承##

    1.无虚函数,无虚继承,钻石结构###

    #include <cstdio>
    #include <string>
    
    using namespace std;
    
    struct X
    {
    	int a;
    };
    
    struct Y : public X
    {
    	int b;
    };
    
    struct Z : public X
    {
    	int z;
    };
    
    struct A : public Y, public Z 
    {
    	int a;
    };
    
    int main(void)
    {
    	X x;
    	Y y;
    	Z z;
    	A a;
    	printf("sizeof(int) = %d, sizeof(X) = %d, sizeof(Y) = %d, sizeof(Z) = %d, sizeof(A) = %d
    ", 
    		sizeof(int), sizeof(X), sizeof(Y), sizeof(Z), sizeof(A));
    	getchar();
    	return 0;
    }
    

    结构和内存布局比较清晰,因为没有虚继承,A中存在两份X。

    2.无虚函数,有虚继承,钻石结构###

    #include <cstdio>
    #include <string>
    
    using namespace std;
    
    struct X
    {
    	int x;
    };
    
    struct Y : virtual public X
    {
    	int y;
    };
    
    struct Z : virtual public X
    {
    	int z;
    };
    
    struct A : public Y, public Z 
    {
    	int a;
    };
    
    int main(void)
    {
    	X x;
    	Y y;
    	Z z;
    	A a;
    	printf("sizeof(int) = %d, sizeof(X) = %d, sizeof(Y) = %d, sizeof(Z) = %d, sizeof(A) = %d
    ", 
    		sizeof(int), sizeof(X), sizeof(Y), sizeof(Z), sizeof(A));
    	getchar();
    	return 0;
    }
    


    a.A没有普通继承的基类,故而直接顺序存放虚继承的基类Y和Z,而Y和Z虚继承X,故而Y,Z均有一个虚基类表;
    b.Y和Z放完后,再放A自己的成员,这里因为A自身没有虚函数,且第一个基类已有虚函数表,故而A接下来不需要存放虚函数指针,又没有虚继承,也不用存放虚基类表指针,所以直接存放成员a;
    c.最后开始存放虚基类,这里虚基类只有一个X,且X没有虚函数,直接存放X的成员x。

    用pA表示A对象的this地址,用pX和pY和pZ分别表示X,Y,Z子对象的开始地址,则
    A::$vbtable@Y 即A的Y子类虚基类表,由于Y只有一个虚基类X,且子对象中该虚基类子对象地址pX = pY + 20;
    A::$vbtable@Z 即A的z子类虚基类表,由于Z只有一个虚基类X,且子对象中该虚基类子对象地址pX = pZ + 12;
    没有虚函数覆盖,不考虑fVtorDisp
    vbi指示了pA到基类子对象pX的偏移20, pX = PA + 20;

    3.不带虚函数,多虚基类,有钻石子结构###

    基类中的普通继承属于is a关系,直接不管,但如果基类中又有虚基类,则会对子类的虚基类表产生影响。

    #include <cstdio>
    #include <string>
    
    using namespace std;
    
    struct X
    {
    	int x;
    };
    
    struct M
    {
    	int m;
    };
    
    struct N
    {
    	int n;
    };
    
    struct Y : virtual public X, virtual public M
    {
    	int y;
    };
    
    struct Z : virtual public X
    {
    	int z;
    };
    
    struct W : public Z
    {
    	int w;
    };
    
    struct A : virtual public N, public W ,public Y
    {
    	int a;
    };
    
    int main(void)
    {
    	X x;
    	Y y;
    	Z z;
    	A a;
    	printf("sizeof(int) = %d, sizeof(X) = %d, sizeof(Y) = %d, sizeof(Z) = %d, sizeof(A) = %d
    ", 
    		sizeof(int), sizeof(X), sizeof(Y), sizeof(Z), sizeof(A));
    	getchar();
    	return 0;
    }
    


    a.A先看普通继承的W,W直接继承自Z,Z又虚继承X,则Z包含虚基类表,先存放Z的虚基类表指针,虚基类X先不管,再存放Z成员z,往下在存放W的成员w
    b.再看A普通继承的Y,Y虚继承X和M,故而Y存在虚基类表,X,和M虚基类先不管,再存放Y自己的成员y
    d.普通继承分析完,再存放A自己的成员
    e.接下来按照继承顺序N,W(Z(X)),Y(X, M),找虚基类,即N,X, M,这因为虚继承,X只会存在一份
    f.顺序排列N,X,M子对象的成员,n,x,m
    至此整个对象结构就排好了,接下来看虚基类表
    a.存在两个虚基类表,一个是A::W::X的,一个是A::Y的。但A::W::X是被A复用了。其实际是A的虚基类表
    b.先看A::$vbtable@Y,Y虚继承自X,M故而有其虚基类表中有两个偏移量,分别指示pX = pY + 16; pX = pM +20;
    c.再看A::$vbtable@W,实际应该是被A复用的A::W::Z的虚表,之前分析的A有三个虚基类,即pX = pA + 28;pN = pA + 24;pM = pA + 32;
    d.至于图示为何"1 | 28 (Ad(Z+0)X)"暂时未理解清楚。后面的vbi也指示了各虚基类相对于A的this指针的偏移

    三.有虚函数多路径继承##

    1.虚函数的菱形继承, 没有虚继承###

    #include <cstdio> 
    #include <string> 
    
    using namespace std; 
    
    class A 
    { 
    	virtual int fun(){} 
    public: 
    	int dataA; 
    }; 
    
    class B : public A 
    { 
    	virtual int fun(){} 
    	virtual int foo(){} 
    public: 
    	int dataB; 
    }; 
    
    class C : public A 
    { 
    	virtual int foo(){} 
    	virtual int bar(){} 
    public: 
    	int dataC; 
    }; 
    
    class D : public B, public C 
    { 
    	virtual int fun(){} 
    	virtual int fff(){} 
    public: 
    	int dataD; 
    }; 
    
    int main(void) 
    { 
    	printf("sizeof(int) = %d, sizeof(A) = %d, sizeof(B) = %d, sizeof(C) = %d, sizeof(D) = %d
    ", 
    		sizeof(int), sizeof(A), sizeof(B), sizeof(C), sizeof(D)); 
    	getchar(); 
    	return 0; 
    }
    


    没有虚继承,且B和C都有虚函数,直接顺序排列B,C
    a.D::$vftable@B@ B从A中继承的虚函数表被D继续继承,被D复用。
    b.A::fun被B::fun覆盖并附加B::foo
    c.B::fun又被D的fun覆盖
    d.附加D::fff
    e.D::$vftable@C@ C本身也继承自A有自己的虚函数表,先放A::fun
    f.然后因为C没有实现fun覆盖,直接附加C::foo和C::bar
    g.但是由于C同时被D继承,而D有fun实现故而覆盖A::fun成D::fun
    h.所以在D::$vftable@C@虚函数表中,有一个&thunk: this-=12; goto D::fun,用于指示当前如果是pC子对象调用fun需要调整this指针 -= 12用于指向pD,实现对D的fun调用

    2.虚函数的菱形继承, 有虚继承###

    #include <cstdio> 
    #include <string> 
    
    using namespace std; 
    
    class A 
    { 
    	virtual void fun(){} 
    public: 
    	int dataA; 
    }; 
    
    class B : virtual public A 
    { 
    	virtual void fun(){} 
    	virtual void foo(){} 
    public: 
    	int dataB; 
    }; 
    
    class C : virtual public A 
    { 
    	virtual void foo(){} 
    	virtual void bar(){} 
    public: 
    	int dataB; 
    }; 
    
    class D : public B, public C 
    { 
    	virtual void fun(){} 
    	virtual void fff(){} 
    public: 
    	int dataD; 
    }; 
    
    int main(void) 
    { 
    	A a; 
    	B b; 
    	C c; 
    	D d; 
    	printf("sizeof(int) = %d, sizeof(A) = %d, sizeof(B) = %d, sizeof(C) = %d, sizeof(D) = %d
    ", 
    		sizeof(int), sizeof(A), sizeof(B), sizeof(C), sizeof(D)); 
    	getchar(); 
    	return 0; 
    }
    


    a.经过前面的不同结构了解,这里D的内存结构基本比较清晰了。
    b.D普通继承B和C,且B有虚函数,直接放B子类,B有虚函数,且有虚基类,先后存放虚函数表和虚基类表,再存放B的成员
    c.C子类也同样,然后存放D自身的成员变量
    d.然后存放虚基类A
    e.这里有两个虚基类表,三个虚函数表,
    f.D::$vbtable@B@ 原本是B的,被D共享,当然这里因为D的继承路径上只有一个虚基类,故而表中也只有一项即A,且偏移是按pB的虚基类表指针地址进行计算的即,pA = &pbptr(B) + 24,注意这里虚基类表前面还有个虚函数表。算偏移时要看清楚。至于第0项的-4猜想是到当前虚基类表指针到pB的this距离,这里隔了一个vfptr的大小
    g.D::$vbtable@C@同D::$vbtable@B@理解,第0项依然是-4,第二项12表示pA = &pbptr(C) + 12
    h.对于虚函数表先看基类的D::$vftable@A@ 因为A::fun最终被D::fun覆盖,故而在其他虚继承自A的子类中不再管fun函数
    i.D::$vftable@B@因为虚继承自A,不管fun直接覆盖B自己的foo,但因为该D::$vftable@B被D复用,故而需要再附加上D的非fun的额外的虚函数表
    j.D::$vftable@C@同样不管继承而来的fun直接附加C自己的C::foo和C::fff
    k.至此虚基类表和虚函数表已经处理完毕,D::fun this adjustor: 28表示pD要调用fun需要调整指针到pD+28到A的虚函数表中去调,而D::fff this adjustor: 0表示pD要调用fff不需要偏移,因为直接pD的第0位置就是存放的pD的vfptr
    l.vbi中的offset = pA - pD即D对象和子对象A的地址偏移。
    m.综上内存布局解析完毕。

    四.虚继承和存在虚函数的基类对子类内存布局的影响##

    1.虚基类对布局顺序的影响###

    #include <cstdio>
    #include <string>
    
    using namespace std;
    
    struct X
    {
    	int x;
    };
    
    struct M
    {
    	int m;
    };
    
    struct Y : public M
    {
    	int y;
    };
    
    struct Z : virtual public X
    {
    	int z;
    };
    
    struct A : public Y, public Z
    {
    	int a;
    };
    
    int main(void)
    {
    	X x;
    	Y y;
    	Z z;
    	A a;
    	printf("sizeof(int) = %d, sizeof(X) = %d, sizeof(Y) = %d, sizeof(Z) = %d, sizeof(A) = %d
    ", 
    		sizeof(int), sizeof(X), sizeof(Y), sizeof(Z), sizeof(A));
    	getchar();
    	return 0;
    }
    


    可见A普通继承自Y,Z并没有应为Z中有虚继承就将Z的内存布局提前。但是如果Z中有虚函数,就会把Z提前。

    2.虚函数对布局顺序的影响###

    #include <cstdio>
    #include <string>
    
    using namespace std;
    
    struct X
    {
    	int x;
    };
    
    struct M
    {
    	int m;
    };
    
    
    struct Y : public M
    {
    	int y;
    };
    
    struct Z : virtual public X
    {
    	int z;
    	virtual ~Z(){}
    };
    
    struct A : public Y, public Z
    {
    	int a;
    };
    
    int main(void)
    {
    	X x;
    	Y y;
    	Z z;
    	A a;
    	printf("sizeof(int) = %d, sizeof(X) = %d, sizeof(Y) = %d, sizeof(Z) = %d, sizeof(A) = %d
    ", 
    		sizeof(int), sizeof(X), sizeof(Y), sizeof(Z), sizeof(A));
    	getchar();
    	return 0;
    }
    

    可以看见,Z中有虚函数同时有虚继承时,先放的是虚函数指针,然后才是虚基类指针。
    同时因为Z中存在虚函数,故而在子类A中并没有按照继承顺序布局,而是将Z进行了优先布局。

    五.虚继承过程中额外会影响对象大小的两点##

    虚基类表特定情况下会增加一个vtordisp。
    虚基类有虚函数,子类虚继承后没有新增虚函数,则子类的虚函数表复用该基类的,不再新增子类的虚函数表指针;如果子类有新增虚函数,则子类需要额外加一个虚函数表指针,
    如下示例:

    #include <cstdio> 
    #include <string> 
    
    using namespace std; 
    
    class A 
    { 
    public: 
    	virtual ~A(){};
    	virtual void fun(){} 
    	int dataA; 
    }; 
    class B : virtual public A
    { 
    public: 
    	virtual void fun(){} 
    	virtual ~B(){}
    	int dataB; 
    }; 
    
    class C : virtual public A 
    { 
    public: 
    	virtual void fun(){} 
    	int dataC;
    }; 
    
    class D : virtual public A 
    { 
    public: 
    	virtual ~D(){}
    	virtual void ddd(){} 
    	int dataD; 
    }; 
    
    class X : virtual public A
    { 
    public: 
    	virtual void fun(){} 
    	virtual void xxx(){} 
    	virtual ~X(){}
    	int dataX; 
    }; 
    
    class Y : virtual public A
    { 
    public: 
    	virtual void fun(){} 
    	Y(){}
    	int dataY; 
    }; 
    
    int main(void) 
    { 
    	printf("sizeof(int) = %d, sizeof(A) = %d, sizeof(B) = %d, sizeof(C) = %d, sizeof(D) = %d
    ", 
    		sizeof(int), sizeof(A), sizeof(B), sizeof(C), sizeof(D)); 
    	getchar(); 
    	return 0; 
    }
    
    

    比较下6个类的布局





    1>class A很简单就一个虚析构和虚函数fun
    2>重点看下虚继承自A的B,C,D的差异。
    3>B::fun覆盖了A::fun,且B有自定义的虚析构。B因为自定义了虚析构,且覆盖了基类的fun,可能存在子类在构造(见类Y布局)或析构函数通过基类指针调用覆盖函数的情况,故而需要额外增加vtordisp。且B没有新增虚函数,直接复用了基类A的虚函数表指针。
    4>C::fun覆盖了A::fun,但C没有自定义的虚析构和构造。没有自定义构造和析构,系统默认的构造或者析构是明确不会存在通过基类指针调用覆盖函数的情况,故而不需要额外增加vtordisp
    5>D没有fun函数覆盖,且D有自定义了虚析构;此外D外增加了一个虚函数ddd。这里因为新增了虚函数而D又没有普通继承的基类可以复用的虚函数表,故而需要新增一个虚函数表指针字段。
    6>再来看X,X是集大成者,即满足B又满足D,故而除了需要vtordisp还需要其自己的虚函数表。即X最大。
    7>Y是为了验证自定义构造和B自定义析构对比,和B大小一样。

    以上是VS编译器下的分析结果,其他编译器对上述两点处理不同,得到的对象大小也不同,以下windows
    C:Program Files (x86)C-Free 5mingwin>g++ -v
    Reading specs from ./../lib/gcc/mingw32/3.4.5/specs编译器下的结果

    六.瞎继承##

    #include <cstdio> 
    #include <string> 
    
    using namespace std; 
    
    class X 
    { 
    	virtual int xxx(){} 
    	virtual int ttt(){} 
    public: 
    	int dataX; 
    }; 
    class Y 
    { 
    	virtual int yyy(){} 
    public: 
    	int dataY; 
    }; 
    class A 
    { 
    	virtual int fun(){} 
    public: 
    	int dataA; 
    }; 
    class B : virtual public A ,public Y 
    { 
    	virtual int fun(){} 
    	virtual int foo(){} 
    public: 
    	int dataB; 
    }; 
    class C : virtual public A 
    { 
    	virtual int foo(){} 
    	virtual int bar(){} 
    public: 
    	int dataC;
    }; 
    class D : virtual public X, public B, public C 
    { 
    	virtual int fun(){} 
    	virtual int fff(){} 
    public: 
    	int dataD; 
    }; 
    int main(void) 
    { 
    	printf("sizeof(int) = %d, sizeof(A) = %d, sizeof(B) = %d, sizeof(C) = %d, sizeof(D) = %d
    ", 
    		sizeof(int), sizeof(A), sizeof(B), sizeof(C), sizeof(D)); 
    	getchar(); 
    	return 0; 
    }
    




    基于前述的各种继承机构及可能由编译做出的优化结构,可推理出此D类的内存结构,不再复述

    另外该例叠加vtordisp以及可能存在的虚函数表的复用,大小也会发生相应的变化。

  • 相关阅读:
    java基础循环、条件语句、switch case
    java基础抽象类、接口、枚举、包
    java基础基本数据类型、变量类型、修饰符、运算符
    Mac权限问题,operation not permitted
    【比赛游记】NOIP2021 游记
    【比赛题解】NOIP2021 题解
    把LeetCode上的Sql题刷完了会有什么收获
    分析函数之Lead()、Lag()
    QT相关(c++)
    grpc
  • 原文地址:https://www.cnblogs.com/kuikuitage/p/12321691.html
Copyright © 2020-2023  润新知