首页 > 编程语言 > 详细

C++类对象内存布局(四)

时间:2018-02-19 22:51:54      阅读:280      评论:0      收藏:0      [点我收藏+]

测试系统:Windows XP

编译器:VS2008

(四)

虚继承的情况:

如果说没有虚函数的虚继承只是一个噩梦的话,那么这里就是真正的地狱。这个C++中最复杂的继承层次在VC上的实现其实我也没有完全理解,摸爬滚打了一番也算得出了微软的实现方法吧,至于一些刁钻的实现方式我也想不到什么理由来解释它,也只算是知其然不知其所以然吧。

也还是从最简单的开始,我们分2个阶段来探讨。一个是有虚函数的派生类虚继承了没有虚函数的基类的情况,一个情况是有虚函数的派生类虚继承了有虚函数的基类的情况。

从第一个开始慢慢深入。派生类有虚函数,而虚基类都没有虚函数的情况也还算比较简单。因为虚函数表指针一定是会放在最开始的,所以根据猜测也可以知道其大概布局情况,先是虚函数表指针,然后是实基类,然后是虚基类表指针,然后到各个成员函数,这些元素组成了实部,然后再到虚部的各个虚基类。看下面一个简单的例子。

#pragma pack(8)

class F1{public: int f1; F1():f1(0xf1f1f1f1){}};

 

class A: virtual F1{public:

    int a;

    A():a(0xaaaaaaaa){}

    virtual void funA2(){}

    virtual ~A(){}

    virtual void funA(){}

};

 

int main()

{

    A aa;

    return 1;

}

 

sizeof(A)    0x00000010 

0x0012FF54  40 57 41 00 50 57 41 00 aa aa aa aa f1 f1 f1 f1

虚函数表:

0x00415740  28 10 41 00 b8 11 41 00 aa 10 41 00  

虚基类表:

0x00415750  fc ff ff ff 08 00 00 00

 

这是只有一个虚继承的简单情况,可以看到类A的前4个字节就是虚函数表指针了,虚函数表里面有3个项,当然就是分别指向类A的那3个虚函数的指针了。虚函数表指针是最大的,都要放在第一位,然后再到虚基类表指针,可以看到虚基类表里面的两个偏移,一个是-4,说明类A的起始地址位于虚基类表指针偏移-4个字节的地方,也就是虚函数表指针的地址了。虚基类表里面的第二项就是第一个虚基类相对于虚基类表指针的偏移,是8字节,可以看到刚好也是f1f1f1f1的位置。

看了一个简单的情况我们再来看一个稍微复杂的继承情况,虚实多继承,所有的基类仍然都没有虚函数。

#pragma pack(8)

class F0{public: char f0; F0():f0(0xf0){}};

class F1{public: int f1; F1():f1(0xf1f1f1f1){}};

class F2{public: int f2; double f22; F2():f22(0),f2(0xf2f2f2f2){}};

class F3:virtual F2{public: int f3; F3(): f3(0xf3f3f3f3){}};

class F4:virtual F2{public: int f4; F4(): f4(0xf4f4f4f4){}};

 

class A: virtual F0, F1, virtual F3, F4{public:

    int a;

    A():a(0xaaaaaaaa){}

    virtual void funA2(){}

    virtual ~A(){}

    virtual void funA(){}

};

 

sizeof(A)    0x00000040 

0x0012FF24  40 57 41 00 cc cc cc cc f1 f1 f1 f1 cc cc cc cc 

0x0012FF34 5c57 4100 f4 f4 f4 f4 aa aa aa aa cc cc cc cc 

0x0012FF44  f0 cc cc cc cc cc cc cc f2 f2 f2 f2 cc cc cc cc 

0x0012FF54  00 00 00 00 00 00 00 00 50 57 4100 f3 f3 f3 f3 

虚函数表:

0x00415740  37 10 41 00 cc 11 41 00 be 10 41

共用的虚基类表:

0x0041575C  00 00 00 00 18 00 00 00 10 00 00 00 28 00 00 00

F3的虚基类表:

0x00415750  00 00 0000 f0 ff ff ff

 

按照我们之前已经了解的知识,类F0~F4的内存布局已经是比较明朗的了,也没必要摆出来了。需要注意的是F3和F4的有效对齐是和他们的虚基类F2一样的8字节,虽然他们都因为有虚部而不需要进行整体的自身对齐。

按照我们了解的多继承布局顺序和上一个例子的了解,大体布局应该先是类A的虚函数表指针,然后是各个实基类的实部,然后可能会有虚基类表指针,再然后才到A的各个成员变量,这些元素组成A的实部,后面跟着是按照继承声明顺序的各个虚基类。事实也是这样:

开始4个字节是A的虚函数表指针,因为在后面的成员中F4的有效对齐是8字节,所以在这里这个隐藏的成员变量需要主动填充4个字节以消除自己的加入而对后续成员带来的对齐影响。虚函数表指针主动填充了4个字节之后就是实基类F1了,然后到实基类F4的实部,由实部的对齐规则我们知道它的起始地址的偏移要是类F4的有效对齐8字节的整数倍,所以还需要在F1后面填充4个字节才开始存放F4的实部,F4的实部只有F4的虚基类表指针和其成员小f4。实基类存放完了之后,因为已经从F4的实部那里继承了一个虚基类表指针了,所以就不需要再产生一个虚基类表指针了,F4的虚基类表在这里和派生类A共用即可,然后就可以存放A的成员变量int a了,到此为止虚函数表指针、F1、F4的实部、成员a 这4个元素组成了派生类A的实部,实部要进行自身对齐,有效对齐参数是各个成员和各个实基类中最大的那个,在这里就是实基类F4的有效对齐参数最大,是8字节,所以A的实部的大小要是8字节的整数倍,所以在实部的末尾,也就是a之后还要填充4 字节才满足要求。处理完实部之后就可以开始存放虚部的各个虚基类了,先是F0,F1没有虚部不要理,然后是整个F3,因为F3本身也有实部和虚部,按照之前我们得出的规则就是当F3作为虚基类的时候,在虚部的存放顺序是先虚部然后实部,所以F0之后就是F3的虚部F2,因为F2是8字节对齐的所以在F1和F2之间还要填充7个字节才能满足F2的对齐要求。然后是F3的实部,F3的实部包括F3的虚基类表指针和其成员变量小f3。最后到F4的虚部,因为F4的虚基类F2已经在前面有了所以也就可以忽略了。

类A的虚函数表也还是指向那几个虚函数。

然后看下类A和其基类F4共用的虚基类表,按照规则这个共用的虚基类表里面的项是这样安排的,第一项是虚基类表指针相对于F4本身的起始地址的偏移,之后的项就是F4的各个虚基类的偏移,然后的项就是类A的其他虚基类的偏移,按照声明顺序排放。比如第一项是0,表示F4的起始地址就是虚基类表指针的起始地址,第二项是F4的虚基类F2相对于虚函数表指针的偏移,是0x18,刚好也是F2的起始地址,第三项开始就不是F4的虚基类的项了,从第三项开始的各个项是类A的其他虚基类,按照声明的顺序排放,所以第三项就是A的虚基类F0的偏移,然后F1没有虚部跳过,再到F3,因为F3的虚部F2已经包含在了之前的项里面了所以也跳得,所以第四项就到F3的实部的偏移,是0x28个字节。最后到F4的虚部,因为也是在之前就有了所以也是忽略掉了,这样就得出了这个共享的虚基类表的各个项。

F3的虚基类表是它自己独享的,先是自己起始地址的偏移,然后是虚基类的偏移,可以看到它的虚基类F2 相对于它这个虚基类表指针的偏移值-16个字节,也是正确的。

把情况再变得复杂一些,多继承中虚基类仍然没虚函数,但是实基类有虚函数。像下面这个例子其实已经比较复杂了。

#pragma pack(8)

class F0{public: char f0; F0():f0(0xf0){}};

class F1{public: int f1; F1():f1(0xf1f1f1f1){}};

class F2{public: int f2; double f22; F2():f22(0),f2(0xf2f2f2f2){}};

class F3:virtual F2{public: int f3; F3(): f3(0xf3f3f3f3){}};

class F4:virtual F2{public: int f4; F4(): f4(0xf4f4f4f4){}};

 

class A: virtual F3{public:

    int a;

    A():a(0xaaaaaaaa){}

    virtual void funA2(){}

    virtual ~A(){}

    virtual void funA(){}

};

class B: F1{public:

    int b;

    virtual void funB(){}

    B():b(0xbbbbbbbb){}

};

class C: virtual F0, F4, B, A {public:

    int c;

    virtual void funC(){}

    virtual void funB(){}

    virtual void funA2(){}

    C():c(0x33333333){}

};

 

int main()

{

    A aa;

    B bb;

    C cc;

    return 1;

}

 

sizeof(A)    0x00000028 

0x0012FF30  40 77 41 005c77 41 00 aa aa aa aa cc cc cc cc 

0x0012FF40  f2 f2 f2 f2 cc cc cc cc 00 00 00 00 00 00 00 00  

0x0012FF50  50 77 4100 f3 f3 f3 f3

    由上面的例子推测出A的布局已经是很简单了,注意的是成员变量a后面的4个填充字节是不属于A的实部也不属于A的虚部的,而且A的有效对齐是8字节了。

 

sizeof(B)    0x0000000c 

0x0012FF1C 8c77 4100 f1 f1 f1 f1 bb bb bb bb

    B的布局更加简单,实继承没有任何虚部,只有一个虚函数表指针而已。

 

sizeof(C)    0x00000050 

0x0012FEC4  c8 77 4100 f1 f1 f1 f1 bb bb bb bb cc cc cc cc 

0x0012FED4  94 77 41 00 b0 77 41 00 aa aa aa aa cc cc cc cc 

0x0012FEE4  d0 77 4100 f4 f4 f4 f4 33 33 33 33 cc cc cc cc 

0x0012FEF4  f0 cc cc cc cc cc cc cc f2 f2 f2 f2 cc cc cc cc 

0x0012FF04  00 00 00 00 00 00 0000 a8 77 4100 f3 f3 f3 f3

C的布局才是我们的看点。可以说一个多继承中的基类终极布局顺序是这样的:先存放实基类中实部有虚函数表指针的基类,然后是普通的实基类,最后是各个虚基类。按照这个逻辑,因为B和A的实部都有虚函数表指针,所以类C的基类布局大概像是这个继承顺序,class C: B, A, F4, virtual F0 {……}; 实部的安排先是B,然后是A,然后是F4。

所以最开始是B的实部,B没有虚部所以也就是整个B类了,然后是A的实部,由实部的字节对齐规则可以知道A的有效对齐是8字节,其实部的起始地址就是代表了A的起始地址,所以他的起始地址偏移要是8字节的整数倍,所以在B后填充了4个字节,才到A的实部。A的实部之后就是F4的实部,同样F4的有效对齐参数也是8字节,所以也还是要填充,然后因为已经从基类中继承了有虚基类表指针了,所以就不需要再产生一个了,就直接开始存放C的成员变量小c,之前的这几个元素就组成了C的实部,C的实部进行自身对齐还要在后面填充4个字节,所以C 的实部的大小就是0x30个字节。然后到虚部,虚部按照声明的顺序一个个的来就得了,先是F0,然后是F4的虚部F2,再是B的虚部,因为B没有虚部所以跳过,就到了A的虚部了,也就是F3,因为F3的虚部F2已经有了所以就不能再重复了,然后就只在最后存放了F3的实部。

这就是整个类C的布局了。也不是太困难。

 

和B共用的虚函数表:

0x004177C8 4f11 41 00 d6 11 41 00

0041114F  jmp         C::funB (411C20h)

004111D6  jmp         C::funC (411BE0h)

    在C的最开始就是和B共用的虚函数表指针,指向同一个表。因为C里面的虚函数覆盖了B里面的虚函数,所以表里面的对应的项也是要用C里面的那个函数来覆盖掉的,而C里面的虚函数funC没有被任何基类覆盖,所以要在这里追加上去,所以这个表就有两项。

 

A的虚函数表:

0x00417794  37 10 41 00 67 12 4100 c8 10 41 00

00411037  jmp         C::funA2 (4118F0h)

00411267  jmp         [thunk]:C::`vector deleting destructor‘ (411980h)

       [thunk]:C::`vector deleting destructor‘:

00411980  sub         ecx,10h

00411983  jmp         C::`scalar deleting destructor‘ (4110F0h)

004110C8  jmp         A::funA (411800h)

    A的虚函数表就比较好理解了,把相应被覆盖掉的函数的地址改正确即可,也就是funA2和C的析构函数两项。那个调整块在这里先不说。

 

A的虚基类表:      

0x004177B0  fc ff ff ff 24 00 00 00 34 00 00 00

    A的虚基类表在这里不是和类C一起共用,C是和按照声明顺序的实基类中的第一个有虚基类表指针的基类共用一个的,虽然A在内存里面排放在前面但是声明却是在F4的后面,所以C是和F4共用一个虚基类表而不是和A共用。A的虚基类表还是它自己独享的。里面的各项分别是它自己的起始地址偏移、虚基类F2的偏移、虚基类F3的实部的偏移。

 

F4共用的虚基类表:

0x004177D0  00 00 00 00 18 00 00 00 10 00 00 00 28 00 00 00

    这个就是C和F4共用的虚基类表了。里面的各项是F4自己的起始地址偏移、F4的虚基类F2的偏移、F0的偏移、F3实部的偏移。

 

F3的虚基类表:

0x004177A8  00 00 0000 f0 ff ff ff

F3的虚基类表还是他自己独享的。

 

到此我们就可以推断出派生类有虚函数和无虚函数时的统一继承布局顺序。就是先安排实继承的基类,然后是虚继承。其中实继承的基类中实部有虚函数表指针的又要先被安排。同样优先级的基类按照继承声明顺序存放。

 

 

我们可以开始进入到第二个大块,虚基类有虚函数的情况。

也还是从最简单的情况开始,没有虚函数的派生类虚继承一个有虚函数的虚基类。

#pragma pack(8)

class B {public:

    int b;

    virtual void funB(){}

    B():b(0xbbbbbbbb){}

};

class F1: virtual B{public: int f1; F1():f1(0xf1f1f1f1){}};

 

sizeof(F1)    0x00000010 

0x0012FF54  50 67 4100 f1 f1 f1 f1 40 67 41 00 bb bb bb bb

虚基类表:

0x00416750  00 00 00 00 08 00 00 00

虚函数表:

0x00416740  40 11 41 00

00411140  jmp         B::funB (411690h)

 

这个还比较简单,类F1的布局就是先虚基类表指针,然后是成员f1一起组成实部,然后是整个B类作为虚部。可见在虚基类有虚函数表指针的情况下,这个虚函数表指针是不需要放在派生类的最开始的,因为它是虚继承的。它应该和整个虚基类作为一个整体被放在派生类的虚部里面。虚基类表和虚函数表的值也很好理解,就不赘述了。

现在再复杂一点,派生类也有虚函数,虚继承的虚基类也有虚函数,但是派生类的虚函数并没有覆盖虚基类里面的虚函数的情况。如下

#pragma pack(8)

class B {public:

        int b;

        B():b(0xbbbbbbbb){}

    virtual void funB(){}

};

 

class C: virtual B{public:

    int c; double c2;

    virtual void funC(){}

    C():c(0x33333333),c2(0){}

};

 

    C的布局也可以按照我们常规的思想来猜测,C本身有虚函数,所以最开始肯定是C的虚函数表指针了,因为它都是会放在第一位的。然后因为有虚继承,还会有虚基类表指针,然后才到C的成员变量,实部就到这里了,之后就到虚部,也就是整个B了。看下抓包的数据。

 

sizeof(C)    0x00000028 

0x0012FF2C  68 57 41 00 cc cc cc cc 70 57 41 00 cc cc cc cc 

0x0012FF3C  33 33 33 33 cc cc cc cc 00 00 00 00 00 00 00 00  

0x0012FF4C 5c57 41 00 bb bb bb bb

 

虚函数表:

0x00415768  8b 11 41 00

0041118B  jmp         C::funC (411600h)

虚基类表:

0x00415770  f8 ff ff ff 18 00 00 00

B的虚函数表:

0x0041575C  18 11 41 00

00411118  jmp         B::funB (411520h)

 

得到的结果也还算正常,和我们预期的一样,可以看到排在第一位的确实就是虚函数表指针,然后才是存放虚基类表指针,也可以看到因为这两个特殊的隐藏成员需要主动对齐的原因都在其后填充了4个字节,c和c2之间的4个字节就是常规的填充了。

所以在这种情况下会有两个隐藏成员变量,而不会再和虚基类共用一个虚函数表了,因为虚基类要放在后面,在这里没办法很好的共享,因为如果还和虚基类共享一个虚函数表的话即使访问一个和虚基类没有任何联系的虚函数也需要先找到虚基类的位置然后再找到对于的虚函数,这样的开支就没必要了。

那么我们看下当派生类的虚函数全部把虚基类的虚函数覆盖掉的时候,这种情况下是不是就可以和虚基类共享一个虚函数表了呢?理论上是可以的,而且也是合理的。

#pragma pack(8)

class B {public:

        __int64 b;

        B():b(0xbbbbbbbbbbbbbbbb){}

    virtual void funB(){}

    virtual void funB2(){}

    virtual void funB3(){}

};

 

class C: virtual B{public:

    int c;

    virtual void funB(){}

    virtual void funB3(){}

    C():c(0x33333333){}

};

 

sizeof(C)    0x00000020 

0x0012FF2C  64 57 41 00 33 33 33 33 cc cc cc cc 00 00 00 00  

0x0012FF3C  54 57 41 00 cc cc cc cc bb bb bb bb bb bb bb bb

虚基类表:

0x00415764  00 00 00 00 10 00 00 00

虚函数表:

0x00415754  be 10 41 009f11 41 00 cd 10 41 00

004110BE  jmp         C::funB (4135C0h)

0041119F  jmp         B::funB2 (411580h)

004110CD  jmp         C::funB3 (4116D0h)

 

可以看到,当派生类C的虚函数都覆盖掉了虚基类的虚函数的时候,是可以和虚基类共享一个虚函数表的,而且也是很合逻辑的。所以只会产生虚基类表,也就是第一个4字节,可以看到表里面的东西确实就是虚基类表。而和B共用的虚函数表里面的B被C覆盖的项也已经改成了正确的函数地址了。

最后看到蓝色的那4个0,这个东西是比较蹊跷的地方,至于为什么会产生这4个字节的东西,他的理由是什么,我是没有办法搞懂了,跟进了汇编码也没有什么实际的收获,它在汇编码里面使用了一个常量,这个常量的意义是什么无从得知,总之我测试了几种情况,使用的常量都不一样但是最终导致的这4个字节就是都是为0,说白了就是使用虚基类表里面的这个虚基类的偏移值减去一个和这个偏移值相等的常量,所以导致这个值最后都是0,没有测试到为其他值的情况,实在是搞不懂为什么了~……

后来我查了一下MSDN,这4个字节是一个叫做vtordisp 的东西,我不知道怎么读也不知道是什么意思。这个东西是一个比较特殊的东西,我是从编译指令里面使用-d1reportAllClassLayout 或者-d1reportSingleClassLayoutXXX这两个编译指令观察类对象内存布局的时候看到的这个关键字,然后顺藤摸瓜勉强找到了一些资料,但是谜底始终没有解开,即使MSDN也没提供太多有意义的解释。我们其实可以用编译指令禁止产生这个东西,MSDN上面是这么说的:

 

#pragma vtordisp({on | off} )

The vtordisp pragma is applicable only to code that uses virtual bases. If a derived class overrides a virtual function that it inherits from a virtual base class, and if a constructor or destructor for the derived class calls that function using a pointer to the virtual base class, the compiler may introduce additional hidden "vtordisp" fields into classes with virtual bases.

The vtordisp pragma affects the layout of classes that follow it. The /vd0 and /vd1 options specify the same behavior for complete modules. Specifying off suppresses the hidden vtordisp members. Specifying on, the default, enables them where they are necessary. Turn off vtordisp only if there is no possibility that the class‘s constructors and destructors call virtual functions on the object pointed to by the this pointer.

 

大概的意思就是说,vtordisp 这个东西只使用于虚基类里面,如果一个派生类的虚函数覆盖了虚基类里面的虚函数,而且派生类的构造函数或者析构函数干嘛干嘛了的话,编译器就会为虚基类生成这个隐藏的字段。翻译不准确,自己看原文哈~······

但是我发现,即使派生类里面的构造函数或者析构函数什么都没干,就是一个空函数的话,只要你明写了一个构造或者析构函数在那里,他就会产生vtordisp 这个东西;而如果你在派生类里面没有明写一个构造函数或者析构函数的话,编译器就不会为虚基类产生这个字段。

最后一句话说,只有在什么什么的情况下才能将vtordisp 关闭,我测试过蛮多种情况,在关闭了vtordisp 的情况下,无论派生类的构造函数怎么调用虚函数、还有我强行把这个字段的值给改掉、通过this指针转换再调用虚函数等手段都没有发现什么错误,程序都是正常执行,而且调用的函数都是正确的那个,理论上也应该是这样,因为在进入到派生类的构造函数的大括号的时候,此时其实派生类对象已经全部构造完毕了,你使用什么方式和使用什么指针来调用或者在构造函数里面调用和在外面调用又有什么区别呢?为什么MSDN上要说不能在构造或者析构函数里面调用呢?这个着实不懂,我觉得是微软耍了我们,等微软自己来维基解密吧。~

而且还有一个很搞笑的地方,应该是从vs2003开始吧,每一个版本的MSDN在这里都有这么一句话:

Note:

vtordisp is now deprecated and will be removed in a future release of Visual C++.

 

    每一个版本都说这个东西将会在以后的版本中去掉,但是每个之后的版本都没去掉,而且还是这样下去一直在耍我们~~……

 

关于vtordisp这个东西,我只知道一些实际得出的结论而已:

当派生类有虚函数覆盖了虚基类里面的虚函数,而且派生类有显式构造或者析构函数的话,编译器就会为每一个这样的虚基类生成一个vtordisp字段,位于其起始地址之前开辟这4个字节,如果虚基类被覆盖的仅仅是虚析构函数的话除外,只是被覆盖了虚析构的话虚基类不会产生这4个字节,只是在其虚函数表里面将相应的项改成正确的地址而已。

当派生类有虚函数覆盖了虚基类里面的虚函数,而且派生类没有任何显式构造或者析构函数的话,编译器就不会为虚基类开辟这4个字节。

如果使用了编译指令,关闭了vtordisp的话,编译器自然是都不会产生这个东西的了。虽然我们更加关注的是默认情况。

编译器为虚基类产生的这4个字节对于整个派生类来说是属于虚部的,也可以理解为是属于那个虚基类的。对齐规则也比较特殊:当虚基类因为这4个字节的加入,而起始地址满足不了对齐规则的时候会在这4个字节的前面填充字节,而不会在虚基类和这4个字节之间填充,虚基类的起始地址一定会是紧紧挨着这4个字节的……

所以上面的蓝色的4个字节前面的CCC就是因为虚基类B的起始地址的对齐要求而填充的字节,既不属于实部又不属于虚部,而那4个蓝色的00字节,也就是vtordisp字段,却是属于虚部的。如果类C再有一个4字节的成员变量的话,蓝色的0前面的填充字节就刚好又不需要了。

class C: virtual B{public:

    int c;int c2;

    virtual void funB(){}

    virtual void funB3(){}

    C():c(0x33333333),c2(0xc2c2c2c2){}

};

sizeof(C)    0x00000020 

0x0012FF2C  64 57 41 00 33 33 3333 c2 c2 c2 c2 00 00 00 00  

0x0012FF3C  54 57 41 00 cc cc cc cc bb bb bb bb bb bb bb bb

    所以这个东西的对齐规则貌似比虚函数表指针、虚基类表指针这些隐藏成员还要畸形。然后我们来证明它是属于虚部的。

#pragma pack(8)

class B {public:

        __int64 b;

        B():b(0xbbbbbbbbbbbbbbbb){}

    virtual void funB(){}

    virtual void funB2(){}

    virtual void funB3(){}

};

class C: virtual B{public:

    int c; 

    virtual void funB(){}

    virtual void funB3(){}

    C():c(0x33333333) {}

};

class D:virtual C{public:

    int d;int d2;

    D():d(0xdddddddd),d2(0xd2d2d2d2){}

};

 

sizeof(C)    0x00000020 

0x0012FF2C  64 57 41 00 33 33 33 33 cc cc cc cc 00 00 00 00  

0x0012FF3C  54 57 41 00 cc cc cc cc bb bb bb bb bb bb bb bb

 

sizeof(D)    0x00000028 

0x0012FEFC  48 58 41 00 dd dd dd dd d2 d2 d2 d2 00 00 00 00  

0x0012FF0C  e4 57 41 00 cc cc cc cc bb bb bb bb bb bb bb bb  

0x0012FF1C  78 57 41 00 33 33 33 33

 

我们使用类D虚继承C就可以看到结果了,我们知道在虚继承基类的时候,C在D的虚部里面的排放次序是先C虚部然后再C实部。而在D的成员变量d2之后就是D的虚部了,所以我们看到那4个莫名其妙的0确实是位于D的虚部的开始的,也就证明了它在类C里面是属于类C的虚部的。也可以看到在D的虚部的最后是安排了类C的实部,一个虚基类表指针和C的成员变量小c。也可以侧面论证了这4个字节的对齐规则,因为在虚基类B的前面开辟了这4个字节,而虚基类B的起始地址刚好能满足它的对齐要求,所以在D的虚部里面就没有必要再在那4个0字节前面填充字节了。

然后我们让派生类里面没有任何显式的构造函数和析构函数,看下vtordisp字段是不是不会再生成了呢。

#pragma pack(8)

class B {public:

        __int64 b;

        B():b(0xbbbbbbbbbbbbbbbb){}

    virtual void funB(){}

    virtual void funB2(){}

    virtual void funB3(){}

};

class C: virtual B{public:

    int c;

    virtual void funB(){}

    virtual void funB3(){}

//     C():c(0x33333333){}

};

int main()

{

    B bb;

    C cc;cc.c = 0x33333333;

    return 1;

}

 

sizeof(C)    0x00000018 

0x0012FF34 5c77 41 00 33 33 3333 ac7a41 00 cc cc cc cc 

0x0012FF44  bb bb bb bb bb bb bb bb

虚基类表:

0x0041775C  00 00 00 00 08 00 00 00

C和B共用的虚函数表:

0x00417AAC 4a11 41 008f12 41 00 ff 10 41 00

0041114A  jmp         C::funB (411AB0h)

0041128F  jmp         B::funB2 (411970h)

004110FF  jmp         C::funB3 (4119C0h)

 

可以很清楚的看到,因为派生类没有任何显式的构造函数或者析构函数,所以vtordisp字段也就没有必要产生了。而且,很自然的,之前因为vtordisp字段而引入的填充在这里也没必要了,所以整个派生类C的总大小下降了8个字节,在C类的成员变量小c之后就紧接着是虚基类B了。自然,虚基类表里面的值要变一下,而虚函数表的项没什么变化。

我们再来看下,派生类只是覆盖了虚基类的虚析构函数的情况下,虚基类是不是不会产生vtordisp字段。

#pragma pack(8)

class B {public:

        __int64 b;

        B():b(0xbbbbbbbbbbbbbbbb){}

    virtual void funB(){}

    virtual ~B(){}

};

class C: virtual B{public:

    int c;

    C():c(0x33333333){}

    virtual ~C(){}

};

 

sizeof(C)    0x00000018 

0x0012FF28 5c77 41 00 33 33 3333 ac7a41 00 cc cc cc cc 

0x0012FF38  bb bb bb bb bb bb bb bb

虚函数表:

0x00417AAC 4f11 4100 c1 12 41 00

0041114F  jmp         B::funB (411690h)

004112C1  jmp         C::`scalar deleting destructor‘ (4118F0h)

 

在这种情况下,也确实有派生类的虚函数覆盖了虚基类的虚函数,只不过这个虚函数是虚析构函数而已,这种情况下也是不会产生vtordisp字段的,就算派生类显式写了构造函数和析构函数都是一样,自然虚函数表里面的对应项也是该改成正确的地址的。所以说vtordisp 这个东西就是有点畸形。

而且只要是派生类覆盖了其层次结构中的任何一个虚基类中的虚函数的话,这个被覆盖虚函数的虚基类也都是会产生vtordisp字段的。比如我们实继承一个C。

class A: C{public:

    int a;

    A():a(0xaaaaaaaa){}

    void funB(){}

};

sizeof(A)    0x00000028 

0x0012FEF8  84 67 41 00 33 33 33 33 aa aa aa aa cc cc cc cc 

0x0012FF08  cc cc cc cc 00 00 00 00 74 67 41 00 cc cc cc cc 

0x0012FF18  bb bb bb bb bb bb bb bb

虚基类表:

0x00416784  00 00 00 00 18 00 00 00

虚函数表:

0x00416774  58 12 41 00 31 11 41 00

00411258  jmp         A::funB (411AE0h)

00411131  jmp         A::`vector deleting destructor‘ (411B50h)

 

    可以看到,在单独的类C里面的话虚基类B是不会产生vtordisp字段的,但是由于再之后的派生类A覆盖了B里面的虚函数,所以最终在类A里面的B是会产生vtordisp字段的,而不一定就得A虚继承C才行,只要派生类覆盖了继承层次中的任何一个虚基类里面的虚函数,那么这个虚基类就会产生vtordisp字段,当然派生类得有显示的构造或者析构函数等这些前提。

以上一些例子就是分别证明了我前面所说的,关于vtordisp这个东西,我知道的一些实际得出的结论。然后我们再接着之前的话题继续拓展。

 

而当派生类的虚函数只是部分覆盖了虚基类里面的虚函数的话,其实大概我们也可以想到了,那就是还是要产生一个派生类独立的虚函数表指针,而且当派生类有显示的构造或者析构函数的时候,被覆盖了虚函数的虚基类要在其起始地址前面开辟4个字节,也就是vtordisp字段。

#pragma pack(8)

class B {public:

        __int64 b;

        B():b(0xbbbbbbbbbbbbbbbb){}

    virtual void funB(){}

    virtual void funB2(){}

    virtual void funB3(){}

};

class C: virtual B {public:

    int c; 

    virtual void funB(){}

    virtual void funC(){}

    virtual void funB3(){}

    C():c(0x33333333) {}

};

 

sizeof(C)    0x00000020 

0x0012FF2C  68 57 41 00 e0 57 41 00 33 33 33 33 00 00 00 00  

0x0012FF3C  54 57 41 00 cc cc cc cc bb bb bb bb bb bb bb bb

C的独立虚函数表:

0x00415768  49 12 41 00

00411249  jmp         C::funC (4135C0h)

虚基类表:

0x004157E0  fc ff ff ff0c00 00 00

B的虚函数表:

0x00415754  be 10 41 009f11 41 00 cd 10 41 00

004110BE  jmp         C::funB (4116F0h)

0041119F  jmp         B::funB2 (411580h)

004110CD  jmp         C::funB3 (4114E0h)

 

可以看到类C会产生一个独立的虚函数表来存储那些没有覆盖掉基类虚函数的虚函数项;而当派生类有显示的构造或者析构函数的时候,被覆盖了虚函数的虚基类就会在他的起始地址事情开辟4个字节的空间。

 

 

类的各种继承状况的布局模型大概就这样了。知道了这些我们就可以预测出很复杂层次的类的每一个字节。

还是小总结一下吧,推断类的内存布局先得把类的继承层次的排放次序弄好,类的继承层次的一个终极排放次序就是如上所说的,先是实继承中实部有虚函数表指针的基类,然后是普通的实继承基类,整个派生类组成了实部之后再排放虚部,继承都是按照实继承和虚继承的本质意义来继承的,然后注意字节对齐的规则和一些特殊对齐规则,还有隐藏成员的产生等就差不多了。

其实也没什么好总结的,太多东西了就算总结出来没实践过也是没用的,毕竟这个东西实在是太复杂了。

 

此致。

C++类对象内存布局(四)

原文:https://www.cnblogs.com/yulinhanhonor/p/8454455.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!