引例:
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所产生的数据布局