博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C++对象模型学习——Data语意学
阅读量:6935 次
发布时间:2019-06-27

本文共 25073 字,大约阅读时间需要 83 分钟。

hot3.png

     对于下面代码,sizeof的结果:

#include 
class X{ };class Y : public virtual X { };class Z : public virtual X { };class A : public Y, public Z { };int main(){ X x; Y y; Z z; A a; std::cout << "对于class X的sizeof结果为:" << sizeof( x ) << std::endl; std::cout << "对于class Y的sizeof结果为:" << sizeof( y ) << std::endl; std::cout << "对于class Z的sizeof结果为:" << sizeof( z ) << std::endl; std::cout << "对于class A的sizeof结果为:" << sizeof( a ) << std::endl;}

    结果的大小和机器还有编译器都有关。

    从class X并不是空,它有一个隐藏的1byte大小,这是编译器安插进去的一个char。这使得这

一class的两个objects得以在内存中配置独一无二的地址:   

// #include 
#include
class X{ };int main(){ X x1, x2;// std::cout << "x1的地址:" << &x1 << std::endl;// std::cout << "x2的地址:" << &x2 << std::endl; printf( "x1的地址:%x\n", (int)&x1 ); printf( "x2的地址:%x\n", (int)&x2 );}

   

        而对于Y和Z的大小受到三个因素的影响:

        1)语言本身所造成的额外负担(overhead):当语言支持virtual base classes时,就会导

致一些额外的负担。在derived class中,这个额外负担反映在某种形式的指针身上,它或者指

向virtual base class subobject,或者指向一个相关表格;表格中存放的若不是virtual base class

subject的地址,就是其偏移位(offest)。这里是4bytes。 

         2)编译器对于特殊情况所提供的优化处理:Virtual base class X subobject的1bytes大小

也出现在class Y和Z身上。传统上它被放在derived class的固定部分的尾端。某些编译器会对

empty virtual base class提供特殊支持。 

         3)Alignment的限制:class Y和Z的大小截至目前为5bytes。在大部分机器上,聚合的结

构体大小会受到alignment的限制,使它们能够更有效率地在内存中被存取。

          Empty virtual base class提供了一个virtual interface,没有定义任何数据。某些编译器对

此提供了优化处理。在这个策略之下,一个empty virtual base class被视为derived class object

最开头的一部分,也就是说它并没有花费任何的额外空间。这就节省了1bytes(因为既然有了

members,就不需要原本为了empty class而安插的一个char),也就不再需要第三点所说的

3bytes的填补。只剩下第一点所说的额外负担。

           一个virtual base class subobject只会在derived class中存在一份实例,不管它在class继

承体系中出现了多少次!class A的大小由下列几点决定:

           1)被大家共享的唯一一个class X实例,大小为1bytes。

           2)Base class Y的大小,减去“因virtual base class X而配置”的大小,结果是4bytes。

Base class Z的算法亦同。加起来8bytes。

           3)class A自己的大小:0 byte。

           4)class A的alignment数量(如果有的话)。前述三项总和,表示调整前大小是9

bytes。class A必须调整至4 bytes边界,所以需要填补3bytes,结果是12 bytes。

           而对于优化的编译器,会拿掉class X的那1byte,则3bytes也不用补齐了,则结果是

8bytes。而如果在virtual base class X中放置一个(以上)的data members,两种编译器(“有

特殊处理”者和“没有特殊处理”者)就会产生出完全相同的对象布局。

            每个class object的nonstatic data members大小可能很大,因为:

           1)由编译器自动加上的额外data members,用以支持某些语言特性(主要是各种virtual

特性)。

           2)因为alignment(边界调整)的需要。

一、Data Member的绑定(The Binding of a Data Member)

         考虑下面的代码:

// 某个foo.h头文件,从某处含入extern float x;// 程序员的Point3d.h文件class Point3d{  public:    Point3d( float, float, float );    // 问题:被传回和被设定的x是哪一个x呢?    float X() const { return x; }    void X( float new_x ) const { x = new_x; }    // ...    private:    float x, y, z;}

         是class内部那个还是外部那个x?现在的答案是内部那个。

          而在最早的编译器上,该操作会指向global x object!并因此导出早期C++的两种防御性

程序设计风格:

         1)把所有的data members放在class声明起头处,以确保正确的绑定:

class Point3d{  // 防御性程序设计风格 #1  // 在class声明起头处先放置所有data member  float x, y, z;    public:    float X() const { return x; }    // ... etc. ...};

           2)把所有的inline functions,不管大小都放在class声明之外:

class Point3d{  public:    // 防御性程序设计风格 #2    // 把所有的inlines都移到class之外    Point3d();    float X() const;    void X( float ) const;    // ... etc. ...};inline float Point3d::X() const{  return x;}// ... etc. ...

          这些古老的语言规则被称为“member rewriting rule”,大意是“一个inline函数实体,在整个

class声明未被完全看见之前,是不会被评估求值(evaluated)的”。C++ Standard以“member

scope resolution rules”来精炼这个“rewriting rule”,其效果是,如果一个inline函数在class声明

之后立刻被定义的话,那么就还是对其评估求值(evaluate)。也就是说,当一个人写下以下

这样的代码:

extern int x;class Point3d{  public:    ...    // 对于函数本体的分析将延迟,直至class声明的右大括号出现才开始    float X() const { return x; }    // ...    private:    float x;    ...};// 事实上,分析在这里进行

         然而这对于member function的argument list并不为真。

typedef int length;class Point3d{  public:    // length被决议位global      // _val被决议为Point3d::_val    void mumble( length val ){ _val = val; }    length mumble() { return _val; }    // ...  private:    // length必须在“本class对它的第一个参与操作“之前被看见    // 这样的声明将使先前的参考操作不合法    typedef float length;    length _val;    // ...};

         上面这种语言情况,仍然需要某种防御性程序风格:总把”nested type声明“放在class 的起

始处。

二、Data Member的布局(Data Member Layout)

       已知下面一组data members:

class Point3d{  public:    // ...    private:    float x;    static List
*freeList; float y; static const int chunkSize = 250; float z;};

          Nonstatic data members在class object中的排列顺序将和其被声明的顺序一样static data

members存放在程序的data segment中,和个别的class objects无关。

          C++ Standard要求在同一个access section(也就是private、public、protected等区段)

中,members的排列只需符合“较晚出现的members在class object中有较高的地址”这一条件即

可。也就是说各个members并不一定得连续排列。members的边界调整(alignment)可能就需

要填补一些bytes。

           编译器还可能会合成一些内部使用的data members,以支持整个对象模型。vptr就是这

样的东西,目前所有的编译器都把它安插在每一个“内含virtual function之class”的object内。vptr

传统上它被放在所有显示声明的members的最后。不过如今也有一些编译器把vptr放在一个

class object的最前端。C++ Standard则秉承“对于布局所持的放任态度”。

            C++ Standard也允许编译器将多个access sections之中的data members自由排列,不必

在乎它们出现在class声明中的顺序。也就是说:

#include 
#include
class Point3d{ public: // ... public: float x; static std::list
*freeList; public: float y; static const int chunkSize = 250; public: float z; };int main(){ Point3d point; std::cout << "point.x的地址:" << &point.x << std::endl; std::cout << "point.y的地址:" << &point.y << std::endl; std::cout << "point.z的地址:" << &point.z << std::endl;}

            members的排列顺序视编译器而定。

            目前各家编译器都是把一个以上的access section连锁在一起,依照声明的顺序,成为一

个连续区块。Access sections的多寡不会招来额外负担。

三、Data Member的存取

       已知下面这段程序代码:

Point3d origin;origin.x = 0.0;

        x的存取成本视x和Point3d如何声明而定。x可能是个static member,也可能是个nonstatic

member。Point3d可能是个独立(非派生)的class,也可能是从另一个单一的base class派生

而来,甚至有可能是从多重继承或虚拟继承而来的。 

        如果有两个定义,origin和pt:

Point3d origin, *pt = &origin;

       用它们来存取data members,如下:

origin.x = 0.0;pt->x = 0.0;

       会在后面讲解通过origin存取和通过pt存取的重大差异。       

         1、Static Data members

         Static data members按字面意义,被编译器提出于class之外,并被视为一个global变量

(但只在class生命范围内可见)。每一个member的存取许可(private、protected或public),

以及与class的关联,并不会招致任何空间上或执行时间上的额外负担——不论是在个别的class

object还是在static data member本身。        

          每个static data member只有一个实例,存放在程序的data segment之中。每次程序取用

static member时,就会被内部转化为对该唯一extern实例的直接参考操作。例如:

// origin.chunkSize = 250Point3d::chunkSize = 250;// pt->chunkSize = 250;Point3d::chunkSize = 250;

         从指令执行的观点来看,这是C++语言中“通过一个指针和通过一个对象来存取member,

结论完全相同”的唯一一种情况。这是因为“经由member selection operators(也就是“.”运算

符)对一个static data member进行存取操作”只是文法上的一种便宜行事而已。member其实并

不在class object之中,因此存取static members并不需要通过class object。

          即便chunkSize是一个从复杂继承关系中继承而来的member,或许它是一个“virtual base

class的virtual base class”(或其它同等复杂的继承结构)的member也说不定。程序中对于

static members还是只有唯一一个实例,而其存取路径仍然是那么直接。 

         如果static data member是经由函数调用,或其他某些语法而被存取呢?如下:

foobar().chunkSize = 250;

          cfront的做法是简单的把foobar()函数扔掉,但C++ Standard明确要求foobar()必须被求值

(evaluated),虽然其结果并无用处。下面是一种可能的优化:

// foobar().chunkSize = 250;// evaluate expression, discarding result( void ) foobar();Point3d.chunkSize = 250;

         若取一个static data member的地址,会得到一个指向其数据类型的指针,而不是一个指向

其class member的指针,因为static member并不内含一个class object之中。例如:

&Point3d::chunkSize;

          会获得类型如下的内存地址:

const int*
#include 
#include
class Point3d{ public: // ... public: float x; static std::list
*freeList; public: float y; static int chunkSize; public: float z; };int Point3d::chunkSize = 0;int main(){ Point3d point; point.chunkSize = 250; std::cout << "Point3d::chunkSize的地址:" << &Point3d::chunkSize << std::endl;}

           而对于直接在类中定义并初始化的static const member ,编译器不会给其分配地址,直

接用常量代替。如下:

#include 
#include
class Point3d{ public: // ... public: float x; static std::list
*freeList; public: float y; static const int chunkSize = 250; public: float z; };//int Point3d::chunkSize = 0;int main(){ Point3d point; // point.chunkSize = 250; std::cout << "Point3d::chunkSize的地址:" << Point3d::chunkSize << std::endl;}

     如果有两个classes,每一个都声明一个static member freeList,那么当它们都被放在程序的

data segment时,会导致名字冲突,编译器的解决办法是进行名字编码(即name-

mangling),如下代码:

#include 
class X{ public: static int free;};class Y{ public: static int free;};int X::free = 1;int Y::free = 2;int main(){ std::cout << X::free << std::endl; std::cout << Y::free << std::endl;}

        对于两个class中的static member free,转换而成的汇编代码结果如下:

.globl	_ZN1X4freeE	.data	.align 4	.type	_ZN1X4freeE, @object	.size	_ZN1X4freeE, 4_ZN1X4freeE:	.long	1	.globl	_ZN1Y4freeE	.align 4	.type	_ZN1Y4freeE, @object	.size	_ZN1Y4freeE, 4_ZN1Y4freeE:	.long	2

          任何name-mangling做法都有两个重点:

          1)一个算法,推导出独一无二的名称。

          2)万一编译系统(或环境工具)必须和使用者交谈,那些独一无二的名称可以轻易被推

导回到原来的名称。

         2、Nonstatic Data members

         Nonstatic data members直接存放在每一个class object之中。除非经由显示的(explict)

或隐式的(implict)class object,否则没有办法直接存取它们。只要程序员在一个member

function中直接处理一个nonstatic data member,所谓“implicit class object”就会发生。例如下

面这段代码:

Point3d Point3d::translate( const Point3d &pt ){  x += pt.x;  y += pt.y;  z += pt.z;}

        表面上所看到的对于x、y、z的直接存取,事实上是经由一个“implicit class object“(由this

指针表达)完成的。事实上这个函数的参数是:

// member function的内部转化Point3d Point3d::translate( Point3d *const this, const Point3d &pt ){  this->x += pt.x;  this->y += pt.y;  this->z += pt.z;}

         欲对一个nonstatic data member进行存取操作,编译器需要把class object的起始地址加上

data member的偏移位置(offset)。例如:

origin._y = 0.0;

         那么地址&origin._y将等于:

&origin + ( &Point3d::_y - 1 )

         指向data member的指针,其offset值总是被加上1,这样可以使编译器系统区分出”一个指

向data member的指针,用以指出class的第一个member“和”一个指向data member的指针,没

有指出任何member“两种情况。

         每一个nonstatic data member的偏移位置(offset)在编译时期即可获知,甚至如果

member属于一个base class subobject(派生自单一或多重继承串链)也是一样的。因此,存

取一个nonstatic data member,其效率和存取一个C struct member或一个nonderived的

member是一样的。

         虚拟继承将为”经由base class subobject存取class members“导入一层新的间接性,比

如:

Point3d *pt3d;pt3d->_x = 0.0;

         其执行效率在_x是一个struct member、一个class member=单一继承、多重继承的情况下

都完全相同。但如果_x是一个virtual base class的member,存取速度会稍微慢一点。于是回到

先前的一个问题:以两种方法存取x坐标,像这样:

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

        在C++继承模型中,一个derived class object所表现出来的东西,是其自己的members加

上其base class(es)members的总和,在大部分编译器上,base class members总是先出

现,但属于virtual base class的除外。

        1、只要继承不要多态(Inheritance without Polymorphism)

        或许程序员希望,不论2D或3D坐标点,都能够共享同一个实例,但又能够继承使用”与类

型性质相关(所谓type-specific)“的实例。我们有一个设计策略,就是从Point2d派生一个

Point3d,于是Point3d将继承x和y坐标的一切(包括数据实例和操作方法)。带来的影响则是

可以共享”数据本身“以及”数据的处理方法“,并将之局部化。一般而言,具体继承(concrete

inheritance,相对于虚拟继承virtual inheritance)并不会增加空间或存取时间上的额外负担。

class Point2d{  public:    Point2d( float x = 0.0, float y = 0.0 )             : _x( x ), _y( y ){ }    float x() { return _x; }    float y() { return _y; }    void x( float newX ) { _x = newX; }    void y( float newY ) { _y = newY; }    void operator+=( Point2d& rhs )    {      _x += rhs.x();      _y += rhs.y();    }    // ... more members  protected:    float _x, _y;};// inheritance from concrete classclass Point3d : public Point2d{  public:    Point3d( float x = 0.0, float y = 0.0, float z = 0.0 )             : Point2d( x, y ), _z( z ){ }    float z() { return _z; }    void z( float newZ ) { _z = newZ; }    void operator+=( Point3d& rhs )    {      Point2d::operator+=( rhs );      _z += rhs.z();    }    // ... more members  protected:    float _z;};

        把两个原本独立不相干的classes凑成一对”type/subtype“,并带有继承关系,会犯一些错

误。可能会重复设计一些相同操作的函数。上面例子中的constructor和operator+=为例,并没有

被做成inline函数。一般而言选择某些函数做成inline函数,是设计class时的一个重要课题。      

         第二个易犯的错误是,把一个class分解为两层或多层,有可能会为了”表现class体系之抽

象化“而膨胀空间。C++语言保证”吹按在derived class中的base class subobject有其完整原样性

“,正式重点所在。例子如下:

#include 
class Concrete{ public: // ... public: int val; char c1; char c2; char c3;};class Concrete1{ public: // ... public: int val; char bit1;};class Concrete2 : public Concrete1{ public: // ... public: char bit2;};class Concrete3 : public Concrete2{ public: // ... public: char bit3;};int main(){ Concrete object; Concrete1 object1; Concrete2 object2; Concrete3 object3; std::cout << "Concrete类的大小:" << sizeof( object ) << std::endl; std::cout << "Concrete1类的大小:" << sizeof( object1 ) << std::endl; std::cout << "Concrete2类的大小:" << sizeof( object2 ) << std::endl; std::cout << "Concrete3类的大小:" << sizeof( object3 ) << std::endl;}

         可以看到Concrete object的确是8bytes,而Concrete1内含两个members:val和bit1,加

起来5bytes。而一个Concrete1 object实际用掉8bytes,包括补用的3bytes。一般而言,边界调

整(alignment)是由处理器(processor)来决定的。

          虽然Concrete2只增加了一个bit2,但它却是放在填补空间所用的3bytes之后。于是大小变

为12bytes。这里Concrete3应该被编译器做了优化,不是原来的16bytes,而是12bytes。

        2、加上多态(Adding Polymorphism)

        如果要处理一个坐标点,而不打算在乎它是一个Point2d或Point3d实例,那么就需要在继

承关系中提供一个virtual function接口。

class Point2d{  public:    Point2d( float x = 0.0, float y = 0.0 )             : _x( x ), _y( y ){ }    float x() { return _x; }    float y() { return _y; }    void x( float newX ) { _x = newX; }    void y( float newY ) { _y = newY; }      // 加上z的保留空间    virtual float z() { return 0.0; }    virtual void z( float ) { }    virtual void operator+=( Point2d& rhs )    {      _x += rhs.x();      _y += rhs.y();    }    // ... more members  protected:    float _x, _y;};

        只有当我们企图以多态的方式(polymorphically)处理2d或3d坐标时,在设计之中导入一

个virtual才显得合理。也就是说下面代码:

void foo( Point2d &p1, Point2d &p2 ){  // ...  p1 += p2;  // ...}

        其中,p1和p2可能是2d,也可能是3d坐标点。这样的弹性,正是面向对象程序设计的中

心。代价是空间和存取时间上的额外负担:

        1)导入一个和Point2d有关的virtual table,用来存放它所声明的每一个virtual functions的

地址。这个table的元素个数一般而言是被声明的virtualfunctions的个数,再加上一个或者两个

slots(以支持runtime type identification)。

         2)在每一个class object中导入一个vptr,提供执行期的链接,使每一个object能够找到相

应的virtual table。

         3)加强constuctor,使它能够为vptr设定初值,让它指向class所对应的virtual table。这可

能意味着在derived class和每一个base class的constructor中,重新设定vptr的值。其情况视编

译器优化的积极性而定。

          4)加强destructor,使它能够抹消”指向class之相关virtual table“的vptr。

          这些额外负担带来的冲击程度视”被处理的Point2d object的个数和生命期“而定,也视”对

这些objects做多台程序设计所得的利益“而定。如果一个应用程序知道它所能使用的point

objects只限于二维坐标点或三维坐标点,则这种设计所带来的额外负担可能变得令人无法接

受。

           以下是新的Point3d声明:

class Point3d : public Point2d{  public:    Point3d( float x = 0.0, float y = 0.0, float z = 0.0 )             : Point2d( x, y ), _z( z ){ }    float z() { return _z; }    void z( float newZ ) { _z = newZ; }    void operator+=( Point2d& rhs )    {      Point2d::operator+=( rhs );      _z += rhs.z();    }    // ... more members  protected:    float _z;};

         最大的好处是可以把operator+=运用在一个Point3d对象和一个Point2d对象身上:

Point2d p2d( 2.1, 2.2 );Point3d p3d( 3.1, 3.2, 3.3 );p3d += p2d;// 得到的p3d新值将是(5.2, 5.4, 3.3)

          把vptr放在class object的尾端,可以保留base class C struct的对象布局,因而允许在C程

序代码中也能够使用。这种做法在C++最初问世时,被许多人采用。

           后来到了C++2.0开始支持虚拟继承以及抽象基类,并且由于面向对象范式(OO

paradigm)的兴起,某些编译器开始把vptr放到class object的起头处。

           把vptr放在class object的前端,对于”在多重继承之下,通过指向class members的指针调

用virtual function“,会带来一些帮助。代价就是丧失了对C语言的兼容性。

        3、多重继承(Multiple Inheritance)

         单一继承提供了一种“自然多态(natural polymorphism)”形式,是关于classes体系中的

base type和derived type之间的转换。其中base class和derived class的objects都是从相同地址

开始,其间差异只在于derived object比较大,用以多容纳它自己的nonstatic data members。

下面的指定操作:

Point3d p3d;Point2d *p = &p3d;

        把一个derived class object指定给base class(不管继承深度有多深)的指针或reference。

这个操作并不需要编译器去调停或修改地址。它很自然地可以发生,而且提供了最佳执行效

率。

         把vptr放在class object的起始处。如果base class没有virtual function而derived class有,

那么单一继承的自然多态就会被打破。这种情况下,把一个derived object转换为其base类型。

就需要编译器的介入,用以调整地址(因vptr插入之故)。在既是多重继承又是虚拟继承的情况

下,编译器的介入更有必要。

class Point2d{  public:    // virtual接口,所以Point2d对象之中会有vptr    protected:    float _x, _y;};class Point3d : public Point2d{  public:    // ...    private:    float _z;};class Vertex{  public:    // virtual接口,所以Vertex对象之中会有vptr    protected:    Vertex *next;};class Vertex3d : public Point3d, public Vertex{  public:    // ...    protected:    float mumble;};

         多重继承的问题主要发生于derived class objects和其第二或后继base class objects之间的

转换。不论是直接转换如下:

extern void mumble( const Vertex& );Vertex3d v;...// 将一个Vertex3d转换为一个Vertex。这是“不自然的”mumble( v );

         对一个多重派生对象,将其地址指定给“最左端(也就是第一个)base class的指针”,情况

将和单一继承时相同,因为二者都指向相同的起始地址。需要付出的成本只是地址的指定操作

而已。至于第二个或者后继的base class的地址指定操作,则需要将地址修改过:加上(或减

去,如果downcast的话)介于中间的base class subject(s)大小,例如:

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

          那么下面这个指定操作:

pv = &v3d;

           需要这样的内部转化:

// 虚拟C++代码pv = ( Vertex* )( ( ( char* )&v3d ) + sizeof( Point3d ) );

           而下面的指定操作:

p2d = &v3d;p3d = &v3d;

           都只需要简单地拷贝其地址就好。如下:

Vertex3d *pv3d;Vertex *pv;

           那么下面的指定操作:

pv = pv3d;

           不能够只是简单地被转换为:

// 虚拟C++代码pv = ( Vertex* )( ( ( char* )pv3d ) + sizeof( Point3d ) );

           因为如果pv3d为0,pv将获得sizeof(Point3d)的值。这是错误的!所以,对于指针,内部

转换操作需要有一个条件测试:

// 虚拟C++代码pv = pv3d ? ( Vertex* )( ( char* )pv3d ) + sizeof( Point3d ) : 0;// pv3d为0,说明指针为空

           至于reference,则不需要针对可能的0值做防卫,因为reference不可能参考到“无物“。

           C++ Standard并未要求Vertex3d中的base classes Point3d和Vertex有特定的排列顺序。

原始的cfront编译器是根据声明顺序来排列它们的。目前各编译器仍然以此方式完成多重base

classes的布局(但如果加上虚拟继承,就不一样了)。

             如果要存取第二个(或后继)base class中的一个data member,并不会付出额外的成

本,因为members的位置在编译时就固定了,因此存取members只是一个简单的offset运算。

        4、虚拟继承(Virtual Inheritance)

        多重继承的一个语意上的副作用就是,它必须支持某种形式上的”shared subobject继承“。

典型的一个例子是最早的iostream library:

// pre-standard iostream implementationclass ios { ... };class istream : public ios { ... };class ostream : public ios { ... };class iostream : public istream, public ostream { ... };

         istream和ostream都内含一个ios suboject。然而在iostream的对象布局中,我们只需要单

一一份ios suboject就好。语言层面的解决办法是导入所谓的虚拟继承:

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

        实现虚拟继承的难度在于要找到一个足够有效的方法,将istream和ostream各自维护的一

个ios suboject,折叠成为一个由iostream维护的单一ios suboject,并且还可以保存base class

和derived class的指针(以及references)之间的多态指定操作(polymorphism

assigbments)。

        一般实现方法如下。Class如果内含一个或多个virtual base class subobjects,像istream那

样,将被分割为两部分:一个不变区域和一个共享区域。不变区域中的数据,不管后继如何衍

化,总是拥有固定的offset(从object的开头算起),所以这一部分数据可以被直接存取。至于

共享区域,所表现的就是virtual base class subobject。这一部分数据,其位置会因为每次的派

生操作而有变化,所以它们只可以被间接存取。编译器实现技术的差异就在于间接存取的方法

不同。下面是3中主流策略。下面是Vertex3d虚拟继承的层次结构:

class Point2d{  public:    // ...    protected:    float _x, _y;};class Point3d : public Point2d{  public:    // ...    private:    float _z;};class Vertex{  public:    // ...    protected:    Vertex *next;};class Vertex3d : public Vertex, public Point3d{  public:    // ...    protected:    float mumble;};

        一般的布局策略是先安排好derived class的不变部分,然后再建立其共享部分。

        如何能够存取class的共享部分呢?cfront编译会在每一个derived class object中安插一些指

针,每个指针指向一个virtual base class。要存取继承得来的virtual base class members,可以

通过相关指针间接完成。举个例子:

void Point3d::operator+=( const Point3d &rhs ){  _x += rhs._x;  _y += ths._y;  _z += ths._z;};

          在cfront策略之下,这个运算符会被内部转换为:

// 虚拟C++代码_vbcPoint2d->_x += rhs._vbcPoint2d->_x; // vbc意为:virtual base class_vbcPoint2d->_y += rhs._vbcPoint2d->_y;_z += ths._z;

           而一个derived class和一个base class的实例之间的转换,像这样:

Point2d *p2d = pv3d;

           在cfront实现模型之下,会变为:

// 虚拟C++代码Point2d *p2d = pv3d ? pv3d->_vbcPoint2d : 0;

            这样的实现模型有两个主要的缺点:

          1)每一个对象必须针对每一个virtual base class背负一个额外的指针。然而理想上我们却

希望class object有固定的负担,不因为其virtual base classes的个数而有所变化。

          2)由于虚拟继承串链的加长,导致简介存取层次的增加。意思是,如果有三层虚拟派

生,就需要三次间接存取(经由三个virtual base class指针)。然而理想上我们却希望有固定的

存取时间,不因为虚拟派生的深度而改变。

          一些编译器使用cfront的原始实现模型来解决第二个问题,它们经由拷贝操作取得所有的

nested virtual base class指针,放到derived class object之中。这就解决了“固定存取时间”的问

题,虽然付出了一些空间上的代价。

          至于第一个问题,一般而言有两个解决办法。Microsoft编译器引入所谓的virtual base

class table。每一个class object如果有一个或多个virtual base classes,就会由编译器安插一个

指针,指向virtual base class table。

           第二个解决方法,也是Bjarne比较喜欢的方法,是在virtual function table中放置virtual

base class的offset(而不是地址)。这种实现方法将virtual base class offset和virtual function

entries混杂在一起。在Sun编译器中,virtual function table 可经由正负值来索引。如果是正

值,很明显就是索引到virtual functions;如果是赋值,则是索引到virtual base class offsets。在

这样的策略下,Point3d的的operator+=运算符必须被转换为以下形式:

// 虚拟C++代码( this + _vptr_Point3d[ -1 ] )->_x +=       ( &rhs + rhs._vptr_Point3d[ -1 ] )->_x;( this + _vptr_Point3d[ -1 ] )->_y +=       ( &rhs + rhs._vptr_Point3d[ -1 ] )->_y;_z += ths._z;

          虽然在此策略之下,对于继承而来的members做存取操作,成本会比较昂贵,不过成本已

经被分散至“对member的使用”上,属于局部性成本。Derived class实例和base class实例之间

的转换操作,例如:

Point2d *p2d = pv3d;

          在上述实现模型下将变成:

// 虚拟C++代码Point2d *p2d = pv3d ? pv3d->_vptr_Point3d[ -1 ] : 0;

         经由一个非多态的class object来存取一个继承而来的virtual base class的member,像这

样:

Point3d origin;...origin._x;

         可以被优化为一个直接存取操作。

         一般而言,virtual base class最有效的一种运用形式就是:一个抽象的virtual base class,

没有任何data members。

五、对象成员的效率(Object Member Efficiency)

        下面几个测试,意旨在测试聚合(aggregation)、封装(encapsulation)以及

继承(inheritance)所引发的额外负荷的程度。所有测试都是以个别局部变量的加

法、减法、赋值(assign)等操作的存取成本为依据。下面就是个别的局部变量:

float pA_x = 1.725, pA_y = 0.875, pA_z = 0.478;float pB_x = 0.315, pB_y = 0.317, pB_z = 0.838;

           每次表达式需执行1000万次,如下所示:

for( int iters = 0; iters < 10000000; iters++ ){  pB_x = pA_x - pB_z;  pB_y = pA_y + pB_x;  pB_z = pA_z + pB_y;}

          我们首先针对三个float元素所组成的局部数组进行测试:

enum fussy { x, y, z };for( int iters = 0; iters < 10000000; iters++ ){  pB[ x ] = pA[ x ] - pB[ z ];  pB[ y ] = pA[ y ] + pB[ x ];  pB[ z ] = pA[ z ] + pB[ y ];}

       未优化:        

        -O1优化:

          第二个测试是把同质的数组元素转换为一个C struct数据抽象类型,其中的成员皆为float,

成员名称是x、y、z:

for( int iters = 0; iters < 10000000; iters++ ){  pB.x = pA.x - pB.z;  pB.y = pA.y + pB.x;  pB.z = pA.z + pB.y;}

         未优化:

 

         -O1优化:

         更深一层的抽象化,是做出数据封装,并使用inline函数。坐标点现在以一个独立的

Point3d class来表示。两种不同的存取函数。第一,我定义一个inline函数,传回一个

reference,允许它出现在assignment运算符的两端:

class Point3d{  public:    Point3d( float xx = 0.0, float yy = 0.0, float zz - 0.0 )             : _x( xx ), _y( yy ), _z( zz ) { }        float& x() { return _x; }    float& y() { return _y; }    float& z() { return _z; }  private:    float _x, _y, _z;};

          那么真正对每一个坐标元素的存取操作应该像这样:

for( int iters = 0; iters < 10000000; iters++ ){  pB.x() = pA.x() - pB.z();  pB.y() = pA.y() + pB.x();  pB.z() = pA.z() + pb.y();}

      未优化:

       

      -O1优化:

 

         第二种存取函数形式是,提供一对get/set函数:

float x() { return _x; }             // get函数void x( float newX ) { _x = newX; }  // set函数

         于是对于每一个坐标值的存取操作应该像这样:

pB.x( pA.x() - pB.z() );

    未优化:

     

      -O1优化:

 

     可以看到封装带来的成本在未优化时还是挺大的,但在-O1优化后,“封装”就不会带来执行期

的效率成本。

     下一个测试中,要介绍Point抽象化的的一个三层单一继承表达法,然后再介绍Point抽象化

的一个虚拟继承表达法。这里要测试直接存取和inline存取。

      单一继承(直接存取):

    

      单一继承(inline):

       虚拟继承(双层,直接存取):

  

       虚拟继承(双层,inline):

六、指向Data Member的指针(Pointer to Data Members)

        指向data members的指针,在需要详细调查class members的底层布局时特别

有用。这样的调查可用以决定vptr是放在class的起始处或是尾端。另一个用途,可用

来决定class中的access sections的顺序。

        考虑下面的Point3d声明。其中有一个virtual function,一个static data

member,以及三个坐标值:

class Point3d{  public:    virtual ~Point3d(){ }    // ...    protected:    static Point3d origin;    float x, y, z;};

    

#include 
#include
class Point3d{ public: virtual ~Point3d(){ } // ... public: static Point3d origin; float x, y, z;};Point3d Point3d::origin;int main(){ Point3d point; Point3d point1; std::cout << "&Point3d::x = " << &Point3d::x << std::endl; std::cout << "&Point3d::y = " << &Point3d::y << std::endl; std::cout << "&Point3d::z = " << &Point3d::z << std::endl; std::cout << "&point = " << &point << std::endl; std::cout << "&point.x = " << &point.x << std::endl; std::cout << "&point.y = " << &point.y << std::endl; std::cout << "&point.z = " << &point.z << std::endl; std::cout << "&point.origin = " << &point.origin << std::endl; std::cout << "&point1.origin = " << &point1.origin << std::endl; std::cout << "&point.origin.x = " << &point.origin.x << std::endl; std::cout << "class object的大小: " << sizeof( point ) << std::endl;}

         可以从Point3d object的members位置结果看出,vptr是放在对象头的。静态对象在程序中

只有1个,而一个指向data member的指针为1。是为了区分“没有指向任何data member”的指针

和一个“指向data member”的指针,如下:

float Point3d::*p1 = 0;float Point3d::*p2 = &Point3d::x;// 如何区分if( p1 == p2 ){  // ...}

         多重继承之下,若要将第二个(或后继)base class的指针,和一个“与derived class

object绑定”的member结合起来,那么将会因为“需要加入offset值”而变得相当复杂。

#include 
struct Base1 { int val1; };struct Base2 { int val2; };struct Derived : Base1, Base2 { int val3; };int main(){ Derived derive; std::cout << "&Base1::val1 = " << &Base1::val1 << std::endl; std::cout << "&Base2::val2 = " << &Base2::val2 << std::endl; std::cout << "&Derived::val1 = " << &Derived::val1 << std::endl; std::cout << "&Derived::val2 = " << &Derived::val2 << std::endl; std::cout << "&Derived::val3 = " << &Derived::val3 << std::endl; std::cout << "&derive = " << &derive << std::endl; std::cout << "&derive.val1 = " << &derive.val1 << std::endl; std::cout << "&derive.val2 = " << &derive.val2 << std::endl; std::cout << "&derive.val3 = " << &derive.val3 << std::endl; std::cout << "sizeof( Base1 ) = " << sizeof( Base1 ) << std::endl; std::cout << "sizeof( Base2 ) = " << sizeof( Base2 ) << std::endl; std::cout << "sizeof( Derived ) = " << sizeof( Derived ) << std::endl;}

        “指向Members的指针”的效率问题

       

#include 
class Point3d{ public: Point3d( float xx = 0.0, float yy = 0.0, float zz = 0.0 ) : x( xx ), y( yy ), z( zz ) { } public: float x, y, z;};int main(){ Point3d pA( 1.725, 0.875, 0.478 ); Point3d pB( 0.315, 0.317, 0.838 ); float *ax = &pA.x; float *ay = &pA.y; float *az = &pA.z; float *bx = &pB.x; float *by = &pB.y; float *bz = &pB.z; for( int iters = 0; iters < 10000000; iters++ ) { *bx = *ax - *bz; *by = *ay + *bx; *bz = *az + *by; } return 0;}

       

#include 
class Point3d{ public: Point3d( float xx = 0.0, float yy = 0.0, float zz = 0.0 ) : x( xx ), y( yy ), z( zz ) { } public: float x, y, z;};int main(){ Point3d pA( 1.725, 0.875, 0.478 ); Point3d pB( 0.315, 0.317, 0.838 ); float Point3d::*ax = &Point3d::x; float Point3d::*ay = &Point3d::y; float Point3d::*az = &Point3d::z; float Point3d::*bx = &Point3d::x; float Point3d::*by = &Point3d::y; float Point3d::*bz = &Point3d::z; for( int iters = 0; iters < 10000000; iters++ ) { pB.*bx = pA.*ax - pB.*bz; pB.*by = pA.*ay + pB.*bx; pB.*bz = pA.*az + pB.*by; } return 0;}

转载于:https://my.oschina.net/u/2537915/blog/707400

你可能感兴趣的文章
病毒实验二
查看>>
IOS开发中的变量、方法、属性
查看>>
IT风险管理专家CISRE认证
查看>>
yii redirect
查看>>
uva-10954-贪心
查看>>
wxPython笔记
查看>>
使用 邮箱配置 激活码 用于 用户注册激活
查看>>
Recover Binary Search Tree
查看>>
Linux之因BASH造成的键盘错误和环境问题
查看>>
RBAC权限设计实例(转)
查看>>
JavaScript求当月天数
查看>>
一个典型的后台软件系统的设计复盘——(二)如何id一个事物
查看>>
springboot 详细配置2
查看>>
这么小的key-val数据库居然也支持事务——与短跑名将同名的数据库Bolt
查看>>
下拉列表框Spinner
查看>>
pageResponse - 让H5适配移动设备全家(移动端适配)1
查看>>
day2
查看>>
[WIKIOI1298]凸包周长[裸凸包]
查看>>
Android布局之weight属性解析
查看>>
OGNL中的<s:property />标签
查看>>