[原] Inside C++ Object model 笔记(2)

发布时间:2011-12-5 23:36
分类名称:C++


The Semantics of Data

class X { };
class Y : public virtual X { };
class Z : public virtual X { };
class A : public Y, public Z { };

sizeof(X) = ?
sizeof(Y) = ?
sizeof(Z) = ?
sizeof(A) = ?

你会发现,不同机器上,不同的编译器里,可能结果都会不一样。

通常情况下,X的大小应该在都是一样的,大小为1byte,这是被编译器安插进去的一个char,这样才使得这个class的object拥有独一无二的地址。

Y和Z的大小会依据下面三种情况而定:

  1. 语言本身造成的额外负担。由于是虚继承,不同编译器,实现的方式略有差异。例如微软实现方式为插入一个指向虚基表的指针。

    而其他编译器可能不插入额外指针,而是通过其他机制。

  2. 编译器对特殊情况的优化处理。X那1byte的大小也会落在Y和Z身上,但某些编译器会对empty virtual base class做特殊处理,它会去掉这1byte(本身没用,只是占位用的)。

    微软的就是这么处理的。

  3. 字节对齐的问题。每个编译器下,默认的字节对齐可能都不一样。而且某些编译器支持设定特定的字节对齐。

下面安装4字节对齐的方式来分析:

  1. 不对empty virtual base class做处理:

    sizeof(X) = 1
    sizeof(Y) = 1 + 4 + 3 = 8 // 为何加3?因为要按照4字节的对齐1+4=5, 加3为8,正好能被4整除
    sizeof(Z) = 1 + 4 + 3 = 8
    sizeof(A) = 1 + 4 + 4 + 3 = 12

  2. 对empty virtual base class做处理:(微软就做了处理,可以使用virtual studio 6-9做实验)

    sizeof(X) = 1
    sizeof(Y) = 4
    sizeof(Z) = 4
    sizeof(A) = 4 + 4 = 8

     

Data Member 的布局

C++Standard要求,同一个access section(private,public,protected)中,member排列只要符合"较晚出现的member在class object中有较高的地址。没有要求一定要连续。而且允许那些从内部产生处理的members(如vptr)自由放在任何位置上。(有的编译器vptr在所有member之后,有的则在所有member之前)。

目前,各家编译器都是把一个以上的access section连锁成一个,依照声明次序,成为一个连续区域。

Data Member的存取

Point3d origin;
origin.x = 0.0;

x的存取成本是什么?分几种情况考虑:

  1. Static Data Members Static Members相当于一个全局变量,存取没有任何代价,无论它是从哪儿或者如何继承而来的。
  2. NonStatic Data Members 在不继承,单一继承,多重继承的情况下,是没有什么代价的。但如果是存取虚继承链中的virtual base class的member,那就会有速度上影响了。

继承

Derived class object = itself members + base class(es) member

至于它们的排列次序,C++ standard没有规定,可以自由安排。大部分编译器都是base class members在前,derived的在后。 virtual base class除外。

单一继承(没有多态)

下面是关于一个字节补位与继承的问题
class Concrete1
{
private:
    int val;
    char bit1;
}

class Concrete2 : public Concrete1
{
private:
    char bit2;
}

class Concrete3 : public Concrete2
{
private:
    char bit3;
}

为什么字节补位也被继承下来了?看看下图:

Concrete2 *pc2;
Concrete1 *pc1_1, *pc1_2;

pc1_1 = pc2;
*pc1_2 = *pc1_1; //就发生了上图的情况。

 

单一继承(有多态)

多态带来的代价:

  1. virtual table的产生
  2. vptr的安插
  3. 构造函数的加强(插入初始化vptr的代码)
  4. 析构函数的加强(插入相关virtual table清理操作)

 

vptr被放在Class的尾端

 

Vptr被放在Class的前端 (微软的编译就是放在前端,低字节处)

 

vptr被放在了base class的尾端

Point 3d;
Point *p2d = &3d;

单继承下,基本上指针无需调整,但vptr在前端例外,如果Base class没有虚函数而其子类有虚函数,指针需要加一个vptr的长度。

因为这个vptr是子类安插的,属于其子类。使用微软编译器打印出来俩种单一继承的vptr:

1. 从父类继承来的vptr:

class CTestD size(12):
 +---
 | +--- (base class CTest)  // 这是父类范围
 0 | | {vfptr}              // vfptr 在父类范围内
 4 | | a
 | +---
 8 | m_n
 +---

CTestD::$vftable@:
 | &CTestD_meta
 |  0
 0 | &CTestD::{dtor}
 1 | &CTestD::somefunc

2. 父类没有虚函数,但其子类有:

 class CTestD size(12):
 +---
 0 | {vfptr}                // vfptr在子类范围
 | +--- (base class CTest)  // 父类范围,如果将子类实例地址赋值给父类指针,指针首先需要加一个vptr长度。
 4 | | a
 | +---
 8 | m_n
 +---

CTestD::$vftable@:
 | &CTestD_meta
 |  0
 0 | &CTestD::somefunc

多重继承

多重继承的问题出现在derived class objects和其第二个和后继的base class objects直接的转换。

Vertex3d v3d;
Vertex *pv;
Point2d *p2d;
Point3d *p3d;

pv = &v3d;
转换为:
pv = pv3d ? (vertex *)((char *)&v3d + sizeof(Point3d)) : 0;

而:

p2d = &v3d;
p3d = &v3d;
无需转换。(Point2d中有virtual functions,如果没有,在vptr前置的情况系啊,可能需要进行移位)

下图是几个class对象的布局:

class Point2d
{
public:
        int _x;
        int _y;

        virtual void test2d()
        {

        }
};

class Point3d : public Point2d
{
public:
        int _z;
};

class Vertex
{
public:
        Vertex *_next;
        virtual void testVertex()
        {

        }
};

class Vertex3d :
        public Point3d,
        public Vertex
{
public:
        int mumble;
};

 

VS2008布局如下:

class Vertex3d size(28):
 +---
 | +--- (base class Point3d)
 | | +--- (base class Point2d)
 0 | | | {vfptr} // 从Pointer2d继承下的vfptr
 4 | | | _x
 8 | | | _y
 | | +---
12 | | _z
 | +---
 | +--- (base class Vertex)
16 | | {vfptr} // 从Vertex继承下的vfptr
20 | | _next
 | +---
24 | mumble
 +---

Vertex3d::$vftable@Point3d@:
 | &Vertex3d_meta
 |  0
 0 | &Point2d::test2d
Vertex3d::$vftable@Vertex@:
 | -16
 0 | &Vertex::testVertex

所以,非虚继承的多继承下,子类存储着多个vptr,每一条单条的继承,都能继承下vptr。也就对应多张虚表。因为多个父类不能共用一张虚表,调整起来,会更加混乱。

虚拟继承
为何需要虚拟继承?看下图。

如果 在iostream中引用ios中的成员,编译器就会因为无法判断到底引用的是istream的ios,还是ostream的ios。(当然可以使用作用符强制指定,但子类可能并不需要知道太多细节)如果为虚继承,ios的实体则为一个。

编译器一般将含有virtual base class的subobjects(如iostream)分割为两部分,一部分不变,一部分共享(ios)。不变部分拥有固定的offset。而共享部分会随着每次的派生操作而发生变化。cfont编译器实现上,会为每一个derived class object安插一些指针,来指向virtual base class,来存取virtual base class members。

Point2d *p2d = pv3d; 转换为: Point2d *p2d = pv3d ? pv3d->__vbcPoint2d : 0;

微软的实现方式为产生virtual base class table,每个class object如果有一个或者多个virtual base classes,编译器就会安插一个指针,指向virtual base class table,table里面存放了真正的virtual base class指针。

下面一段代码,在微软编译器中表现的内存如下:

class Base
{
public:
 int base;
 virtual void fooBase() {}
};

class A : virtual public Base
{
public:
 int a;
 virtual void fooA() {}
};

class B : virtual public Base
{
public:
 int b;
 virtual void fooB() {}
};

class Derived : public A, public B
{
public:
 int d;
 virtual void fooD() {}
};


布局:

1>class Derived size(36):
1> +---
1> | +--- (base class A)
1> 0 | | {vfptr} // A的虚函数指针,指向virtual funtions table
1> 4 | | {vbptr} // A的虚父类指针,指向virtual base class table
1> 8 | | a
1> | +---
1> | +--- (base class B)
1>12 | | {vfptr} // B的虚函数指针,指向virtual funtions table
1>16 | | {vbptr} // B的虚父类指针,指向virtual base class table
1>20 | | b
1> | +---
1>24 | d  
1> +---  // 到此为止,都是不变的部分
1> +--- (virtual base Base)
1>28 | {vfptr} // Base的虚函数指针,指向virtual funtions table
1>32 | base
1> +--- // 虚基类,可变部分


1>Derived::$vftable@A@:  // A的虚函数表
1> | &Derived_meta
1> |  0                  // offset(Derived,A) = 0
1> 0 | &A::fooA
1> 1 | &Derived::fooD


1>Derived::$vftable@B@:  // B的虚函数表
1> | -12                 // offset(Derived,B) = 12
1> 0 | &B::fooB


1>Derived::$vbtable@A@:  // A的虚基类表
1> 0 | -4                // offset(A,A.vbptr) = 4
1> 1 | 24 (Derivedd(A+4)Base)


1>Derived::$vbtable@B@:  // B的虚基类表
1> 0 | -4                // offset(B,B.vbptr) = 4
1> 1 | 12 (Derivedd(B+4)Base)


1>Derived::$vftable@Base@: // Base的虚函数表
1> | -28                   // offset(Derived,Base) = 0
1> 0 | &Base::fooBase


1>Derived::fooD this adjustor: 0
1>vbi:    class  offset o.vbptr  o.vbte fVtorDisp
1>        Base      28       4       4      0