《深度探索C++对象模型》读书笔记(3)


《深度探索C++对象模型》读书笔记(3)
2010年12月20日
  巨盾补补,今天你“补”了吗? 巨盾补补:是一款针对个人用户而设计的绿色漏洞补丁软件,集绿色、小巧、全面、精确于一身,是您升级系统漏洞的不二选择。 1、绿色、小巧:下载完双击即可运行,使用完即可完全删除。 2、全面:完全支持Windows XP和Windows 7 系统。 3、精确,采用的是和Windows Update相同的判定机制,必须补丁一个不漏。 4、迅捷,连接到微软官方服务器进行下载,保证下载速度及安全性。 官方下载地址:http://update.ggsafe.com/hotfix/GGBuBu.zip 官方反馈地址:http://bbs.ggsafe.com/thread-36769-1-1.html
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  在visual C++ 6.0中测试如下代码:  #include "iostream"
  using namespace std;
  class X {};
  class Y : public virtual X {};
  class Z : public virtual X {};
  class A : public Y,public Z {};
  int main()
  {
  coutchunkSize = 250;
  Point3d::chunkSize = 250;  (b)若取一个static data member的地址,会得到一个指向其数据类型的指针,而不是一个指向其class member的指针,因为static member并不内含在一个class object之中。
  &Point3d::chunkSize会获得类型如下的内存地址:const int*
  (c)如果有两个classes,每一个都声明了一个static member freeList,那么编译器会采用name-mangling对每一个static data member编码,以获得一个独一无二的程序识别代码。
  (2)Nonstatic Data Members以两种方法存取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***
  (1)只要继承不要多态(Inheritance without Polymorphism)
  让我们从一个具体的class开始:  class Concrete{
  public:
  // ...
  private:
  int val;
  char c1;
  char c2;
  char c3;
  };  每一个Concrete class object的大小都是8 bytes,细分如下:(a)val占用4 bytes;(b)c1、c2、c3各占用1 byte;(c)alignment需要1 byte.
  现在假设,经过某些分析之后,我们决定采用一个更逻辑的表达方式,把Concrete分裂为三层结构:  class Concrete {
  private:
  int val;
  char bit1;
  };
  class Concrete2 : public Concrete1 {
  private:
  char bit2;
  };
  class Concrete3 : public Concrete2 {
  private:
  char bit3;
  };  现在Concrete3 object的大小为16 bytes,细分如下:(a)Concrete1内含两个members:val和bit1,加起来是5 bytes,再填补3 bytes,故一个Concrete1 object实际用掉8 bytes;(b)需要注意的是,Concrete2的bit2实际上是被放在填补空间之后的,于是一个Concrete2 object的大小变成12 bytes;(c)依次类推,一个Concrete3 object的大小为16 bytes.
  为什么不采用那样的布局(int占用4 bytes,bit1、bit2、bit3各占用1 byte,填补1 byte)?
  下面举一个简单的例子:  Concrete2 *pc2;
  Concrete1 *pc1_1, *pc1_2;
  pc1_1 = pc2; // 令pc1_1指向Concrete2对象
  // derived class subobject被覆盖掉
  // 于是其bit2 member现在有了一个并非预期的数值
  *pc1_2 = *pc1_1;  pc1_1实际指向一个Concrete2 object,而复制内容限定在其Concrete subobject,如果将derived class members和Concrete1 subobject捆绑在一起,去除填补空间,上述语意就无法保留了。在pc1_1将其Concrete1 subobject的内容复制给pc1_2时,同时将其bit2的值也复制给了pc1_1.  (2)加上多态(Adding Polymorphism)
  为了以多态的方式处理2d或3d坐标点,我们需要在继承关系中提供virtual function接口。改动过的class声明如下:  class Point2d {
  public:
  Point2d(float x = 0.0, float y = 0.0) : _x(x),_y(y) {};
  virtual float z() ...{ return 0.0; } // 2d坐标点的z为0.0是合理的
  virtual void operator+=(const Point2d& rhs) {
  _x += rhs.x();
  _y += rhs.y();
  }
  protected:
  float _x,_y;
  };  virtual function给Point2d带来的额外负担:
  (a)导入一个和Point2d有关的virtual table,用来存放它声明的每一个virtual function的地址;
  (b)在每一个class object中导入一个vptr;(c)加强constructor和destructor,使它们能设置和抹消vptr.  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+=(const Point2d& rhs) { //注意参数是Point2d&,而非Point3d&
  Point2d::operator+=(rhs);
  _z += rhs.z();
  }
  protected:
  float _z;
  };  自此,你就可以把operator+=运用到一个Point3d对象和一个Point2d对象身上了。
  (3)多重继承(Multiple Inheritance)
  请看以下的多重继承关系:  class Point2d {
  public:
  // ... // 拥有virtual接口
  protected:
  float _x,_y;
  };
  class Point3d : public Point2d {
  public:
  // ...
  protected:
  float _z;
  };
  class Vertex {
  public:
  // ... // 拥有virtual接口
  protected:
  Vertex *next;
  };
  class Vertex3d : public Point3d,public Vertex {
  public:
  // ...
  protected:
  float mumble;
  };  对一个多重继承对象,将其地址指定给"第一个base class的指针",情况将和单一继承时相同,因为二者都指向相同的起始地址,需付出的成本只有地址的指定操作而已。至于第二个或后继的base class的地址指定操作,则需要将地址修改过,加上(或减去,如果downcast的话)介于中间的base class subobjects的大小。  Vertex3d v3d;
  Vertex3d *pv3d;
  Vertex *pv;
  pv = &v3d;
  // 上一行需要内部转化为
  pv = (Vertex*)((char*)&v3d) + sizeof(Point3d));
  pv = pv3d;
  // 上一行需要内部转化为
  pv = pv3d ? (Vertex*)((char*)pv3d) + sizeof(Point3d)) : 0; // 防止可能的0值  (4)虚拟继承(Virtual Inheritance)
  class如果内含一个或多个virtual base class subobject,将被分隔为两部分:一个不变局部和一个共享局部。不变局部中的数据,不管后继如何衍化,总是拥有固定的offset,所以这一部分数据可以被直接存取。至于共享局部,所表现的就是virtual base class subobject.这一部分的数据,其位置会因为每次的派生操作而变化,所以它们只可以被间接存取。
  以下均以下述程序片段为例:  void Point3d::operator+=(const Point3d& rhs)
  {
  _x += rhs._x;
  _y += rhs._y;
  _z += rhs._z;
  }  间接存取主要有以下三种主流策略:
  (a)在每一个derived class object中安插一些指针,每个指针指向一个virtual base class.要存取继承得来的virtual base class members,可以使用相关指针间接完成。
  由于虚拟继承串链得加长,导致间接存取层次的增加。然而理想上我们希望有固定的存取时间,不因为虚拟衍化的深度而改变。具体的做法是经由拷贝操作取得所有的nested virtual base class指针,放到derived class object之中。  // 在该策略下,这个程序片段会被转换为
  void Point3d::operator+=(const Point3d& rhs)
  {
  _vbcPoint2d->_x += rhs._vbcPoint2d->_x;
  _vbcPoint2d->_y += rhs._vbcPoint2d->_y;
  _z += rhs._z;
  }  (b)在(a)的基础上,为了解决每一个对象必须针对每一个virtual base class背负一个额外的指针的问题,Micorsoft编译器引入所谓的virtual base class table.
  每一个class object如果有一个或多个virtual base classes,就会由编译器安插一个指针,指向virtual base class table.这样一来,就可以保证class object有固定的负担,不因为其virtual base classes的数目而有所变化。
  (c)在(a)的基础上,同样为了解决(b)中面临的问题,Foundation项目采取的做法是在virtual function table中放置virtual base class的offset.
  新近的Sun编译器采取这样的索引方法,若为正值,就索引到virtual functions,若为负值,则索引到virtual base class offsets.  // 在该策略下,这个程序片段会被转换为
  void Point3d::operator+=(const Point3d& rhs)
  {
  (this + _vptr_Point3d[-1])->_x += (&rhs + rhs._vptr_Point3d[-1])->_x;
  (this + _vptr_Point3d[-1])->_y += (&rhs + rhs._vptr_Point3d[-1])->_y;
  _z += rhs._z;
  }  小结:一般而言,virtual base class最有效的一种运用方式就是:一个抽象的virtual base class,没有任何data members.
  ***对象成员的效率***
  如果没有把优化开关打开就很难猜测一个程序的效率表现,因为程序代码潜在性地受到专家所谓的与特定编译器有关的奇行怪癖。由于members被连续储存于derived class object中,并且其offset在编译时期就已知了,故单一继承不会影响效率。对于多重继承,这一点应该也是相同的。虚拟继承的效率令人失望。  ***指向Data Members的指针***
  如果你去取class中某个data member的地址时,得到的都是data member在class object中的实际偏移量加1.为什么要这么做呢?主要是为了区分一个"没有指向任何data member"的指针和一个指向"的第一个data member"的指针。
  考虑这样的例子:  float Point3d::*p1 = 0;
  float Point3d::*p2 = &Point3d::x;
  // Point3d::*的意思是:"指向Point3d data member"的指针类型
  if( p1 == p2) {
  cout*dmp;
  }
  void func2(Derived *pd)
  {
  // bmp将成为1
  int Base2::*bmp = &Base2::val2;
  // bmp == 1
  // 但是在Derived中,val2 == 5
  func1(bmp,pd);
  }  也就是说pd->*dmp将存取到Base1::val1,为解决这个问题,当bmp被作为func1()的第一个参数时,它的值必须因介入的Base1 class的大小而调整:  // 内部转换,防止bmp == 0
  func1(bmp ? bmp + sizeof(Base1) : 0, pd);  系列文章:
  《深度探索C++对象模型》读书笔记(1)
  《深度探索C++对象模型》读书笔记(2)
  《深度探索C++对象模型》读书笔记(4)
  《深度探索C++对象模型》读书笔记(5)
  《深度探索C++对象模型》读书笔记(6)
  《深度探索C++对象模型》读书笔记(7)
  《深度探索C++对象模型》读书笔记 最后一记

猜你喜欢

转载自qnf782rm.iteye.com/blog/1361524