Data语义学——指向Data Members的指针

前言

 
通过指向Data Members的指针,我们可以更加细致地探究对象布局,比如说了解vptr是在顶端还是底端,又或者说了解class中access sections的次序。


问题实例

 
考虑下面的Point3d,其内含一个virtual function,一个static member,以及三个坐标值:

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

该class object内部会含有vptr,我们不能确定的是其位于头部还是尾部。

在一个32位的机器上,一个float是4bytes,一个vptr也是4bytes。因此,三个坐标值在对象中的偏移量应该是0,4,8(vptr位于尾部)或4,8,12(位于头部)。但如果你去取data members的地址,传回的值会总是多1,也就是1,5,9或5,9,13。下面将详细描述原因。


问题剖析

 
如何区分一个“没有指向任何data member”的指针和一个“指向第一个data member的指针”?考虑如下例子:

1
2
3
4
5
6
float Point3d::*p1 = 0;//Point3d::*的意思在于“指向Point3d data member”的指针类型
float Point3d::*p2 = &Point3d::x;

if(p1==p2){//如何区分p1 p2?
cout << "contain same value" << endl;
}

为了区分p1与p2,每一个真正的member offset值都会被加1,因此编译器和开发者都应当明确,在真正使用该值以指出一个member之前,先减1。

真正Data地址与offset

在明确了“指向data members的指针”之后,我们发现解释& Point3d::z& origin.z之间的差异则非常容易。
取一个nonstatic data member的地址将获得其在class中的offset,而取一个绑定于真正object身上的data,将会得到该member在内存中的真正地址。。那么我们执行

1
& origin.z - & Point3d::z

返回得到的就是origin的其实地址,但其类型是float *而非float Point3d::*


多重继承

 
在多重继承下,如果需要将后继base class的指针和一个“derived class object data member”相结合,会由于需要加入offset而变得相当复杂,举例而言:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Base1 {int val1;};
struct Base2 {int val2;};
struct Derived:Base1,Base2 {...};
void func1(int derived::*dmp,Derived *pd){
//期待的是一个指向derived的data member指针
//但实际传入的可能是一个指向base class的data member的指针
pd->*dmp;
}
void func2(Derived *pd){
//bmp==1,但在derived中,val2==5
int Base2::*bmp = &Base2::val2;
func1(bmp,pd);
}

当bmp被作为func1的第一个参数时,其值必须受到调整,否则pd->*dmp将存取到Base::val1,而非开发者所以为的Base::val2。为了解决这个问题,必须由编译器转为:
1
2
//非null判定
func1(bmp?bmp+sizeof(Base1):0,pd);


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

 
接下来作者以一系列数据比较了不同编译器优化下经由指针存取members的效率。