浅谈C++中虚基类的内存布局

来源:互联网 发布:闲鱼淘宝介入的客服 编辑:程序博客网 时间:2024/06/04 18:24

    今天重温C++的知识,当看到虚基类这点的时候,那时候也没有太过追究,就是知道虚基类是消除了类继承之间的二义性问题而已,可是很是好奇,它是怎么消除的,内存布局是怎么分配的呢?于是就深入研究了一下,具体的原理如下所示:来至于网络:http://www.cdtarena.com

 

    C++中,obj是一个类的对象,p是指向obj的指针,该类里面有个数据成员mem,请问obj.memp->mem在实现和效率上有什么不同。

 

    答案是:只有一种情况下才有重大差异,该情况必须满足以下3个条件:

 

    1)、obj是一个虚拟继承的派生类的对象

 

    2)、mem是从虚拟基类派生下来的成员

 

    3)、p是基类类型的指针

 

    当这种情况下,p->mem会比obj.mem多了两个中间层。(也就是说在这种情况下,p->memobj.mem要明显的慢)

 

    WHY

 

    如果好奇心比较重的话,请往下看 :

 

    1、虚基类的使用,和为多态而实现的虚函数不同,是为了解决多重继承的二义性问题。

 

    举例如下:

 

    class A

 

    {

 

    public:

 

    int a;

 

    };

 

    class B : virtual public A

 

    {

 

    public:

 

    int b;

 

    };

 

    class C :virtual public A

 

    {

 

    public:

 

    int c;

 

    };

 

    class D : public B, public C

 

    {

 

    public:

 

    int d;

 

    };

 

    上面这种菱形的继承体系中,如果没有virtual继承,那么D中就有两个A的成员int a;继承下来,使用的时候,就会有很多二义性。而加了virtual继承,在D中就只有A的成员int a;的一份拷贝,该拷贝不是来自B,也不是来自C,而是一份单独的拷贝,那么,编译器是怎么实现的呢??

 

    在回答这个问题之前,先想一下,sizeofA),sizeofB),sizeofC),sizeofD)是多少?(在32x86linux2.6下面,或者在vc2005下面)在linux2.6下面,结果如下:sizeofA = 4; sizeofB = 12; sizeofC = 12; sizeofD = 24;sizeofB)为什么是12呢,那是因为多了一个指针(这一点和虚函数的实现一样),那个指针是干嘛的呢?

 

    那么sizeofD)为什么是24呢?那是因为除了继承B中的bC中的cA中的a,D自己的成员d之外,还继承了BC多出来的2个指针(BC分别有一个)。再强调一遍,D中的int a不是来自B也不是来自C,而是另外的一份从A直接靠过来的成员。

 

    如果声明了D的对象d D d

 

    那么d的内存布局如下:

 

    vb_ptr: 继承自B的指针

 

    int b:继承自B公有成员

 

    vc_ptr:继承自C的指针

 

    int c:继承自C的共有成员

 

    int d D自己的公有成员

 

    int a:继承自A的公有成员

 

    那么以下的用法会发生什么事呢?

 

    D dD;

 

    B *pb = &dD;

 

    pb->a;

 

    上面说过,dD中的int a不是继承自B的,也不是继承自C的,那么这个B中的pb->a又会怎么知道指向的是dD内存中的第六项呢?

 

    那就是指针vb_ptr的妙用了。原理如下:(其实g++3.4.3的实现更加复杂,我不知道是出于什么考虑,而我这里只说原理,所以把过程和内容简单化了)

 

    首先,vb_ptr指向一个整数的地址,里面放的整数是那个int a的距离dD开始处的位移(在这里vb_ptr指向的地址里面放的是20,以字节为单位)。编译器是这样做的:

 

    首先,找到vb_ptr(这个不用找,因为在g++中,vb_ptr就是B*中的第一项,呵呵),然后取得vb_ptr指向的地址的内容(这个例子是20),最后把这个内容与指针pb相加,就得到pb->a的地址了。

 

    所以说这种时候,用指针转换多了两个中间层才能找到基类的成员,而且是运行期间。

 

    由此也可以推知dD中的vb_ptrvc_ptr的内容都是一样的,都是指向同一个地址,该地址就放20(在本例中)

 

    如下的语句呢:

 

    A *pa = &dD;

 

    pa->a = 4;

 

    这个语句不用转换了,因为编译器在编译期间就知道他把A中的成员插在dD中的那个地方了(在本例中是末尾),所以这个语句中的运行效率和dD.a是一样的(至少也是差不多的)

 

    这就是虚基类实现的基本原理。

 

    注意的是:那些指针的位置和基类成员在派生类成员中的内存布局是不确定的,也就是说标准里面没有规定int a必须要放在最后,只不过g++编译器的实现而已。c++标准大概只规定了这套机制的原理,至于具体的实现,比如各成员的排放顺序和优化,由各个编译器厂商自己定~

=======================================================================================================

Q:C++ Interface接口类中的纯虚函数是否占用内存?

例: 设二个库 A.DLL, B.DLL

A.DLL如下

interface ITest{public:  virtual ~ITest() {}  /// 测试OK  virtual int TestOK() = 0;}

B.DLL如下

class AppTest : public ITest{private:  int x, y, z;public:  virtual int TestOK() { return true; }}

如果我要A.DLL库中为ITest接口为添加一个 virtual int TestNO() = 0; 接口, 是否会影响到B.DLL AppTest类中的变量x,y,z内存布局

A:

不会影响x、y、z的内存布局。

理由:

  1. ITest作为基类,其析构函数是virtual的,这是很正确的。这样做的结果就会多出一个虚函数表指针。此后,无论你在ITest中增加纯虚函数还是普通虚函数,都不会改变ITest的内存布局。

  2. 派生类继承ITest,那么在派生类对象中,会有一个ITest这样的子对象,而且该子对象会保证其原始的完整性(就像一个真正的ITest对象一样,只不过它存在于派生类对象中),所以由于ITest的内存布局没有发生变化,那么这一部分也不会发生变化。

  3. 如果在派生类中重写了ITest的虚函数或者纯虚函数,由于2说到的原因,派生类已经有了一个虚函数表指针,在VS2003/5中并不会再增加一个虚函数表指针,所以这样的做法也不会是的派生类对象的内存布局发生变动。

 

 

原创粉丝点击