读《C++对象模型》- Data语意学

一、Data Member的存取

static data members,会被编译器提出于class之外,并被视为一个global变量(但只在class生命范围之内可见)。每一个member的存取许可(private, protected, public),以及与class的关联,并不会导致任何空间上或执行时间上的额外负担——不论是在个别的class objects或是在static data member本身。每一个static data member只有一个实体,存放在程序的data segment之中。

nonstatic data members直接存放在每一个class object之中。每一个nonstatic data member的偏移量(offset)在编译时期即可获知,甚至如果member属于一个base class subobject(派生自单一或多重继承串链)也是一样。因此,存取一个nonstatic data member,其效率和存取一个C struct member或一个nonderived class的member是一样的。

但是使用继承和指针还是会带一些差异的,如下代码:

// Point3d是一个derived class 其定义就忽略哈
Point3d origin, *pt = &origin;
origin.x = 0.0;
pt->x = 0.0

从上面代码中,“从origin存取”和“从pt存取”有什么重大的差异?答案是“当Point3d是一个derived class,而在其继承结构中有一个virtual base class,并且被存取的member(如上例中的x)是一个从该virtual base class继承而来的member时,就会有重大的差异“。这时候不能够说pt必然指向哪一种class type(因此也就不知道编译时期这个member真正的offset位置),所以这个存取操作必然延迟至执行期,经由一个额外的间接导引,才能够解决。但如果使用origin,就不会有这个问题,其类型无疑是Point3d Class,而即使它继承自virtual base class,members的offset位置也在编译时期就固定了。

二、”继承“与Data Member

在继承体系中,关于derived class members和base class(es) members的内存布局中的排列次序并未在C++ Standard中强制指定;理论上编译器可以自由安排之。在大部分编译器上头,base class members总是先出现,但属于virtual base class除外(一般而言,任何一条规则一旦遇上virtual base class基本就难成立的了)。

下面将分别讨论“单一继承且不含virtual function”、“单一继承并含virtual functions”、“多重继承”、“虚拟继承”等四种情况的继承模型。

1.单一继承且不含多态

singleinherit

单一继承而且没有多态时的数据布局

这里有一个易犯了的错误,把一个class分解为两层或更多层,有可能会为了“表现class体系之抽象化”而膨胀所需的空间。例子如下:

class Concrete {
public:
    //...
private:
    int val;
    char c1;
    char c2;
    char c3;
};

在一部32位机器中,每一个Concrete class object的大小都是8 bytes,细分为(1)val占用4 bytes;(2)c1,c2,c3各占用1 bytes;(3)alignment需要1 byte。如下图所示:

concreteobject

假如将上述的Concrete分裂为三层的继承结构表示的话:

class Concrete1 {
public:
    //...
private:
    int val;
    char c1;
};

class Concrete2 : public Concrete1 {
public:
    //...
private:
    char c2;
};

class Concrete3 : public Concrete2 {
public:
    //...
private:
    char c3;
};

经过这样分裂的设计后,Concrete3 object的大小不再是8 bytes了,而是16 bytes了。其继承结构的内存布局如下:

concrete123object

之所以出现这种情况是因为C++语言保证”出现在derived class中的base class subobject有其完整性原样性”,正是如此,继承后的Concrete3 object会因为继承串链中的父类alignment的空间而导致本身的空间膨胀.

2.加上多态

加上多态后,不可避免地在数据结构中多了一个vptr,C++ Standard没有规定vptr放在对象结构的位置。把vptr放在class object的尾端,可以保留base class C struct的对象布局,因而允许在C程序代码中也能使用。把vptr放在前端,对于“在多重继承之下,通过指向class members的指针调用virtual function”,会带来一些帮助,而代价就是丧失了C语言兼容性。

inherit&virtual3.多重继承

相对于加入多态的单一继承,多重继承在把一个derived object转换为其base类型时,需要由编译器的介入,用以调整地址。具体来说,对于一个多重继承对象,将其地址指定给“最左端(也就是第一个)base class的指针”,情况将和单一继承时相同,因为二者都指向相同的起始地址。需付出的成本只有地址的指定操作而已。至于指定给第二个或后继的base class的地址指定操作,则需要将地址修改过:加上(或减去,如果downcast的话)介于中间的base class subobject(s)大小。

4.虚拟继承

多重继承的一个语意上的副作用就是,它必须支持某种形式的”shared subobject”继承。一个典型的例子是最早的iostream library:

class ios { ... };
class istream : public ios { ... };
class ostream : public ios { ... };
class iostream : public istream, public ostream { ... };

这样在iostream中得维护两份ios的subobject,而解决这个问题是使用虚拟继承:

class ios { ... };
class istream : public virtual ios { ... };
class ostream : public virtual ios { ... };
class iostream : public istream, public ostream { ... };

至于虚拟继承的一般实现方法如下所述。Class如果内含一个或多个virtual base class subobjects,像istream那样,将被分割成两部分:一个不变局部和一个共享局部。不变局部中的数据(实际上是在本类而非在base class中定义的data members),不管后继如何衍化,总是拥有固定的offset(从object的开头算起),所以这一部分数据可以被直接存取。至于共享局部,所表现的就是virtual base class subobject。这一部分的数据,其位置会因为每次的派生操作而会有所变化,所以它们只可以被间接存储(如在istream中的ios)。一般的布局策略是先安排好derived class的不变部分,然后再建立其共享部分。
有两个实现模型,第一个是在每个类中添加一个指针,用于指向共享局部的地址,即使共享局部在后继的继承中出现布局变化,但指针仍然会在编译阶段设定为正确的地址。另一个模型则是通过在virtual table上放置offset(相对于对象开头)来获取共享局部的地址。

This entry was posted in C/C++. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>