• 深度探索C++对象模型之第三章:数据语义学



     如下三个类:

    class X  { };
    class Y :public virtual X { };
    class Z : public virtual X {};
    class A :public Y,public Z {};

    一、编译器优化之前的大小:

    上述四个类在优化之前的大小分别是:1、8、8 、12

     类X明明没有任何成员为什么大小是1byte呢?因为那是编译器插入的一个char,这使得这一class的两个object在内存中有独一无二的地址。

     Y和Z的大小都是8,这受到了机器和编译器共同的影响。即以下三个因素:

    • 语言本身造成的负担(overhead)  当C++支持virtual base classes时,就会导致一些额外负担。在derived class中,这个负担会反映成一个指针,它要么指向virtual base class subobject或者指向一个相关table:表格中存放的不是virtual base class subobject的地址,就是其偏移位置。
    • 编译器对特殊情况所提供的优化处理。virtual base class X subobject的1bytes大小也出现在Y和Z上。传统上它们被放在derived class的固定部分的尾端。
    • Alignment的限制(将数值调整到某数的整数倍)

    以下是X Y Z A的对象布局:

      

    来看下类A的大小:一个virtual base class subobject只会在derived class存在一份实例,不管它在class继承体系中出现了多少次:

      • 被大家共享的一个X实例,大小是1byte
      • Base class Y的大小,是4Bytes。Base class Z也是一样。
      • class A自己的大小是:0 bytes
      • alignment 将9bytes调整到12bytes

    每个类的大小会增加,由以下两个因素决定:

    • 编译器自动加上额外的data members,用以支持某些语言特性(主要是virtual特性)
    • alignment(边界调整)的需要

    二、编译器优化之后的大小:

       对于Empty virtual base class (X ),它没有定义任何数据。某些编译器对它提供了特殊处理,处理之后,empty virtual base class变成了derived class object最开头的一部分(既然有了members,就不需要原本为了empyt安插的一个char).

    如果编译器进行优化处理(将empty virtual base class X)的那1byte拿掉,类A只剩下8bytes。

      所以优化后的模型大小分别是:1/4/4/8


    三、类的data member;

      一个class的data members,可以表现这个class在执行程序时的某种状态。Nonstatic data member放置的是“个别class object”感兴趣的数据,static data members则是放置的整个“class”感兴趣的事情。

      C++将nonstatic data members数据直接放在每一个class object中。对于继承而来的nonstatic data members也是如此,但是并没强制定义其间的排序顺序。而static data member永远只存在一份实例(即使class没有任何object实例),它被存放于程序的一个global data segment中。但是一个template class的static data members行为稍有不同。


    四、data member的绑定:  

      我们知道编译器先对整个类的声明进行编译,在进行名字查找时,总是先查找类内的名字,所以当类外和类内使用相同名字的变量时,总是会屏蔽掉类外的那一个,如果我们真的想使用类外的那一个,就使用作用域运算符::。

      但是这种情况对于argument list并不为真, 对于这种情况,应该将nested type声明放置于class的起始处。


    五、data member的布局:

     

    如上所述的一个类,其Nonstatic data members在class object中的排列顺序将和其声明的顺序一样。任何static data member都不会放入对象布局之中,它们被放在data segment中。members的排列在class object中有较高的地址(C++ Standard)各个members之间并不一定需要连续,比如members的alignment也会填补一些bytes,所以C++标准对数据成员的布局还是相对开放的。

      除了上述members以外,编译器还会合成一些members比如vptr,vptr的位置也是随意,不过最好还是放在开头或结尾部分、 

      另外access sections(public/private/protected)的多寡,不会影响class object的大小和组成(暂时认为是同一个access section的多少:比如private)

     


     六、Data的存取

      

    1 Point3d origin;
    2 Point3d origin , *pt = &origin

      对于static data member,它被视为一个global变量(但是只在class的生命范围之内可见)。classd的存取、关联都不会影响static data member。

    并且static data member有且只有一个实例,存放在data segment中。

       比如如下操作:

    1 origin.chunkSize = 250;
    2 pt->chunkSize =250;

      C++通过指针和一个对象来存取member,结论是一样的。其实member并不在class object之中,因此存取static member并不需要class object.即使chunkSize是经过复杂关系继承而来,那也不会影响,static members还是只有一个实例,且存取路径仍然是那么直接。

      若取一个static data member的地址,会得到一个指向其数据类型的指针,而不是一个指向其class member的指针,因为static member并不包含在class object之中。

    1 &Point3d::chunkSize;
    2 //会得到如下结果:
    3 const int*

     如果两个class中都声明了static data member,放心啦~编译器会自动对它们进行编码的,咱们不用操心。

    对于Nonstatic data members,它们直接存放在每个class object中,除非经过explicit或implicit的class object,否则没有办法直接存取它们。只有程序员在member function中直接处理一个nonstatic data member,那么implicit class object就会发生。如下所示:

      

     1 Point3d Point3d::translate(const Point3d*pt){
     2  x+= pt.x;
     3  y+= pt.y;
     4  z+= py.z;
     5 };
     6 
     7 //表面看到的对x/y/z的直接存取,实际上是通过implicit class object (this指针表达)完成的
     8 //成员函数的内部转换如下:
     9 Point3d Point3d::translate(Point3d *const this,const Point3d &pt){
    10  this->x += pt.x;
    11  this->y += pt.y;
    12  this->z += pt.z;
    13 
    14 };

      若是对一个nonstatic data member进行存取操作,则是通过class object的起始地址加上data member的偏移位置(offset).

      

    1 origin._y = 0.0;
    2 //则地址&origin._y是如下:
    3 &origin + (&Point3d::_y - 1);

     上述-1操作是指向data member的指针,其offset总是被加上1,用于区分指向data member和class的第一个data member的情况。每个nonstatic data members的偏移位置在编译时期就可以知道。

    1 origin._x = 0.0;
    2 pt-> _x = 0.0;

       上述代码的区别在于当Point3d是一个derived class,且其继承结构中有一个virtual base class,并且被存取的member是从virtual base class继承而来的,那么此时我们就不能说pt具体是指向的哪一种类型,所以我们也就不知道这个offset的真正位置,这个存取操作必须必须延迟到执行时期,经过一个额外的间接导引,才能实现。但是如果使用第一种方式,其类型无疑是Point3d,即使它继承自virtual base class,所以_x的offset在编译时期也就能够确定下来。

     


     七、继承的数据成员布局:

       对于derived class和base class的排列顺序,一般编译器总是让base class先出现,但是属于virtual base class的除外。任何一条通则遇到virtual base class都会失效.

     1 class Point2d{
     2 public:
     3 
     4 private:
     5  float x,y;
     6 };
     7 
     8 class Point3d{
     9 public:
    10 
    11 private:
    12  float x,y,z;
    13 };

    1.无virtual function的对象布局图:

       

    2.单一继承无虚函数:

     

    3.单一继承有虚函数:

     当加入虚函数之后,势必会对Point2d class带来空间和存取时间上的额外负担:

    • 导入一个和Point2d有关的virtual table,用于存放每个virtual function的地址。table的元素数 =虚函数个数 + 一个或两个slots(用于RTTI)
    • 在每个class object中,导入一个vptr,用于提供执行期的链接,使每个object都能够找到相应的virtual table.
    • 增强constructor,使它能够为vptr设定初值,让它指向class所对应的virtual table。
    • 增强destructor,使它能够抹消vptr。

     

    4.多重继承:

     例如如下多重继承机制:

     

      

     多重继承的问题主要发生在derived class objects和其第二或后继的base class objects之间的转换:对一个多重派生对象,将其地址指定给最左端(第一个)base class的指针,情况和单一继承相同,因为二者具有相同的起始地址。至于第二个或后继的base class则需要,对地址进行修改。

    1 Vertex3d v3d;
    2 Vertex *pv;
    3 Point2d *p2d;
    4 Pint3d *p3d;
    5 
    6 pv = &v3d; //这是将一个派生类赋给第二个base class
    7 
    8 //则需要这样的内部转换
    9 pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d));//要找到v3d显示转换为Vertex大小,然后再加上(Point3d)的地址长度。

    4.虚拟继承:

      

      要找到一个办法将istream和ostream各自维护的一个ios subobject,折叠成一个由iostream维护的单一 ios subobject,并且可以保存base class 和derived class的指针(以及reference)之间的多态指定操作。

       方法如下:

      如果类中含有一个或多个virtual base class subobjects,像istream一样,那么这个class将被分割为两部分:一个不变区域和一个共享区域。

    • 不变区域中的数据不管后继如何衍化,总有固定的offset(从object的开头算起),所以这一部分数据可以被直接存取。
    • 共享区域中的数据(virtual base class subobject)其位置会因为每次的派生操作而有所变化,所以它们只能被间接存取。

    其中不同编译器之间的差距就在于,间接存取的方式,例如如下继承体系:

     

     一般布局策略是先安排好derived class中的派上部分,然后再建立其共享部分。如何存取class的共享部分呢?cfront编译器会在每一个derived class object中安插一些指针,每个指针指向virtual base class。要存取继承得来的virtual base class members,通过相关指针间接完成

      例如如下代码:

      

     1 void Point3d:: operator +=(const Point3d &rhs){
     2  _x += rhs._x;
     3  _y += rhs._y;
     4  _z += rhs._z;
     5 };
     6 
     7 //在cfront的策略之下,运算符会被内部转换为:
     8 
     9 _vbcPoint2d->_x +=rhs._vbcPoint2d->_x; //_vpcPoint2d是那个derived class的指向virtual base class 
    10 _vbcPoint2d->_y  +=rhs._vbcPoint2d->_y;
    11 _z += rhs._z; 

     同时derived class到base class的之间的转换:

    1 Point2d *p2d = pv3d;
    2 //在c++模型之下变成
    3 Point2d *p2d = pv3d ? pv3d->_vbcPoint2d : 0

       上述模型缺点:

    • 每个对象必须对其每个virtual base class背负一个额外的指针,导致其负担因virtual base classes的个数而变化。
    • 若虚拟继承串链加长,导致间接存取层次增加,如果我有三层虚拟派生,我就需要三次间接存取(经过三个virtual base class的指针),但是我希望的是有固定的存取时间。

    解决第二个问题:由拷贝操作取得所有的nested virtual base class的指针,放到derived class object中,虽然空间上付出了一些代价,但是解决了固定存取时间的问题:如下所示:

      

    解决第一个问题:引入所谓的virtual base class table.每个class object如果有一个或多个virtual base classes,就会由编译器安插一个指针,指向virtual base class table,这里面放的是真正的virtual base class 指针。

    通常,virtual base class最有效的运用方式是:一个抽象的Virtual base class,没有任何data members.


    八、对象成员的效率

     1.封装和inline测试:

    • 如果把编译器的优化打开,“封装和inline”就不会带来执行期的效率成本。
    • 如果没有把优化开关打开,就很难猜测到一个程序的效率表现。

      2.继承测试:

    • 单一继承并不会影响效率,因为members被连续存储在derived class object中,并且它的offset在编译时期就已经知道了。这个结果在多重继承下应该也是一样的。
    • 虚拟继承的效率令人失望,间接存取操作压抑了“把所有运算都移往寄存器的优化能力”,但是间接性并不会严重影响非优化程序的执行效率。

     九、指向数据成员的指针

      如下述代码:

    1 class Point3d{
    2 public:
    3    virtual ~Point3d();
    4 protected:
    5     static Point3d origin;
    6     float x,y,z;
    9 };

     static成员放在class object之外,x/y/z按照声明顺序进行排列。vptr不是放在开头就是结尾。

    先假设将vptr放在尾端:则x y z在对象布局中offset分别是0 4 8,但是传回的是1 5 9 ?问题在于如何区分一个“没有指向任何data member的指针”和一个指向“第一个data member”的指针:

      考虑如下例子:

    float Point3d::*p1 = 0;
    float Point3d::*p2 = &Point3d::x;
    //Point3d::*的意思是指向“Point3d data member”的指针类型

      很明显p1是一个没有指向任何data member的指针,而p2是指向第一个data member的指针。

    为了区分p1和p2,每一个真正的data member offset的值都应该被加上1.因此当我们在真正使用该值以指出一个member之前,请先减掉1.

    接下里看一下,取一个nonstatic data member的地址和取一个绑定到真正class object上的data member的地址,有什么不同?

    & Point3d::z;
    & origin.z;

    第一种将会得到它在class中的offset,即float Point3d::*,而第二行将会得到该member在内存中的真正地址float*。


    十、指向members的指针的效率问题:

      为每个成员存取操作加上一层间接性,会使得执行时间多出一倍不止。以指向member的指针来存取数据,再一次几乎用掉了双倍时间。而优化使得三种存取策略表现一致。

      由于被继承的data members是直接存放在在class object中的,所以继承的引入并没有影响这些代码的效率。

    陈小洁的三只猫
  • 相关阅读:
    asp.net自己创建的app_code文件夹中的类不能访问的解决办法
    html+css实现选项卡功能 【转】
    jquery实现文本框数量加减功能的例子分享
    css3中的background
    通过CSS3 实现响应式Web设计的三个步骤
    鼠标移动到图片透明度改变
    jQuery之简单的表单验证【转】
    手机web——自适应网页设计(html/css控制)【转】
    css3 media媒体查询器用法总结
    把人团结在一起的力量
  • 原文地址:https://www.cnblogs.com/ccpang/p/11368573.html
Copyright © 2020-2023  润新知