Data语意学
...引子:
C++对象模型尽量以空间优化和存取速度考虑来表现nonstatic data members,并且和C语言struct保持数据配置的兼容性,它把nonstatic data members 直接存放在每一个class object之中,对于继承而来的nonstatic data members(无论是virtual或是nonvirtual base class)也是如此,不过并没有强制定义其空间排列顺序,至于static data members,则被放置在程序的一个global data segment中,不会影响个别的objects的大小,static data members永远只存在一份实体(对于template class的static data members情况稍有不同).
每一个class object 必须有足够的大小,有时会很大,因为:
1)由编译器自动加上的额外data members,用以支持某些语言特性(主要是各种virtual 特性).
2)因为alignment的需要.
3.1 Data Member的绑定:
...一个inline函数实体,在整个class声明未被完全看见之前是不会被评估求值的,如果一个inline函数在class声明之后立刻被定义的话,那就还是会对其评估求值的.
但是,对于member function的argument list并不为真,argument list中的名称还是会在它们第一次遭遇时被适当地决议完成,所以extern 和nested type names之间的非直觉绑定还是会发生,因此,应该始终把"nest type 声明"放在class的起始处.
3.2 Data Member的布局:
...C++ Standard规定,在同一个access section(如public,private...)中,members的排列只需符合"较晚出现的members在class object之中有较高的地址即可",也就是说各个members不一定连续排列,在这之间可能有alignment填补的一些bytes,同时编译器可能会合成一些内部使用的data members,以支持整个对象模型(如vptr).
3.3 Data Member存取
1)static data members
static data members被编译器提出与class之外,并视为一个global变量,每一个member的存取许可,以及class的关联并不会导致任何空间是或是执行时间上的额外负担-无论是在个别的class objects或是static data member本身,这是通过一个指针和通过一个对象来存取member,结论完全相同的唯一情况.
为了解决static data member的命名冲突问题,一般采取name-mangling手法.
2)nonstatic data members
nonstatic data members直接存放在每一个class object之中,除非经由明确的(explicit)或是暗喻的(implicit)class object,没有办法直接存取它们.
就存取效率来说,因为对于每一个nonstatic data member 的偏移量,在编译时期即可获知,甚至如果是一个base class subobject(派生自单一或是多重继承串连)也是一样,因此存取一个nonstatic data member和存取一个C struct member在效率上没有差别,但是对于虚拟继承,如果存取的变量正是从一个virtual base class中继承而来的member时,其效率就会差一些.
3.4 "继承"与data member
...在C++继承模型中,一个derived class object所表现出来的东西,是其自己的members加上其base classes members的总和,至于derived class members和base classes members的排列顺序在C++ Standard并未强制规定,对于大部分编译器来说,base class membes总是先出现,但是属于virtual base class的除外.
1)单一继承并且不含有多态:
一般而言,单一无多态的继承并不会增加空间或是存取时间上的额外负担,但是把一个class分解为两层或是多层,有可能会为了"表现class体系之抽象化"而膨胀所需的空间,C++语言保证"出现在derived class 中的base class subobject保持其完整原样性",所以,有可能因为alignment填补的空间在derived class保持其原来的大小,而导致derived class的空间膨胀,这就会导致一些非预期的结果发生.
2)加上多态:
加上多态以后带来的空间和时间上的负担:
1)导入一个virtual table,用来存放它所声明的每一个 virtual functions的地址,这个table的元素数目一般是被声明的virtual functions的数目在加上一两个slots(用以支持runtime type identification).
2)在每一个class object中导入一个vptr,提供执行期的链接,使每一个object能够找到相应的virtual table.
3)加强constructor使它能够为vptr设定初值,让它指向class所对应的virtual table.
4)加强destructor,使它能够摸消"指向clas之相关virtual table"的vptr,vptr很可能已经在derived class destructor 中被设定为derived class的virtual table的地址.
...对于vptr的位置:在C++2.0以后由于虚拟继承和抽象基类的出现使得vptr,一般会被放置在class的起始处.
3)多重继承:
...对于多重继承其复杂度在于derived class和其上一个乃至于上上一个base class......之间的"非自然"关系.
...对一个多重派生的对象,将其地址指定给"最左端(也就是第一个)base class",情况和单一继承时相同,因为二者都指向相同的起始地址,至于第二个和后继的base class的地址指定操作,则需要将地址进行一下修改,加上或是减去介于中间的base class subobjects大小.
...对于存取第二个(或后继)base class中的一个data memeber的效率问题,由于members的位置在编译时就固定利率,因此存取members只是一个简单的offset运算,就像单一继承一样简单,不管是经由一个指针,reference,或是一个object来存取.
4)虚拟继承:
...一般虚拟继承的实现方法是,如果class内涵一个或是多个virtual base class subobject,将被分割为两个部分:一个不变局部和一个共享局部.
不变局部中的数据,不管后继如何衍化,总是拥有固定的offset,所以这一部分数据可以被直接存取,至于共享局部,所表现的就是virtual base class subobject.这一部分的数据,其位置会因为每次的派生操作而有变化,所以它们只能间接存取,各个编译器的差异就在于间接存取的方法不同.
一般的布局策略是先安排好derived class的不变部分,然后在建立其共享部分.
在这中间如何存取class的共享部分是问题的关键,对于virtual base class的模型的实现有两种方法,一种是Microsoft 编译器的做法,就是引入所谓的virtual base class table,每一个class object如果有一个或是多个virtual base classes,就会有编译器安插一个指针,指向virtual base class table,真正的virtual base class指针,就被放在该表格当中,还有一种就是在virtual function table中放置virtual base class的offset(不是地址).
注:一般而言,一个抽象的virtual base class没有任何data members.
3.5 指data members的指针:
...取一个类的nonstatic data member的地址,得到的将是该数据成员在类中的偏移量,然而,实际得到值是该数据的偏移地址加一,这个问题在于,为了区分一个"没有指向任何data member"的指针和一个指向"第一个data member"的指针,每一个真正的member offset的值都被加一(在vc++ 2008中,没有加一).
...取一个"nonstatic data member"的地址将会得到它在class中的offset,取一个"绑定与真正class object身上的data member"的地址,将会得到该member在内存中的真正地址.