0%

第三章 Data语义学

  • 一个空的class如:
    1
    2
    3
    4
    5
    // sizeof X == 1
    class X { }; // sizeof X == 1
    class Y : public virtual X {}; // sizeof X == 8
    class Z : public virtual X {}; // sizeof X == 8
    class A : public Y, public Z {};// sizeof X == 12
    1
    2
    3
    4
    5
    graph BT
    Y --- X
    Z --- X
    A --- Y
    A --- Z
    事实上并不是空的,它有一个隐藏的1byte大小,那是编译器安插进去的一个char。有机器上Y、Z得出大小是8。这个值的大小和机器有关,也和编译器有关:
    1. 语言本身所造成的额外负担(overhead)。当语言支持virtual base classes时,会导致额外负担。在derived中,反映在某种形式的指针身上,它或者指向virtual base class subobject,或指向一个相关表格;表格中是virtual base class subobject地址。
    2. 编译器对于特殊情况所提供的优化处理
    3. Allgnment的限制。大部分机器结构体大小会收到alignment的限制(内存对齐),使它们能够更有效率在内存中被存取。alignment就是将数值调整到某数的整数倍
  • Empty virtual base class已经成为C++OO设计的一个特有术语了。他提供一个virtual interface,没有定义任何数据。
  • 对于class A竟然大小为12这个结果。记住,一个virual base class sbobject只会在derived class中存在一份实例,不管它在继承体系中出现多少次。它的大小由以下决定:
    1. 被大家共享的唯一一个Class X,大小为1byte
    2. Base class Y,减去因virtual base class X二配置的大小,结果是4bytes
    3. class A自己的大小:0byte
    4. class A的alignment数量
  • C++对象模型尽量以空间优化和存取速度优化的考虑来表现nonstatic data members,并且保持和C语言struct数据配置的兼容性。至于static data members,则被放置在程序的一个global data segment中,不会影响个别的class object大小。在程序之中,不管class被产生出多少个objects(经由直接产生或间接产生),static data members永远只存在一份实例。但是一个template class的static data members的行为稍有不同。
  • 每一个class object必须有足够的大小容纳所有的nonstatic data members,因为它可能比你想象的还大,原因是:
    • 编译器自动加上额外的data members,用以支持某些特性(virtual)。
    • 因为alignment的需要

3.1 Data Member的绑定(The Binding of a Data Member)

  • 早期C++有两种防御性程序设计风格:
    1. 把所有data members放在class声明处,以确保正确的绑定:
      1
      2
      3
      4
      5
      6
      7
      class Point3d
      {
      float x, y, z;
      public:
      float X() const { return x; }
      // ...etc
      };
    2. 把所有的inline functions,不管大小都放在class声明之外:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      class Point3d
      {
      public:
      Point3d();
      float X() const;
      void X(float) const;
      // ...etc
      };

      inline float Point3d::X() const
      {
      return x;
      }
      但这种设计风格在C++2.0之后就不存在了。这个古老的语言规则称为“member rwriting rule”。大概意思是“一个inline函数实体,在整个class声明未被完全看见之前,不会被评估求值(evaluated)的”。也就是说:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      extern int x;
      class Point3d
      {
      public:
      // 对于函数本体的分析延迟,直到class声明右大括号出现才开始
      float X() cons { return x; }
      private:
      float x;
      };
      // 分析在这里进行
      因此一个inline member function躯体之内的data member绑定操作,会在整个class声明完成之后才发生。但这对member function的argument list并不是真的。argument list中的名称会在第一次遇到的时候被适当resolved完成。因此extern和nested type names之间的非直觉绑定操作还是会发生。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      typedef int length;
      class Point3d
      {
      public:
      // length is resolved for global
      // _val is resolved for Point3d::_val
      void mumble(length val) { _val = val; }
      length mumble() { return _val; }
      private:
      // length必须在class对他第一个参考操作前被看见
      // 这样的声明使之前操作不合法
      typedef float length;
      length _val;
      // ...
      };
      上述的语言状况,仍然需要某种防御性程序风格:请总是把“nested type 声明”放在class的起始处。这样做才能保证非直觉绑定的正确性。

3.2 Data Member的布局(Data Member Layout)

  • 已知一组data members:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Point3d {
    public:
    // ...
    private:
    float x;
    staic List<Point3d*> *freeList;
    float y;
    static const int chunkSize 250;
    float z;
    };
    Nonsatic data members在class object中的排列顺序和其被声明的顺序一样任何中介的static data members都不会放进对象布局之中。上述例子中,每个Point3d对象是由三个float组成的。static data members存放在程序的data segment中,和个别class objects无关。
    C++ Standard要求,在同一个access section(private、public、protected等区段)中,members的排列只需要符合“较晚出现的members在class object中有较高的地址”这一条件就可以了。下面这个template funciton,接受两个data members,然后判断谁先出现在class object中。如果两个members都是不同的access sections中的第一个被声明者,函数就会判断哪个section先出现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    template<class class_type, class data_type1, class data_type2>
    char* access_order(data_type1 class_type::*mem1, data_type2 class_type::*mem2)
    {
    assert (mem1 != mem2);
    return mem1 < mem2 ? "Member 1 occurs first" : "member 2 occurs first";
    }

    // call
    access_order(&Point3d::z, &Point3d::y);

3.3 Data Member的存取

  • 已知这段代码:
    1
    2
    3
    4
    Point3d origin, *pt = &origin;
    // 存取data members, like this:
    origin.x = 0.0;
    pt->x = 0.0;
    通过origin存取和通过pt存取有什么重大差异吗?
Static Data Members
  • Static data members,按字面意义,被编译器提出class之外,并被视为global变量。每个member的存取许可,以及class的关联,都不会招致任何空间上或执行时间上的额外负担。每个static data member只有一个实例。
  • 但如果static data members是一个从复杂继承关系中继承而来的,它仍然只有唯一一个实例,其存取路径仍然那么直接。如果static data member经由函数调用,或其他某些语法存取呢?例如:
    1
    2
    3
    4
    5
    foobar().chunkSize = 250;

    // 可能的转化
    (void) foobar();
    Point3d.chunkSize = 250;
    若取一个static data member的地址,会得到一个指向其数据类型的指针,而不是指向其class member的指针,因为static member并不内含在一个class object之中:
    1
    2
    3
    &Point3d::chunkSize;
    // 会得到如下类型的内存地址
    const int*
  • 如果有两个classes,每个都声明了一个static member freeList,那么当它们放在程序的data segment时,会导致名称冲突。编译器的解决方式是暗中对每个static data member编码name-mangling),以获得一个独一无二的程序识别代码。任何name-mangling做法都有两个重点:
    1. 一个算法,推导出独一无二的名称
    2. 万一编译系统(或环境工具)必须和使用者交谈,那些独一无二的名称可以轻易被推导回到原来的名称
Nonstatic Data Members
  • Nonstatic data members直接放在每个class object中。除非经由显式explcit)或隐式的(implicit),否则没有办法直接存取它们。只要在member funcion中直接处理一个nonstatic data member,implicit class object就会发生。例如:
    1
    2
    3
    4
    5
    Point3d Point3d::translate(const Point3d &pt) {
    x += pt.x;
    y += pt.y;
    z += pt.z;
    }
    表面上x、y、z直接存取,事实上是经由implicit class object(由this指针表述)完成的。这个函数的参数是:
    1
    2
    3
    4
    5
    6
    // 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)。例如:
    1
    2
    3
    origin._y = 0.0;
    // 相当于
    &origin + (&Point3d::_y - 1);
    每一个nonstatic data member的偏移位置(offset)在编译时期即可获知,甚至如果派生自单一或多重继承串链也是一样。
  • 再来看看虚拟继承。虚拟继承将为经由base class subobject存取class members导入一层间接性。
    1
    2
    origin.x = 0.0;
    pt->x = 0.0;
    从origin存取和从pt存取有什么重大差异?答案是当Pointe3d是一个derived class,而其继承结构中有一个virtual base class,并且被存取member是一个从该virtual base class继承而来的member就会有重大差异。这时候,我们无法在编译时期直到member真正的offset位置。这个存取操作必须延迟到执行期。如果使用origin就不会有这个问题。

3.4 “继承”于Data Member

  • 如果为2D和3D坐标点提供两个抽象数据类型:
    1
    2
    3
    graph BT
    Point2d
    Point3d
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // supporting abstract data types
    class Point2d {
    public:
    // constructor(s)
    // operations
    // access functions
    private:
    float x, y;
    };

    class Point3d {
    public:
    // constructor(s)
    // operations
    // access functions
    private:
    float x, y, z;
    };
    这和提供两层或三层继承结构,每一层(代表一个维度)是一个class,派生自较低维层次有什么不同?
只要继承不要多态(Inheritance without Polymorphism)
  • 我们可能希望不论是2D或3D坐标点,能共享同一个实例,但又能继续使用于类型性质相关的实例。以上的设计策略。带来的影响则是可以共享数据本身以及数据处理方法,并将它局部化。一般而言,具体继承concrete inheritance,相对于虚拟继承virtual inheritance)并不会增加空间或存取时间上的额外负担。
    1
    2
    graph BT
    Point3d --- Point2d
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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+=(const Piont2d &rhs) {
    _x += rhs.x();
    _y += rhs.y();
    }
    // ... more members
    protected:
    float _x, _y;
    };

    // inheritance from concrete class
    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 oeprator+=(const Point3d &rhs) {
    Point2d::operator+=(rhs);
    _z += rhs.z();
    }
    // ... more members
    protected:
    float _z;
    };
    这样的设计的好处是可以把管理x和y坐标的程序代码局部化。也表现处两个类之间的紧密关系。但把原本两个独立不相干的classes凑成一堆”type/subtype“,并带有继承关系,会有什么易犯的错误呢?
    • 经验不足的人可能会重复设计一些相同操作的函数。以例子中的constructor和operator+=为例,它们并没有被做成inline函数。
    • 第二是,把class分解成两层或更多层,可能会为了”表现class继承体系的抽象化“而膨胀所需的空间。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      class Concrete {
      public:
      // ...
      private:
      int val;
      char c1;
      char c2;
      char c3;
      };
      在32位机器中,每个Concrete class object大小是8bytes,细分如下:
    1. val占用4bytes;
    2. c1、c2和c3各占用1bytes;
    3. alignment(调整到word边界)需要1bytes。
    现在把Concrete分裂为三层结构:
    1
    2
    3
    graph BT
    Concrete2 --- Concrete1
    Concrete3 --- Concrete2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Concrete1 {
    public:
    // ...
    private:
    int val;
    char bit1;
    };

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

    class Concrete3 : public Concrete2 {
    public:
    // ...
    private:
    int val;
    char bit3;
    };
    现在Concrete3 object的大小是16bytes,比原先的设计多了100%。(p106)
    1
    2
    3
    4
    5
    6
    7
    Concrete2 *pc2;
    Concrete1 *pc1_1, *pc1_2;
    // 如果C++把derived class members和Concrete1 subobject捆绑在一起,去除填补空间
    pc1_1 = pc2; // pc1_1指向Concrete2对象
    // derived class subobject被覆盖掉
    // 于是bit2 member现在有了一个并非预期的数值
    *pc1_2 = *pc1_1;
加上多态(Adding Polymorphism)
  • 如果在继承关系中提供要给virtual function接口:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class Point2d {
    public:
    Point2d(float x = 0.0, float y = 0.0) : _x(x), _y(y) {};
    // 之前的存取操作
    // 加上z的保留空间(目前什么也不做)
    virtual float z() { return 0.0; }
    virtual void z(float) {}
    // 谁当以下运算符为virtual
    virtual void operator+=(const Point2d &rhs) {
    _x += rhs.x();
    _y += rhs.y();
    }
    // ... more members
    protected:
    float _x, _y;
    };

    class Point3d : public Point2d {
    pblic:
    Point3d(float x = 0.0, float y = 0l0, float z = 0.0) : Ponit2d(x, y), _z(z) {};
    float z() { return _z; }
    void z(float newZ) { _z = newZ; }
    vodi operator+=(const Piont2d &rhs) {
    Point2d::oeprator+=(rhs);
    _z += rhs.z();
    }
    // ... more members
    protected:
    float _z;
    };
    这个设计有个好处是可以把operator+=运用在一个Pointe3d对象和一个Point2对象身上:
    1
    2
    3
    Point2d p2d(2.1, 2.2);
    Point3d p3d(3.1,, 3.2, 3.3);
    p3d += p2d;
    虽然class的声明语法没变,但事情不一样了:z() member function和operator+=()运算符都成了虚函数;每个Point3d class object内含一个额外的vptr member和一个Piont3d virtual table;此外每个virtual member function的调用也复杂了。
  • C++编辑器领域主要讨论的问题是把vptr放置在class object哪里最好?cfont编译器中,被放在class object的尾端:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct no_virts {
    int d1, d2;
    };

    class has_virts : public no_virts {
    public:
    virtual void foo();
    // ...
    private:
    int d3;
    };

    no_virts *p = new has_virts;
    struct no_virts nv; class has_virts :
    public no_virts hv;
    int d1 int d1
    int d2 int d2
      int d3
      __vptr__has_virts
    把vptr放在object尾端,可以保留base class C struct的对象布局,因而允许C程序代码也能使用,这做法出现在C++问世时。
    到了C++2.0,开始支持虚继承以及抽象基类,某些编译器开始把vptr放到class object前端:
    struct no_virts nv; class has_virts :
    public no_virts hv;
    :-: :-:
    int d1 __vptr__has_virts
    int d2 int d1
      int d2
      int d3
    vptr放在class object前端,对于多继承下通过指向class members的指针调用virtual function会有一些帮助。否则,不仅从class object起始点开始量起的offset必须在执行器备妥,class vptr之间的offset也必须备妥
    以下是Point2d和Point3d加上了virtual function之后的继承布局(单一继承):
    class Point2d p2d; class Point3d :
    public Point2d pt3d;
    :-: :-:
    float _x float _x
    float _y float _y
    __vptr__Point2d __vptr__Point2d
      float _z
多重继承(Multiple Inheritance)
  • 单一继承提供了一种自然多态natural polymorphism)形式,是关于base type和derived type之间的转换。base 它们的object都是从相同的地址开始。差异只在derived object比较大,用以容纳它自己的nonstatic data members。一个derived class指定给base class的指针或reference。这个操作并不需要编译器调停或修改地址。可以很自然地发生,而且提供了最佳执行效率。
  • 多重继承不像单一继承,也不容易模塑处模型。它的复杂度在于derived class和其base class之间的非自然关系:
    1
    2
    3
    4
    graph BT
    Point3d --- Point2d
    Vertex3d --- Point3d
    Vertex3d --- Vertex
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class Point2d {
    public:
    // ...有virtual接口,会有vptr
    protected:
    float _x, _y;
    };

    class Point3d : public Point2d {
    public:
    // ...
    protected:
    float _z;
    };

    class Vertex {
    public:
    // ... vptr
    protected:
    Vertex *next
    };

    class Vertex3d : public Point3d, public Vertex {
    public:
    // ...
    protected:
    float mumble;
    };
    多重继承的问题主要发生在derived class objects和后继base class objects之间的转换,不论是:
    1
    2
    3
    4
    extern void mumble(const Vertex&);
    Vertex3d v;
    ...
    mumble(v); // 不自然
    或是经由所支持的virtual function机制做转换。多继承对象符出的成本在于地址的指定操作而已:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Vertex3d v3d;
    Vertex *pv;
    Point2d *p2d;
    Point3d *p3d;

    // 指定操作
    pv = &v3d;
    // 需要内部转化
    pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d));
    // 如果v3d为指针类型,即pv3d,则:
    pv = pv3d ? (Vertex*)((char*)pv3d) + sizeof(Point3d) : 0; // 防止pv3d为0
    以下为多重继承Multiple Inheritance):
    class Point2d pt2d;
    float _x
    float _y
    __vptr__Point2d
    class Point3d :
    public Point2d pt3d;
    :-:
    float _x
    float _y
    __vptr__Point2d
    float _z
    Vertex v;
    :-:
    Vertex *next
    __vptr__Vertex
    class Vertex3d :
    public Point3d,
    public Vertex
    {}v3d;
    :-:
    float _x
    float _y
    __vptr__Point2d
    float _z
    Vertex *next
    __vptr__Vertex
    float mumble
虚拟继承(Virtual Inheritance)
  • 多重继承的一个语意上的副作用是,必须支持某种形式上的”shared subobject继承“。如早期的iostream library:
    1
    2
    3
    4
    5
    // pre-standard iostream implementation
    class ios { ... };
    class istream : public ios { ... };
    class ostream : public ios { ... };
    class iostream : public istream, public ostream { ... };
    下图可表现iostream的继承体系图。第一个为多重继承,第二个为虚拟多重继承:
    1
    2
    3
    4
    5
    graph BT
    istream --- ios1[ios]
    ostream --- ios2[ios]
    iostream --- istream
    iostream --- ostream
    1
    2
    3
    4
    5
    graph BT
    istream --- ios
    ostream --- ios
    iostream --- istream
    iostream --- ostream
    在iostream对象布局中,只需要一份就好:
    1
    2
    3
    4
    class ios { ... };
    class istream : public virtual ios { ... };
    class ostream : public virtual ios { ... };
    class iostream : public istream, public ostream { ... };
    上述iostream的实现挑战在于:**一个有效的方法,将istream和ostream各自维护ios subobjet,折叠成由iostream维护的单一ios subobject,并且保存base class和derived class的指针(reference)之间的多态指定操作polymorphism assignments)**。
  • 一般的实现方法是。Class如果内含一个或多个virtual base class subobjects,像istream那样,将被分割两部分:一个不变区域一个共享区域。不变区域中的数据,总是有固定的offset,所以这里可以直接存取。而共享区域,就是virtual base class subobject。它们只能被间接存取。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class Point2d {
    public:
    ...
    protected:
    float _x, _y;
    };

    class Vertex : public virtual Point2d {
    public:
    ...
    protected:
    Vertex *next;
    };

    class Point3d : public virtual Point2d {
    public:
    ...
    protected:
    float _z;
    };

    class Vertex3d : public Vertex, public Point3d {
    public :
    ...
    protected:
    float mumble;
    };
    1
    2
    3
    4
    5
    6
    graph BT
    Vertex -- _x,_y--- Point2d
    Point3d -- _z --- Point2d
    Vertex3d -- next --- Vertex
    Vertex3d -- _z --- Point3d
    none[ ] -- mumble --- Vertex3d
    这中间存在一个问题:如何能够存取class共享部分呢?cfont编译器是在derived class object中插指针,每个指向virtual base class。需要完成操作都是通过指针间接完成的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void Point3d::operator+=(const Point3d &rhs)
    {
    _X += rhs._x;
    _y += rhs._y;
    _z += rhs._z;
    };

    // 转化为虚构代码vbc为virtual base class
    __vbcPoint2d->_x += rhs.__vbcPoint2d->_x;
    __vbcPoint2d->_y += rhs.__vbcPoint2d->_y;
    _z += rhs._z;
    这样实现模型有两个主要缺点:
    1. 每个对象针对其每个virtual base class背负一个额外的指针。而我们希望class object负担是稳定的。
    2. 虚继承链加长会导致存取层次增加。我们呢希望有着固定的存取时间。
  • 对于第二个问题。MetaWare和其他编译器到今天还是用cfont原始模型:它们经由拷贝操作取得所有nested virtual base class指针,放到derived class object中,虽然有空间代价。
  • 至于第一个问题,有两个解决方法。Microsoft编译器引入virtual base class table。每个class object如果有一个以上virual base classes,编译器就安插指针,指向virtual base class table,真正的vptr则被放在该表格中。第二个解决方法,是在virtual function table中放置virtual base class的offset(而非地址)。
    class Vertex3d :
    public Vertex,
    public Point3d
    {…} v3d;
    Vertex *next
    Point2d *pPoint2d
    __vptr__Vertex
    float _z
    Point2d *pPoint2d
    __vptr__Point3d
    float mumble
    float _x
    float _y
    __vptr__Point2d
    该方法把virtual base class offsetvirtual function entires混杂在一起。Sum编译器中,virtual function table由正值或负值来索引。正值则索引到virtual functions;负值则索引到virtual base class offsets。
    1
    2
    3
    4
    5
    (this + __vptr__Point3d[-1])->_x +=
    (&rhs + rhs.__vptr__Point3d[-1])->_x;
    (this + __vptr__Point3d[-1])->_y +=
    (&rhs + rhs.__vptr__Point3d[-1])->_y;
    _ += rhs._z;
    因此Derived class实例和base class实例之间的转换操作为:
    1
    2
    3
    Point2d *p2d = pv3d;
    // translation
    Point2d *pt2 = pv3d ? pv3d + pv3d->__vptr__Point3d[-1] : 0;

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

  • 下面测试旨在测试聚合aggregation)、封装encapsulation)以及继承inheritance)所引发的额外负荷程序。跳过。

3.6 指向Data Members的指针(Poniter to Data Members)

  • 考虑下面Point3d声明。有一个virtual function,一个static data member,以及三个坐标:
    1
    2
    3
    4
    5
    6
    7
    8
    class Point3d {
    public:
    virtual ~Point3d();
    // ...
    protected:
    static Point3d origin;
    float x, y, z;
    };
    每个Point3d object含有三个坐标值,依序为x、y、z,以及vptr。static data member origin放在class object之外。vptr的位置根据编译器不同而不同。不是放头就是尾。&Point3d::z;取得某个坐标成员的地址代表什么呢?代表着z坐标在class object中偏移位置(offset)
  • 一台32位机器上,每一个float是4bytes,所以期望获取地址偏移位置要么是8要么是12。然而获取的总是多1,也就是9和13(我的输出并没有+1)。
  • 在多继承下,要将第二个或后继base class的指针,和一个与derived class object绑定的member结合起来,那么将会因为需要加入offset值而变得复杂。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    struct Base1 { int val1; };
    struct Base2 { int val2; };
    struct Derived : Base1, Base2 { ... };
    void func1(int Derived::*dmp, Derived *pd)
    {
    // 期望第一个参数是一个derived class的member指针,结果是base class的会怎样。
    pd->*dmp;
    }

    void func2(Derived *pd)
    {
    // bmp将成为1
    int Base2::*bmp = &Base2::val2;
    // bmp == 1但在Derived中val2 == 5
    func1(bmp, pd);
    }
    要解决这个问题,必须:
    1
    2
    // 经由编译器内部转换
    func1(bmp ? bmp + sizeof(Base1) : 0, pd);
“指向Members的指针”的效率问题
  • 下面是测试数据。了解在3D坐标点的不同class表现方式下指向members的指针所带来的影响。