第3章 Data语意学

第3章 Data语意学

类X, Y, Z, A具有如下关系

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

int main() {
    cout << "sizeof(X): " << sizeof(X) << endl;		// X: 1
    cout << "sizeof(Y): " << sizeof(Y) << endl;		// Y: 8,  和编译器有关
    cout << "sizeof(Z): " << sizeof(Z) << endl;		// Z: 8,  和编译器有关
    cout << "sizeof(A): " << sizeof(A) << endl;		// A: 12, 和编译器相关
    return 0;
}

/*vs2013运行结果
sizeof(X): 1
sizeof(Y): 4
sizeof(Z): 4
sizeof(A): 8 
请按任意键继续. . .
*/
  • 对于X: 编译器安插进一个char, 使得类x的对象可以在内存中配置独一无二的地址
  • 对于Y和Z: Y和Z的大小受3个因素的影响
      - 语言本身所造成的额外负担: 这个额外负担反映在某种形式的指针身上, 他或者指向virtual base class subobject, 或者指向一个相关的表格(表格中存存放的不是"虚基类子对象"的地址, 就是其偏移量, 将在3.4节讨论)
      - 编译器对特殊情况所提供的优化处理: (1) 传统上, 虚基类X子对象的1bytes的大小也出现在Y和Z中, 被放在子类的固定(不变动)部分的尾端(左图); (2)"某些"编译器会对"空需基类"提供特殊的支持, empty virtual base class被视为derived class object最开头的一部分, 即不花费任何空间. 因此节省了传统情况下的1byte, 因为既然有了成员, 就不需要为了"空类"而安插一个char(右图)
      - alignment的限制: 类Y和Z的大小截止至目前是5bytes, 在大部分机器上, 聚合的结构体大小会受到对齐的限制, 为了能更有效率地在内存中被存取, 会进行字节填充
  • 对于A: 类A的大小由下列因素决定
      - 被共享的唯一一个类X实例, 大小为1byte
      - 类Y的大小减去"因virtual base class X而配置的大小", 即4byets, 1bytes+3bytes(因对齐而填充的3bytes)
      - 类Z的大小减去"因virtual base class X而配置的大小", 即4byets, 1bytes+3bytes(因对齐而填充的3bytes)
      - 类A自己的大小, 0byte
      - 对齐要求
      - VS2013是"有特殊处理"的编译器, 对class Y和class Z做了特殊的支持, 没有因空的class X而配置一个空字节, 所以class A的大小为class Y与class Z之和

如果在virtual base classX中放置一个(以上)的data members, 两种编译器("有特殊处理"者和"没有特殊处理"者)就会产生出完全相同的对象布局

class X { int a = 0; };
class Y : public virtual X {};
class Z : public virtual X {};
class A : public Y, public Z {};

int main() {
    cout << "sizeof(X): " << sizeof(X) << endl;		
    cout << "sizeof(Y): " << sizeof(Y) << endl;		
    cout << "sizeof(Z): " << sizeof(Z) << endl;		
    cout << "sizeof(A): " << sizeof(A) << endl;		
    return 0;
}

/*
sizeof(X): 4
sizeof(Y): 8
sizeof(Z): 8
sizeof(A): 12
请按任意键继续. . .
*/

C+ Standard并不强制规定如"base class subobjects排列顺序"或"不同存取层级的data members的排列顺序"这种琐碎细节. 他也不规定virtual functions或virtual base classes的实现细节. C++ Standard只是说: 那些细节由各家厂商自定

C++对象模型尽量以空间优化和存取速度优化的考虑表现nonstatic data members, 并且保持和C语言struct数据配置的兼容性. 它把数据直接存放在每一个class object之中. 对于继承而来的nonstatic data members(不管是virtual还是nonvirtual base class)也是如此. 不过并没有强制定义其空间的排列顺序. 至于static data members, 则被放置在程序的一个全局数据段中(global data segment), 不影响个别class object的大小, 但是一个template class的static data members稍有不同

每一个class object因此必须有足够的大小以容纳它所有的nonstatic data members. 有时候其值可能令人吃惊, 因为它可能比想象的还大, 原因是:

  1. 有编译器自动加上的data members, 用以支持某些语言特性(主要是各种virtual特性)
  2. 因为alignment(边界调整)的需要

3.1 Data Member的绑定

  • 成员函数体内的名字解析直到类的声明全出现了才开始
  • 成员函数的参数列表不符合上一条规则, 名字解析从参数第一次出现开始

成员函数体解析过程

/**
 * 成员函数体内的名字解析, x为Point3d的成员
 */
extern int x;

class Point3d {
public:
    // 对于函数本体的分析将延迟,  
	// 直至class声明的大括号出现才开始
    float X() const { return x; };
private:
    float x;
};		// 函数体内的解析从这开始

成员函数的参数列表解析过程

/**
 * 成员函数参数列表的名字解析
 */

typedef int length;

class Point3d {
public:
	// 参数列表的解析在第一次遇到就开始解析
    // length被决议(resolved)为global
	// _val被决议(resolve)为Point3d::_val
    void mumble(length val) { _val = val; };
    length mumble() { return _val; };
    //...
private:
    // length必须在"本class对他第一个参考操作"之前看见
    // 这样的声明将使先前的参考操作不合法, 要总把"内嵌的类型声明"放在class的起始处
    typedef float length;
    length _val;
};

上述这个语言状况, 仍然需要某种防御性程序风格: 请总是把"nested type声明"放在class的起始处. 在上述例子中, 如果把length的nested typedef定义于"在class中被参考"之前, 就可以确保非直觉绑定的正确性

3.2 Data Member的布局

access section: private, public, protected等段

  • 在同一个access section中的nonstatic data members在class object中的排列顺序将和其他被声明的顺序一样
      - C++标准要求, 统一access section中, members的排列只需符合"较晚出现的members在class object中有较高的地址"即可, 各个members并不一定得连续排列: (1)边界调整可能会填充一些字节; (2)编译器可能会合成一些内部使用的data members, 如vptr(传统上它被放在所有显示声明的members的最后. 不过如今有一些编译器把vptr放在一个class object的最前端. C++标准允许这些内部产生出来的members自由的放在任何位置上, 甚至放在那些被程序员声明出来的members之间)
  • C++标准允许将多个access section之中的data members自由排列, 不必在乎它们出现在class声明中的顺序, access section的多寡并不会招来额外负担
class Point3d {
public:
	// ...
private:
	float x;
	static List<Point3d *> *freeList;
private:
	float y;
	static const int chunkSize = 250;
private:
	flaot z;
};

members的排列顺序视编译器而定, 编译器可以随意把y或z或其他什么东西放在第一个. 目前各家编译器都是把一个以上的access sections连锁在一起, 依照声明的顺序, 称为一个连续区块. access section的多寡并不会招来额外负担. 例如在一个section中声明8个members, 或是在8个section中总共声明8个members, 得到的object大小是一样的

3.3 Data Member的存取

考虑下列代码:

Point3d origin , *pt = &rigin;

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

通过origin存取和通过pt存取有什么差异?

Static Data Members

这是C++中"通过一个指针和通过一个对象来存取member, 结论完全相同"的唯一一种情况. 这是因为"经由'.运算符'对一个static data member进行存取操作"只是文法上的一种便宜行事而已.

member其实并不在class object之中, 因此存取static member并不需要class object(意思是不需要定义一个对象出来), 若存取一个static data member的地址, 将会得到一个指向其数据类型的指针;&Point3d::chunkSize;会等到const int *

如果有两个类, 每一个都声明了一个同名的static member, 那么它们都被放在程序的data segment时, 就会导致命名冲突. 编译器解决的方法是暗中对每一个static member编码, 以获得独一无二的程序识别码

Nonstatic Data Members

nonstatic data members直接存放在每一个class object之中. 除非经由显式的(explicit)或隐式的(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指针表达)完成的
// 事实上这个函数的参数是:

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);

请注意其中的-1操作. 指向data member的指针, 其offset值总是被加上1, 这样可以使编译系统区分出"一个指向data member的指针, 用以指出class的第一个member"和"一个指向data member的指针, 没有指出任何member"两种情况. "指向data members的指针"将在3.6节有比较详细的讨论

每一个nonstatic data member的偏移位置在编译使其即可获知, 甚至如果member属于一个base class subobject(派生自单一或多重继承串链)也是一样. 因此, 存取一个nonstatic data member的效率和存取一个C struct member或一个nonderived class的member是一样.

但是, 虚继承将为"经由base class subobject存取class members"导入一层新的间接性, 如

Point3d *pt3d;
pt3d->_x = 0.0;

其执行效率在_x是一个struct member, 一个class member, 单一继承, 多重继承的情况下都完全相同. 但如果_x是一个virtual base class的member, 存取速度就会稍微慢一点. 下一节会验证"继承对于member布局的影响"

通过origin存取和通过pt存取有什么差异?

origin.x = 0.0;	// 编译时就可固定
pt->x = 0.0;	// 若是从虚基类继承而来, 会有差异, 存取操作会延迟至执行期

"当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位置也在编译时期固定了. 一个积极进取的编译器甚至可以静态地经由origin就解决掉对x的存取

3.4 继承与Data Member

C++继承模型中, 一个派生对象所表现出来的东西, 使其自己的members加上其base class(es) members的总和. 至于派生类成员和基类成员的排列顺序, 则未在C++ Standard中强制指定; 理论上编译器可以自由安排之. 在大部分编译器上, base class members总是先出现, 但属于virtual base class的除外

继承体系中类成员的布局分为5种情况讨论: (1)不使用继承; (2)不含多态的继承; (3)含多态的继承; (4)多重继承; (5)虚继承

(1) 不使用继承

(2) 只要继承不要多态

由于C++标准没有强制指定派生类和基类成员的排列顺序; 理论上编译器可以自由安排. 在大部分编译器上, 基类成员总是先出现(属于虚基类的除外)

将两个原本独立不相干的类凑成一对"类型/子类型(type/subtype)", 并带有继承关系, 需要注意两点
1.可能会重复设计一些相同操作的函数
2.把一个类分成两层或更多层, 出现在派生类中的基类子对象具有完整原样性, 因此可能会导致对象空间膨胀

/**
 * 不使用继承
 */
class Concrete {
public:
    // ...
private:
    int val;
    char c1;
    char c2;
    char c3;
};

/**
 * 使用继承, 设计多层结构
 */
class Concrete1 {
public:
    // ...
private:
    char bit2;
};

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

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

在使用继承时, 派生类中的基类子对象具有完整原样性, 派生类部分的成员不使用基类子对象的填充部分是因为: 如果将一个父类对象拷贝给一个派生类对象, 派生类对象的派生类成员会被覆盖:
这地方没太懂

(3) 加上多态

相比于不含多态的多继承, 这种情况下, 每一个class object会多些负担 1. 导入一个和class有关的virtual table, 用来存放它所声明的每一个virtual functions的地址. 这个table的元素个数一般而言是被声明的virtual functions的个数, 在加上一个或两个slots(用以支持runtime type identification) 2. 在每一个class object中导入一个vptr, 提供执行期的连接, 使每一个object能够找到对应的virtual table 3. 加强constructor, 使它能够为vptr设定初值, 让它向class所对应的virtual table. 这可能意味着在derived class和每一个base class的constructor中, 重新设定vptr的值. 其情况视编译器优化的积极性而定. (第5章对此有比较详细的讨论) 4. 加强destructor, 使它能够抹消"指向class之相关virtual table"的vptr. 要知道, vptr很可能已经在derived class destructor中被设定为derived class的virtual table地址. 记住, destructor的调用顺序是反向的: 从derived class到base class. 一个积极的优化编译器可以压制那些大量的指定操作

某些编译器会把vptr放置在class object的尾端, 另一些编译器会把vptr放置在class object的首部

  • 把vptr放在class object的尾端, 可以保留base class C struct的对象布局, 因而允许在C程序代码中也能使用. 这种做法在C++问世之初, 被许多人使用, 如左图
  • 把vptr放在class object的首部, 对于"在多重继承之下, 通过指向class members的指针调用virtual function", 会带来一些帮助(参考4.4节). 否则, 不仅"从class object起始点开始量起"的offset必须在执行期备妥, 甚至与class vptr之间的offset也必须备妥. 当然, vptr放在前端, 代价就是丧失了C语言的兼容性.(好像没有人从C struct派生出一个具有多态性质的class), 如右图

假设把vptr放在base class的尾端, 则Point2d和Point3d的成员布局如下(Point3d中并没有添加新的virtual functions, 所以只有Point2的vptr)

(4) 多重继承

  • 将vptr放在class object的尾端, 提供了一种"自然的多态"形式, 是关于classes体系中的base type和derived type之间的转换. base class和derived class的object都是从相同的地址开始, 因此把一个派生类对象的地址指定给基类的指针或引用时, 不需要编译器去调停或修改地址. 它很自然的可以发生, 提供了最佳的执行效率
  • 将vptr放在class object的首端, 如果基类没有virtual function而派生类有, 那么单一继承的"自然多态"就会被打破. 这种情况下把一个派生类object转换为其基类型, 就需要编译器的接入, 用以调整地址(因vptr插入之故). 在既是多重继承又是虚继承的情况下, 编译器的介入更有必要

对一个多重派生对象, 将其地址指定给"最左端(也就是第一个)基类的指针", 情况将和单一继承时相同, 因为二者都指向的相同的起始地址. 需要付出的成本只有地址的指定操作而已; 至于第二个或后继的基类的地址指定操作, 则需要将地址修改过: 加上(或减去, 如果downcast的话)介于中间的基类子对象大小

class Point2d {
public:
    // ... (拥有virtual接口, 所以Point2d对象之中会有vptr)
protected:
    float _x, _y;
};

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

class Vertex {
public:
    // ... (拥有virtual接口, 所以vertex对象之中会有vptr)
protected:
    Vertex *next;
};

class Vertex3d : public Point3d, public Vertex{
public:
    // ...
protected:
    float numble;
};

假设将vptr放在class object的尾端, 类的继承关系和members的布局如下

多继承的问题主要发生于derived class objects和其第二或后继的base class objects之间的转换, 或是经由其所支持的virtual function机制做转换. 因而支持"virtual function的调用操作"而引发的问题将在4.2节讨论

extern Void mumble(const Vertex &);
Vertex3d v;
// 将一个Vertex3d转换为一个Vertex. 这是"不自然的"
numble( v );

C++ Standard并未要求Vertex3d中的基类Point3d和Vertex有特定的排列顺序. 原始的cfront编译器是根据声明顺序来排列它们. 因此cfront编译器制作出来的Vertex3d对象, 将可被视为一个Point3d子对象(其中又有一个Point2d子对象)加上一个Vertex子对象, 最后再加上Vertex3d自己的部分. 目前各编译器仍然以此方式完成多重基类的布局(但如果加上虚继承, 就不一样了)

在这个例子中:

  • 如果想一个Vertex3d类的对象的地址指定给Vertex类的指针, 那么需要编译器介入, 执行相应的地址转换
  • 如果将地址指定给Point2d或Point3d类的指针, 则不需要编译器接入
Vertex3d v3d;
Vertex *pv;
Point2d *p2d;
Point3d *p3d;

那么pv = &v3d;需要这样的内部转化pv = (Vertex*)(((char *)&v3d) + sizeof(Point3d));

p2d = &v3d; p3d = &v3d都只需要简单拷贝其地址就好

Vertex3d *pv3d;
// 一系列操作, 使得pv3d可能指向NULL或者一个Vertex3d对象
Vertex *pv;

此时的pv = pv3d;不能只是简单的pv = (Vertex*)(((char *)&v3d) + sizeof(Point3d));这么转换, 因为如果pv3d为0, pv将获得sizeof(Point3d)的值. 这明显是错误的, 因此内部转化需要有一个条件测试: pv = pv3d ? (Vertex *)((char *)pv3d) + sizeof (Point3d) : 0; 若是引用, 则不需要针对0值做防卫, 因为引用不可参考到"无物"(意思是pv3d不能是空指针?)

(5) 虚继承

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

iostream的继承体系, 左为多继承, 右为虚继承

要在编译器中支持虚继承, 实在难度颇高. 以iostream为例, 实现技术的挑战在于, 要找到一个足够有效的方法, 将istream和ostream各自维护的一个ios子对象, 折叠成为一个由iostream维护单一ios子对象, 并且还可以保持基类和派生类的指针(以及引用)之间的多态指定操作

一般的实现方法如下所述, 如果一个类内含有一个或多个虚基类子对象, 想istream那样, 将被分隔为两部分: 一个不变区和一个共享区域

  • 不变区域: 不管后继如何衍化, 总是拥有固定的offset(从object的开头算起), 所以这一部分可以被直接存取
  • 共享区域: 所表现的就是虚基类子对象. 这一部分的数据, 其位置会因为每次的派生操作而有变化, 所以它们只可以被间接存取, 各家编译器实现技术之间的差异就在于间接存取的方法

一般的布局策略是先安排好派生类的不变部分, 然后再建立共享部分
然而, 这中间存在一个问题: 如何能够存取class的共享部分? cfront编译器会在每一个派生类对象总安插一些指针, 每个指针指向一个虚基类. 要存取继承来的虚基类成员, 可以通过相关指针间接完成

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

// 在cfront策略下, 这个运算符会被内部转换为:
// 虚拟C++代码
__vbcPoint2d->_x += rhs.__vbcPoint2d->_x;	// vbc意为
__vbcPoint2d->_y += rhs.__vbcPoint2d->_y;	// virtual base calss
_z += rhs._z;

// 而一个derived class和一个base class
// 的实例之间的转换:
Point2d *p2d = pv3d;

// 在cfront实现模型之下, 会变成:
// 虚拟C++代码
Point2d *p2d = pv3d ? pv3d->__vbcPoint2d : 0;

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

  1. 每一个对象必须针对其每一个virtual base class背负一个额外的指针, 然而理想上却希望class object有固定的负担, 不因为其virtual base class的个数而有所变化, 解决办法有两种
      (1) Microsoft编译器引入所谓virtual base class table. 每一个class object如果有一个或多个virtual base classes, 就会由编译器安插一个指针, 指向virtual base class table. 至于真正的virtual base class指针, 当然存放在该表格中
      (2) 在virtual function table中存放virtual base class的offset(而不是地址), 这样将virtual base class offset和virtual function entries混杂在一起. 在新近的Sun编译器中, virtual function table可经由正值或负值来索引. 如果是正值, 很显然就是索引到virtual functions; 如果是负值, 则索引到virtual base class offset如下图
// Point3d的operator+=运算符必须被转换为以下形式
// 为了可读性, 没有做类型转换, 
// 也没有先执行对效率有帮助的地址预先计算操作
// 虚拟C++代码
(this + __vptr__Point3d[-1])->_x += (&rhs + rhs.__vptr__Point3d[-1])->_x;
(this + __vptr__Point3p[-1])->_y += (&rhs + rhs.__vptr__Point3d[-1])->_y;
(this + __vptr__Point3p[-1])->_z += (&rhs + rhs.__vptr__Point3d[-1])->_z;

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

上述每一种方法都是一种实现模型, 而不是一种标准. 每一种模型都是用来解决"存取shared subobject内的数据(其位置会因每次派生操作而有变化)"所引发的问题. 由于对virtual base class的支持带来额外的负担以及高度的复杂性, 每一种实现模型多少都有点不同, 可能还会随着时间而进化
2. 由于虚继承串链的加长, 导致间接村缺层次的增加. 意思是如果有三次虚派生, 就需要经由3个virtual base class指针进行3次间接存取. 理想上希望能有固定的存取时间, 不因虚拟派生的深度而改变
  MetaWare和其它编译器仍然使用cfront的原始模型来解决这个问题, 它们经由拷贝操作取得的所有nested virtual base class指针, 放到derived class object之中. 从而解决了"固定时间存取"的问题, 虽然付出了一些空间上的代价. MetaWare提供一个编译时期的选项, 允许程序员选择是否要产生双层指针

经由一个非多态的class object来存取一个继承而来的virtual base class的member: Point3d origin; origin._x;可以被优化为一个直接存取操作, 就好像一个经由对象调用的virtual function调用操作, 可以在编译时期被决议(resolved)完成一样. 这次存取以及下一次存取之间, 对象的类型不可以改变, 所以"virtual base class subobject的位置会变化"的问题在此情况下就不再存在了

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

3.5 对象成员的效率

3.6 指向Data Members的指针

class Point3d {
public:
    virtual ~Point3d() {};
public:
    static Point3d origin;	// 静态数据成员存储位置在类外
    float x, y, z;
};

int main() {
/*
	VS中可能做了特殊处理, 地址没有显示出+1处理, 但还是+1了
*/
    // Point3d::*的意思是: "指向Point3d data member"的指针类型
    float Point3d::*p1 = &Point3d::x;
    float Point3d::*p2 = &Point3d::y;
    float Point3d::*p3 = &Point3d::z;
    // 不可以用cout
    printf("&Point3d::x = %p
", p1);		// 根据机器和编译器决定, 本机: 0x4; 注释虚函数后: 0x0;
    printf("&Point3d::y = %p
", p2);		// 根据机器和编译器决定, 本机: 0x8; 注释虚函数后: 0x4;
    printf("&Point3d::z = %p
", p3);		// 根据机器和编译器决定, 本机: 0xc; 注释虚函数后: 0x8;

    Point3d p;
    p.x = 1.1;
    p.y = 2.2;
    p.z = 3.3;
    // x:1.1 y:2.2 z:3.3
    cout << "x: " << p.*p1 << " y: " << p.*p2 << " z: " << p.*p3 << endl;

    return 0;
}

&Point3d::z将得到z坐标在class object中的偏移位置. 最低限度其值将是x和y的大小总和, 因为C++要求统一access section中的members的排列顺序应该和其声明顺序相同(注意是顺序, 并不是连续)

如果vptr放在对象的尾端, 三个坐标值在对象的布局中的偏移量分别是0, 4, 8. 如果vptr放在对象的头部, 三个坐标值在对象的布局中的offset分别是8, 12, 16(64位机器). 然后的结果可能会加1, 即1, 5, 9或者9, 13, 17. 这是为了区分一个"没有任何data member"的指针, 和一个指向"第一个data member"的指针(对象的内存分布并没有增加1, 这里只是编译器可能对指针的处理):

#include<iostream>
using namespace std;

class Point3d {
public:
    //virtual ~Point3d() {};
public:
    static Point3d origin;  // 静态数据成员存储位置在类外
    float x, y, z;
};

int main() {
    /*
    VS中可能做了特殊处理, 地址没有显示出+1处理, 但还是+1了
    */
    // Point3d::*的意思是: "指向Point3d data member"的指针类型
    float Point3d::*p1 = 0;		// "没有任何data member"的指针
    float Point3d::*p2 = &Point3d::x;	// 指向"第一个data member"的指针
    
    if (p1 == p2) {
        cout << "p1 & p2 contain the same value --";
        cout << " they must address the same member!" << endl;
    }

    return 0;
}

/*
请按任意键继续. . .
*/

为了区分p1和p2, 每一个真正的member offset的值都被加上1(如测试的结果, 如果没有增加1, 可能是编译器做了特殊处理, 没有显示出+1, 但实际上好像做了+1处理). 因此, 无论编译器或者使用者都必须记住, 在真正使用该值以指出一个member之前要减去1. 因此,&Point3d::z&origin.z之间的差异, 就分成明确了, "取一个nonstatic data members的地址"将会得到它在class中的offset, 取一个"绑定于真正class object身上的data member"的地址就会等到该member在内存中的真正地址

"指向Members的指针"的效率问题

原文地址:https://www.cnblogs.com/hesper/p/10595708.html