首页 > 代码库 > data语意学

data语意学

引例:

class X{};
class Y:public virtual X{};
class Z:public virtual X{};
class A:public Y,public Z{};
X Y Z A类对象的大小是多少?? 
1> 没有提供empty virtual base特殊支持的编译器:1 8 8 12
2> 提供了empty virtual base特殊支持的编译器:1 4 4 8
 
    一个class的data members,一般而言,可以表现这个class在程序执行时的某种状态。
    nonstatic data members放置的是“个别的class object”感兴趣的数据。 C++对象模型把nonstatic data members直接放置在每一个class object之中,对于继承而来的nonstatic data members也是如此。
    static data members则放置的是“整个class”感兴趣的数据。C++对象模型则把static data members,则放置在程序的一个global data segment中,不会影响个别class object的大小。
    每一个class object因此必须有足够的大小容纳它所有的nonstatic data members。但有时候,它可能比想象的还大,原因是:
1> 由编译器自动加上的额外data members,用以支持某种语言特性(主要是各种virtual特性-虚函数、虚基类?
2> 因为alignment(字节对其调整)

一 Data member的存取

static data member

1 static data member被编译器提出class之外,视为一个global变量(但只在class声明范围之内可见)。
2 若取一个static data member的地址,会得到一个指向其数据类型的指针,而不是一个指向其class member的指针。
如:&Point3d::chunkSize;   获得的内存地址是 int *
3 每一个static data member只有一个实体,存放在程序的data segment之中,每次程序取用static member,就会被内部转化为对唯一的extern实体的直接取用操作。如:
//origin.chunkSize=250;
Point3d::chunkSize=250;
//pt->chunkSize=250;
Point3d::chunkSize=250;
注:这(静态数据成员)是c++语言中"通过一个指针和通过一个对象来存取member,结论完全相同"的唯一一种情况。
4 如果多个class,都声明了一个相同的static data member,那么当他们被放在程序的data segment时,就会导致命名冲突。编译会暗中对每一个static data member编码(即:name-mangling)以获得一个独一无二的程序识别码。

nonstatic data members

1 nonstatic data members直接存放在每一个class object之中,除非经由明确(explicit)或隐式的(implicit)class object,没有办法直接存取他们。
2 只要程序员在一个member function中直接处理一个nonstatic data member,所谓“implicit class object”(即:this指针)就会发生。
3 欲对一个nonstatic data member进行存取操作,编译器需要把class object的起始地址加上data member的偏移量。每一个nonstatic data member的偏移量offset在编译时期即可获得。(因此,存取一个nonstatic data member的效率和存取一个C struct member或一个nonderived class的member是一样的)
4 取一个nonstatic data member的地址,将会得到它在class中的偏移量(offset);取一个绑定于真正class object身上的data member的地址,将会得到该member在内存中的真正地址。

二 “继承”与data member

    在c++继承模型中,一个derived class object所表现出来的东西,是其自己的members加上其base calss members的总和。在大部分编译器中,base class members总是先出现,但属于virtual base class的除外。
    derived class object中数据成员的布局与继承、virtual function、virtual base等情况有关。下面分几种情况进行讨论:

1 无继承、无virtual function

    这种情况和C struct完全一样。如下图:
         
 

2 只要继承不要多态(单一继承、不含virtual function)

    一般而言,具体继承并不会增加空间或存取时间上的额外负担
    这样设计的好处是可以把管理x和y坐标的程序代码局部化。此外这个设计可以明显表现出两个抽象类之间的紧密关系。
    但这样设计,容易犯两类错误:
    1> 经验不足的人可能会重复设计一些相同的函数。
    2> 把一个class分解成两层或更多层,有可能会为了“表现class体系之抽象化”而膨胀所需要空间。c++语言保证“出现在derived class 中的base class subobject有其完整原样性”。举一个例子:
 
              具体类                                               分裂为三层结构
    为了保证"出现在derived class 中的base class subobject有其完整原样性",三层结构可能的布局如图所示:
    然而,如果C++语言把derived class members(也就是concrete2::bit2或concrete3::bit3)和concrete1 subobject捆绑在一起,去掉填补空间,则在如下图语义中,base class subobject的完整原样性就无法保证(但是gcc却采用的正在用方式):

3 单一继承加多态(即:含有虚函数)

    如果我们要处理一个坐标点,而不打算在乎它是一个point2d或point3d实例(也就是我们企图以多态的方式处理2d或3d坐标点),那么我们在继承关系中提供一个virtual function接口。为了支持这样的特性,势必会给我们的Point2d class带来空间和存取时间上的额外负担:
1> 导入一个和Point2d有关的virtual table,用来存放它所声明的每一个virtual functions的地址。
2> 在每一个class object中导入一个vptr,提供执行期链接,是每一个object能找到相应的virtual table。
3> 加强版constructor,使它能够为vptr设定初值,让它指向class所对应的virtual table。
4> 加强版destructor,使它能够抹消"指向class之相关virtual table"的vptr。
    加上多态后,对于每一个对象在空间上的负担就是多了一个vptr指针的空间(通常是一个word,4byte)。然而这个vptr放在类对象的什么位置最好?有两种主流设计:一种放在前端;一种放在尾端
cfront编译器放在class object的尾端(好处:可以保证base class c struct的对象布局):
    到了c++2.0后,某些编译器(gcc 和 vc6.0都是这样的)开始把vptr放到class object的前端。(好处是与class vptr之间的offset不需要专门准备;缺点是丧失了c语言的兼容性)。
    单一继承加多态后的对象布局(vptr放在尾端的情况):
 

4 多重继承

    单一继承提供了一种自然多态。base class和derived class的objects都是从相同的地址开始的,其差异只在于derived object(可能)比较大。把一个derived class object指定给base class(不管继承深度有多深)的指针或引用,该操作并不需要编译器去调停或修改地址。它很自然的可以发生,而且提供了最佳执行效率。
    多重继承既不像单一继承,也不容塑造出其模型:
1> 多重继承的复杂度在于derived class和其第二个或后继base class之间的非自然关系
2> 多重继承的问题主要在与derived class和其第二个或后继base class objects之间的转换:
① 对一个多重派生对象,将其地址指定给“最左端base class的指针”,情况和单一继承时相同。(二者指向相同的地址,只需地址指定操作而已)。
至于第二个或后继的base class的地址指定操作,则需要将地址修改,加上(或减去)介于中间的base class subobjects大小。
实例:
Vertex3d v3d;
Vertex *pv;
Point2d *p2d;
Point3d *p3d;
1> pv=&v3d;//被转化为:
pv=(Vertex *)(((char*)&v3d)+sizeof(Point3d));
2> 而下面的指定操作:
p2d=&v3d;
p3d=&v3d;
都只需要简单拷贝其地址就行了。
3> c++标准并没有要求Vertex3d中base class Point3d和Vertex有特定的排列次序。但现在大多数编译器还是想cfront那样根据声明次序排列他们。
4> members的位置在编译时就固定了所以存取第二个(或后继)base class中的一个data member不需要额外的成本,只是一个简单的offset运算,就像单一继承一样简单-不管是经由一个指针、一个reference或是一个object来存取。

5 虚拟继承

    多重继承的一个语意上的副作用就是,它必须支持某种形式的“shared subobject继承”。在语言层面的解决办法是导入所谓的虚拟继承
    一般的实现方法如下所述:class如果内含一个或多个virtual base class subobjects,像istream那样,将被分割为两部分:一个不变局部和一个共享局部。不变局部中的数据,不管如何衍变,总是拥有固定的offset,所以这部分可以直接存取;至于共享局部,所表现的就是virtual base class subobject,这一部分的数据,其位置会因为每次的派生操作而有变化所以它们只可以间接存取
    一般的布局策略是先安排好derived class的不变部分,然后再建立其共享部分。然而如何存取class的共享不放呢?
    cfront编译器会在每一个derived class object中安插一些指针每个指针指向一个virtual base class。要存取继承得来的virtual base class members,可以使用相关指针间接完成。然而还存在如下优化:
1> 理想上我们希望class object有固定的负担。所以每一个对象不应该针对每一个virtual base class背负一个额外的指针(这样负担会导致随虚基类的数目变化而变化)。解决方案:
①Microsoft编译器:引入所谓的virtual base class stable。每一个class object如果有一个或多个virtual base classes就会有编译器安插一个指针指向virtual base class stable。至于真正的virtual base class指针被放置在virtual base class stable中。
②在virtual function table放置virtual base class的offset(而不是地址)。
2> 理想上我们希望有固定的存取时间。解决方案:
大部分编译器是经由拷贝操作取的所有的nested virtual base class指针放到derived class object之中
 
对象布局:
 
1 以指针指向base class的实现模型
2 使用virtual table offset strategy所产生的数据布局